Phase 2 renamed the routes to /uscg-distribution/* during the extraction of the Distributed companion plugin. ProjectStructure.md (8 occurrences) and README.md (6 occurrences) still referenced the old paths. Caught alongside the worker URL bug during Phase C smoke test.
868 lines
53 KiB
Markdown
868 lines
53 KiB
Markdown
### **ComfyUI Registry Security Requirements**
|
||
|
||
This node is published to the ComfyUI Registry. Every version is scanned by an automated
|
||
security scanner. Versions that fail the scan receive `NodeVersionStatusFlagged` status and
|
||
are hidden from ComfyUI Manager. **All contributors must follow these rules:**
|
||
|
||
1. **No `import requests`** — Use `urllib.request` (stdlib) instead. The scanner flags
|
||
outbound HTTP libraries as potential data exfiltration vectors.
|
||
2. **No `subprocess`, `os.system`, `eval()`, `exec()`** — These are blocked as arbitrary
|
||
code execution risks.
|
||
3. **No runtime `pip install`** — Package installation must go through `requirements.txt`
|
||
only, never called at runtime.
|
||
4. **No code obfuscation** — No encoded strings, multi-statement tricks, or undefined
|
||
variable patterns.
|
||
5. **No custom file-serving endpoints** — Use ComfyUI's built-in `WEB_DIRECTORY` for JS and
|
||
the built-in `/view` endpoint for images. Never use `web.FileResponse` or create custom
|
||
routes that serve files from disk. For external images, create symlinks into the output
|
||
directory and use `/view?filename=X&type=output&subfolder=Y`.
|
||
6. **Path containment on all filesystem operations** — Every endpoint that touches the
|
||
filesystem must validate that resolved paths stay within the expected base directory
|
||
(e.g., `output/benchmarks/`). Use `os.path.realpath()` + `startswith()` checks.
|
||
7. **Sanitize all user-supplied path components** — Session names, filenames, etc. must be
|
||
sanitized with `re.sub(r'[^\w\-]', '', name)` or `os.path.basename()` before use.
|
||
|
||
Reference: https://blog.comfy.org/p/comfyui-2025-jan-security-update
|
||
|
||
---
|
||
|
||
### **Project File Structure**
|
||
|
||
```text
|
||
ComfyUI-Ultimate-Sampler-Grid/
|
||
├── __init__.py # Server Routes & Node Mappings (933 lines)
|
||
├── sampler_node.py # Main "Sampler Grid" Node (314 lines)
|
||
├── dashboard_node.py # "Dashboard Viewer" Node (68 lines)
|
||
├── config_builder_node.py # "Config Builder" Node + API endpoints (1167 lines)
|
||
├── json_text_node.py # "Smart JSON" Node (60 lines)
|
||
│
|
||
├── generation_orchestrator.py # The "Conductor" (Loop Logic, 2281 lines)
|
||
├── image_generation.py # KSampler & VAE Wrappers (748 lines)
|
||
├── model_loader.py # Checkpoint/LoRA/VAE/GGUF Loading Logic (805 lines)
|
||
├── model_cache.py # 3-Tier Model Caching System (900 lines)
|
||
├── remote_vae.py # Remote VAE Offloading (340 lines)
|
||
│
|
||
├── batch_encoding.py # Batch CLIP Encoding with Combinator Support (378 lines)
|
||
├── conditioning_cache.py # Disk-based Conditioning Cache (372 lines)
|
||
├── lora_utils.py # LoRA Searching & Validation (481 lines)
|
||
├── trigger_words.py # CivitAI Trigger Word Fetching & Prompt Assembly (377 lines)
|
||
├── config_utils.py # Config Parsing, Expansion & Cartesian Products (674 lines)
|
||
├── directory_scanner.py # External Directory Session Scanner (463 lines)
|
||
├── metadata_packer.py # PNG/WebP Metadata Embedding for Exports (561 lines)
|
||
├── manifest_utils.py # Manifest Read/Write/Merge Helpers (120 lines)
|
||
├── network_utils.py # ALL outbound network requests (CivitAI, HuggingFace VAE, Distribution LAN) (280 lines)
|
||
├── upscale_runner.py # Dashboard async upscaling background thread (371 lines)
|
||
├── html_generator.py # Reads /resources/ to build the Dashboard HTML (57 lines)
|
||
│
|
||
├── distribution_manager.py # Distributed Job Queue Coordinator/Master (634 lines)
|
||
├── distribution_worker.py # Worker Thread for Remote Processing (813 lines)
|
||
├── distribution_routes.py # Distribution REST API Endpoints (640 lines)
|
||
│
|
||
├── web/ # [ComfyUI Integration Layer]
|
||
│ ├── dashboard.js # Registers Dashboard Node & Message Handling (432 lines)
|
||
│ ├── smart_json_text.js # Registers JSON Node & Syntax Highlighting (239 lines)
|
||
│ └── conf_builder/ # [Config Builder UI Modules]
|
||
│ ├── conf-builder-main.js # Main entry, widget registration (608 lines)
|
||
│ ├── conf-builder-ui-components.js # UI rendering (dropdowns, sliders, cards) (944 lines)
|
||
│ ├── conf-builder-config-management.js # Save/Load/Duplicate config logic (4975 lines)
|
||
│ ├── conf-builder-utilities.js # State-to-JSON conversion, iteration counting (958 lines)
|
||
│ └── conf-builder-distribution.js # Distribution settings UI (workers, toggles) (474 lines)
|
||
│
|
||
├── resources/ # [The Dashboard SPA Core]
|
||
│ ├── template.html # HTML Skeleton for the generated report (662 lines)
|
||
│ ├── report.css # Styling (Infinite Canvas, Cards, Modals) (1642 lines)
|
||
│ ├── logic_init.js # Bootstrapper (Load JSON, Init State) (104 lines)
|
||
│ ├── logic_state.js # State Store (Redux-style: filters, favorites) (104 lines)
|
||
│ ├── logic_pipeline.js # Data Processing (Filter -> Sort -> Layout) (510 lines)
|
||
│ ├── logic_virtual.js # Virtual DOM / Infinite Scroller Engine (925 lines)
|
||
│ ├── logic_ui.js # UI Renderer (DOM creation, Modal handling) (1780 lines)
|
||
│ ├── logic_upscale_modal.js # Dashboard upscale modal (inline config, presets, async upscale) (380 lines)
|
||
│ ├── logic_events.js # Interaction Handler (Hotkeys, API calls) (91 lines)
|
||
│ └── logic_utils.js # Helpers (Export, Scan, Session loading) (599 lines)
|
||
│
|
||
├── docs/plans/ # Design docs and implementation plans
|
||
├── pyproject.toml # Package metadata (36 lines)
|
||
├── requirements.txt # Python deps — diffusers, piexif
|
||
├── .gitignore
|
||
├── README.md
|
||
├── Roadmap.md
|
||
└── SamplrConfig-Basic-Workflow.json # Example workflow for quick start
|
||
```
|
||
|
||
**Output Directory Structure (Generated at Runtime):**
|
||
```text
|
||
ComfyUI/output/
|
||
├── ultimate-configs/ # Saved config presets (NOT under benchmarks/)
|
||
│ └── {name}.json
|
||
└── benchmarks/
|
||
├── loras_tags.json # LoRA trigger word cache (shared, editable via UI)
|
||
├── model_hashes.json # SHA256 hash cache for CivitAI lookups
|
||
├── USCG-custom-resolutions.json # User-defined custom resolution presets
|
||
├── USCG-upscale-presets.json # Upscale pipeline presets (shared Builder UI + Dashboard)
|
||
├── USCG-config-section-presets.json # Config array presets (full config setups)
|
||
├── model-data/ # CivitAI metadata cache per model
|
||
│ └── {model_name}/metadata.json
|
||
└── {session_name}/
|
||
├── manifest.json # Generation metadata + user annotations
|
||
├── images/
|
||
│ ├── img_{id}.webp # Generated images (named by timestamp ID)
|
||
│ └── upscaled_{idx}_{cfg}_{combo}.webp # Upscaled images
|
||
└── favorites/ # Created by export_favorites endpoint
|
||
├── manifest.json # Cleaned favorites-only manifest
|
||
└── {exported images}
|
||
```
|
||
|
||
---
|
||
|
||
### **Project Architecture Overview**
|
||
|
||
**ComfyUI-Ultimate-Sampler-Grid** is a hybrid application with three layers:
|
||
|
||
1. **The Backend (Python):** Runs inside ComfyUI. Orchestrates grid generation, manages resources (caching/loading), and serves the API.
|
||
2. **The Frontend Bridge (JS in `web/`):** Runs in the ComfyUI browser tab. Registers custom nodes and widgets. Lives at ComfyUI's `/extensions/` path.
|
||
3. **The Dashboard SPA (HTML/JS/CSS in `resources/`):** A standalone, virtualized "IDE" for viewing results. Generated by the backend as inlined HTML, runs entirely in the client's browser via iframe.
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────────────┐
|
||
│ ComfyUI Browser Tab │
|
||
│ ┌──────────────────┐ ┌──────────────────────────────────┐│
|
||
│ │ Config Builder │ │ Dashboard Node ││
|
||
│ │ (conf-builder-*) │ │ (dashboard.js) ││
|
||
│ │ Direct DOM widget │ │ ┌──────────────────────────────┐││
|
||
│ │ │ │ │ iframe (srcdoc) │││
|
||
│ │ │ │ │ Dashboard SPA │││
|
||
│ │ │ │ │ (resources/logic_*.js) │││
|
||
│ │ │ │ │ postMessage() ↕ │││
|
||
│ │ │ │ └──────────────────────────────┘││
|
||
│ └──────────────────┘ └──────────────────────────────────┘│
|
||
│ ↕ widget value ↕ server events │
|
||
├─────────────────────────────────────────────────────────────┤
|
||
│ ComfyUI Server (Python) │
|
||
│ ┌──────────────┐ ┌────────────────┐ ┌────────────────┐ │
|
||
│ │config_builder│ │sampler_node.py │ │__init__.py │ │
|
||
│ │_node.py │→ │ │ │(API endpoints) │ │
|
||
│ └──────────────┘ │ ↓ │ └────────────────┘ │
|
||
│ │generation_ │ │
|
||
│ │orchestrator.py │ │
|
||
│ ┌─────────┐ │ ↓ │ ┌─────────────────┐ │
|
||
│ │ModelCache│←────│model_loader + │ │DistributionMgr │ │
|
||
│ └─────────┘ │image_generation│ │(master jobs) │ │
|
||
│ │ ↓ │ └─────────────────┘ │
|
||
│ │html_generator │ ↕ HTTP │
|
||
│ └────────────────┘ ┌─────────────────┐ │
|
||
│ │WorkerThread │ │
|
||
│ │(remote workers) │ │
|
||
│ └─────────────────┘ │
|
||
└─────────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
---
|
||
|
||
### **Node Class Mappings**
|
||
|
||
Defined in `__init__.py` at line 920:
|
||
|
||
| Internal Name | Python Class | Purpose |
|
||
|---|---|---|
|
||
| `UltimateSamplerGrid` | `SamplerGridTester` | Main generation node — receives configs, runs Cartesian grid |
|
||
| `UltimateGridDashboard` | `SamplerConfigDashboardViewer` | Dashboard viewer — renders iframe with results |
|
||
| `UltimateConfigBuilder` | `UltimateConfigBuilder` | Visual config builder — outputs `configs_json` string |
|
||
| `SmartJSONText` | `SmartJSONTextNode` | JSON text widget with syntax highlighting |
|
||
|
||
`WEB_DIRECTORY = "./web"` — tells ComfyUI to serve `web/` files at `/extensions/`.
|
||
|
||
---
|
||
|
||
### **API Routes Reference**
|
||
|
||
All routes registered via `PromptServer.instance.routes`.
|
||
|
||
**Config Builder APIs** (registered in `__init__.py` and `config_builder_node.py`):
|
||
|
||
| Method | Route | Handler File | Line | Purpose |
|
||
|---|---|---|---|---|
|
||
| GET | `/configbuilder/list_configs` | `__init__.py` | 46 | List saved config preset filenames |
|
||
| POST | `/configbuilder/save_config` | `__init__.py` | 58 | Save config preset `{name, data}` |
|
||
| POST | `/configbuilder/load_config` | `__init__.py` | 108 | Load config preset `{name}` |
|
||
| GET | `/configbuilder/custom_resolutions` | `__init__.py` | 86 | Get custom resolution presets |
|
||
| POST | `/configbuilder/custom_resolutions` | `__init__.py` | 97 | Save custom resolution presets |
|
||
| POST | `/configbuilder/lookup_triggers` | `config_builder_node.py` | 742 | Bulk LoRA trigger word lookup `{loras: [...]}` |
|
||
| POST | `/configbuilder/lookup_lora_metadata` | `config_builder_node.py` | 768 | Full CivitAI metadata for LoRA `{lora_name}` |
|
||
| POST | `/configbuilder/lookup_model_metadata` | `config_builder_node.py` | 916 | Full CivitAI metadata for model `{model_name, model_type}` |
|
||
| GET | `/configbuilder/model_lists` | `config_builder_node.py` | 1029 | All model/sampler/scheduler/VAE/upscale lists |
|
||
| POST | `/configbuilder/refresh_models` | `config_builder_node.py` | 1093 | Signal frontend cache clear |
|
||
| GET | `/configbuilder/get_lora_triggers` | `config_builder_node.py` | 1115 | Get triggers for single LoRA |
|
||
| POST | `/configbuilder/save_lora_triggers` | `config_builder_node.py` | 1138 | Save edited triggers for LoRA |
|
||
|
||
**Dashboard/Tester APIs** (registered in `__init__.py`):
|
||
|
||
| Method | Route | Line | Purpose |
|
||
|---|---|---|---|
|
||
| POST | `/config_tester/delete_session` | 142 | Delete session folder `{session_name}` |
|
||
| GET | `/config_tester/list_sessions` | 174 | List all sessions with metadata (name, count, thumbnail, mtime) |
|
||
| POST | `/config_tester/save_changes` | 234 | Delta save — only changed items `{session_name, changed_items}` |
|
||
| POST | `/config_tester/save_manifest` | 308 | Full merge save `{session_name, manifest}` |
|
||
| POST | `/config_tester/get_session_html` | 390 | Generate dashboard HTML on-demand `{session_name, node_id}` |
|
||
| POST | `/config_tester/export_favorites` | 435 | Export favorited images with options (metadata, workflow, organize) |
|
||
| POST | `/config_tester/delete_non_favorites` | 662 | Delete all non-favorited images from session |
|
||
| POST | `/config_tester/scan_directory` | 746 | Scan external dir, create symlinks, generate manifest |
|
||
|
||
**Distribution APIs** (registered in `distribution_routes.py`):
|
||
|
||
| Method | Route | Purpose |
|
||
|---|---|---|
|
||
| GET | `/uscg-distribution/claim_job` | Worker claims next pending job (200 + job JSON, 204 = no jobs, 503 = inactive) |
|
||
| POST | `/uscg-distribution/submit_result` | Worker uploads completed image (multipart: {metadata, image}) |
|
||
| GET | `/uscg-distribution/status` | Get distribution status, worker stats, job counts |
|
||
| POST | `/uscg-distribution/register_worker` | Worker registration with master |
|
||
| GET | `/uscg-distribution/download_model` | Download model file for model sync `{category, filename}` |
|
||
| POST | `/uscg-distribution/heartbeat` | Worker keepalive signal |
|
||
|
||
---
|
||
|
||
### **1. The Backend: Core Generation Pipeline**
|
||
|
||
* **`sampler_node.py`** (The Interface — 314 lines)
|
||
* **Class:** `SamplerGridTester`
|
||
* **Entry:** `run_tests()` (line 252) → delegates to `generation_orchestrator.run_generation_loop()`
|
||
* **Key Logic:**
|
||
* Extracts `_distribution` and `_session_settings` from wrapped `configs_json` (lines 269-293)
|
||
* Disables caching when optional inputs connected (line 264)
|
||
* `IS_CHANGED()` returns `NaN` when optionals connected (forces re-execution), deterministic hash otherwise (line 103)
|
||
* **Required Inputs:** `ckpt_name`, `positive_text`, `negative_text`, `seed`, `denoise`, `vae_batch_size`, `configs_json`, `resolutions_json`, `session_name`, `overwrite_existing`, `flush_batch_every`, `add_random_seeds_to_gens`, `lora_triggerwords_mode`, `remote_vae_endpoint`, `save_conditioning_cache_to_file`, `enable_model_cache`
|
||
* **Optional Inputs:** `optional_model` (MODEL), `optional_clip` (CLIP), `optional_vae` (VAE), `optional_positive` (CONDITIONING), `optional_negative` (CONDITIONING), `optional_latent` (LATENT)
|
||
|
||
|
||
* **`generation_orchestrator.py`** (The Conductor — 2281 lines, largest file)
|
||
* **Entry:** `run_generation_loop()` — manages entire generation lifecycle
|
||
* **Key Functions:**
|
||
* `setup_session_directories()` (line 43) — creates `benchmarks/{session}/images/` directory structure
|
||
* `run_generation_loop()` — main orchestration
|
||
* `check_if_job_completed()` — smart skip (matches seed/resolution/sampler/scheduler/steps/cfg/denoise against existing manifest)
|
||
* `_preencode_all_conditionings()` — groups configs by (model, LoRA), loads each, encodes all unique prompts, serializes
|
||
* `_conditioning_to_serializable()` / `_serializable_to_conditioning()` — tensor ↔ serialized format for distribution
|
||
* `_run_distributed_generation()` — manages distributed pipeline
|
||
* `_send_distribution_status()` — sends worker status updates to dashboard
|
||
* **Upscaling Block** (lines 1096-1204): Array-based with Cartesian expansion — see dedicated section below
|
||
* **GPU Cooldown Block** (lines 1249-1265): Pauses between generations if enabled in session_settings
|
||
* **Interrupt Handling** (line 1269): Catches `InterruptProcessingException`, flushes pending batch and saves manifest before re-raising
|
||
* **Lookahead:** Triggers `model_cache.preload_lora_model()` / `preload_base_model()` for the next job
|
||
* **ETA Tracking:** Sends `ultimate_grid.progress` events to dashboard via `PromptServer.instance.send_sync()`
|
||
|
||
|
||
* **`image_generation.py`** (The Worker — 748 lines)
|
||
* `generate_image()` (line 16) — wraps KSampler or SamplerCustomAdvanced pipeline
|
||
* `create_image_metadata()` (line 483) — creates metadata dict from config, dimensions, duration, seed, prompts
|
||
* `flush_batch_with_vae()` (line 605) — decodes latents, saves images, updates manifest, syncs disk tags, sends dashboard updates
|
||
* `flush_batch_with_remote_vae()` (line 713) — queues latents for remote VAE worker
|
||
* `decode_latent_with_vae()` — single latent → PIL Image
|
||
* `upscale_image()` (line 265) — handles 3 modes: hires_only, model_only, model_then_hires
|
||
* `calculate_eta()` / `print_generation_progress()` — ETA calculation and logging
|
||
* **Image file URL format:** `f"/view?filename={filename}&type=output&subfolder=benchmarks/{session_name}/images"`
|
||
* **Manifest entry requirements:** Each item needs `id` (timestamp-based), `file` (full /view? URL), `rejected` (boolean)
|
||
|
||
|
||
* **`config_builder_node.py`** (The Builder Backend — 1167 lines)
|
||
* **Class:** `UltimateConfigBuilder`
|
||
* `generate_config()` (line 465) — reads ALL state from single `lora_config` widget (all other INPUT_TYPES widgets are vestigial)
|
||
* **Output format** (line 694): Always `{"configs": [...]}` with optional `"_distribution"` and `"_session_settings"` keys
|
||
* **Session settings embedding** (lines 706-715): Upscaling/cooldown data embedded as `_session_settings`
|
||
* **API endpoints:** Trigger lookups, model metadata, model lists (lines 742-1167)
|
||
* **Model lists endpoint** (line 1029): Returns checkpoints, diffusion_models, unet_gguf, clip_gguf, text_encoders, clip_types, vae, upscale_models, samplers, schedulers
|
||
|
||
|
||
* **`html_generator.py`** (57 lines)
|
||
* `get_html_template(title, manifest_data, node_id)` — reads all `/resources/` files, inlines into `template.html`
|
||
* **JS Load Order:** `logic_state.js` → `logic_utils.js` → `logic_ui.js` → `logic_virtual.js` → `logic_pipeline.js` → `logic_events.js` → `logic_init.js`
|
||
* Replaces placeholders: `__CSS_CONTENT__`, `__JS_CONTENT__`, `__TITLE__`, `__NODE_ID__`
|
||
* Zero external HTTP requests — everything inlined
|
||
|
||
---
|
||
|
||
### **2. The Backend: Resource Management**
|
||
|
||
* **`model_cache.py`** (3-Tier Cache — 900 lines)
|
||
* **Tiers:** 1. LoRA File Cache (raw state_dicts in RAM), 2. Incremental States (partial LoRA stacks), 3. Final Patched Models
|
||
* **Key Methods:** `get_base_model()`, `put_base_model()`, `preload_lora_model()`, `preload_base_model()`, `register_schedule()`, `set_current_step()`, `clear()`, `print_stats()`
|
||
* **Async Preloading:** Background threads via `concurrent.futures`
|
||
|
||
* **`conditioning_cache.py`** (372 lines)
|
||
* Caches CLIP text encodings to disk
|
||
* Hashes `(Prompt + CLIP State + LoRA Config)` to ensure validity
|
||
* Auto-disabled when optional inputs connected
|
||
|
||
* **`batch_encoding.py`** (378 lines)
|
||
* `encode_prompt_with_combinators(clip, text, clip_skip)` — supports AND, CAT, AVG, BREAK combinators
|
||
* `batch_encode_prompts()` — bulk prompt encoding with cache integration
|
||
* **Combinators:** `AND` (multi-cond with optional weights like `:1.5`), `CAT` (tensor concatenation), `AVG(weight)` (weighted average), `BREAK` (77-token segment break)
|
||
|
||
* **`model_loader.py`** (805 lines)
|
||
* `load_checkpoint()`, `load_loras()`, `load_vae_by_name()`, `load_diffusion_model_and_clip()`, `load_loras_for_preencoding()`, `cleanup_model_references()`, `print_incompatible_loras_summary()`
|
||
|
||
* **`trigger_words.py`** (377 lines)
|
||
* `collect_unique_prompts_with_triggers()`, `build_prompt_with_triggers()`, `get_filtered_lora_triggers()`
|
||
* **Modes:** `"None"`, `"Append To End"`, `"Append To Start"`, `"Read From Config"`
|
||
* Per-LoRA placement via `lora_triggerwords_append_settings`. Also applies `model_prompt_prefix` / `model_prompt_suffix`.
|
||
|
||
* **`config_utils.py`** (674 lines)
|
||
* `expand_configs()` — Cartesian product expansion of multi-value fields
|
||
* `parse_prompt_input_nested()` — supports arbitrarily deep nested arrays for Cartesian prompting
|
||
* `prepare_input_jobs()` — final job list preparation
|
||
* `parse_lora_definition()` — parses `"name:model_str:clip_str"` format
|
||
* `sanitize_session_name()`, `get_files_from_folder()`
|
||
|
||
* **`manifest_utils.py`** (120 lines)
|
||
* `load_existing_manifest()`, `save_manifest()`, `merge_manifest_user_changes()`
|
||
* **Critical:** `save_manifest()` calls `merge_manifest_user_changes()` first — reloads from disk to preserve concurrent user favorites/rejected/notes
|
||
|
||
* **`directory_scanner.py`** (463 lines)
|
||
* `scan_directory_for_images()` — scans external directories, parses A1111-style PNG metadata
|
||
* Preserves user tags on re-scan by merging with existing manifest
|
||
|
||
* **`metadata_packer.py`** (561 lines)
|
||
* `pack_metadata_into_image()` (line 181) — accepts `workflow_data` kwarg for embedding workflow
|
||
* `extract_metadata_from_image()` — reads metadata from PNG/WebP
|
||
* `calculate_file_hash()` — SHA256 with disk cache at `benchmarks/model_hashes.json`
|
||
* **Workflow embedding:** Supports both full ComfyUI workflow and per-image nodes workflow
|
||
* Contains fallback `workflowExample` (line 21) — used only when no workflow data provided
|
||
|
||
---
|
||
|
||
### **2b. The Backend: Distribution System**
|
||
|
||
* **`distribution_manager.py`** (634 lines)
|
||
* **Class:** `DistributionManager` — thread-safe distributed job queue on master
|
||
* **Job states:** PENDING → CLAIMED → COMPLETED/FAILED
|
||
* `populate_jobs()`, `claim_job(worker_id)`, `complete_job(job_id)`, `get_stats()`, `set_encoded_conditionings()`, `get_encoded_conditionings_for_job()`
|
||
* Thread-safe with `RLock`, timeout handling (default 600s), `MAX_FAIL_COUNT = 3`
|
||
|
||
* **`distribution_worker.py`** (813 lines)
|
||
* **Class:** `WorkerThread(threading.Thread)` — daemon thread on remote instances
|
||
* Polls `/uscg-distribution/claim_job` every 2s, processes locally, uploads results
|
||
* Caches model/CLIP/VAE between jobs, supports model sync from master
|
||
* Exits after 30 empty polls or 503 response
|
||
|
||
* **`distribution_routes.py`** (640 lines)
|
||
* Registers `/uscg-distribution/*` endpoints on PromptServer
|
||
* Global state: `_distribution_manager` (master), `_worker_thread` (worker)
|
||
|
||
---
|
||
|
||
### **3. The Frontend: ComfyUI Integration (`web/`)**
|
||
|
||
* **`dashboard.js`** (432 lines)
|
||
* Registers `UltimateGridDashboard` node
|
||
* Listens for `ultimate_grid.update`, `ultimate_grid.progress`, `ultimate_grid.distribution_status`
|
||
* Forwards events to matching iframe via `postMessage()`
|
||
* **Auto-loads on node creation** with 500ms delay via `forceLoadSession()`
|
||
* **Widgets:** "RELOAD / SHOW SESSION" button, "DELETE SESSION" button
|
||
* **Singleton guards:** `window.__ultimateGrid*ListenerInstalled` prevents duplicate registration
|
||
|
||
* **`conf-builder-main.js`** (608 lines)
|
||
* Registers `UltimateConfigBuilder` node
|
||
* `onNodeCreated` **MUST be synchronous** — async init in fire-and-forget IIFE
|
||
* Module loading via `import()` with `Date.now()` cache-busting
|
||
* **Default state** (lines 80-147): Defines all config fields including `upscaling` and `cooldown`
|
||
* **Node methods:** `saveState()`, `renderUI()`, `loadSession()`, `saveConfigToBackend()`, `loadConfigFromBackend()`, `triggerAutoSave()` (2s debounce)
|
||
|
||
* **`conf-builder-utilities.js`** (958 lines)
|
||
* `convertStateToConfigs()` (line 497) — JS-side config conversion (MUST stay in sync with Python `generate_config()`)
|
||
* `getIterationCount()` (line 457) — calculates total Cartesian product count
|
||
* `parseLoraString()` / `buildLoraString()` — LoRA string format parsing
|
||
* `refreshAllConfigBuilders()`, `clearAllCaches()`
|
||
* **Global caches:** `availableLoras`, `availableModels`, `availableVAEs`, `availableSamplers`, `availableSchedulers`, `availableSessions`, `availableConfigs`, `availableUpscaleModels`
|
||
* **Session settings output** (lines 689-711): Attaches `_session_settings` with upscaling/cooldown data
|
||
|
||
* **`conf-builder-config-management.js`** (4975 lines — largest JS file)
|
||
* `renderUI()`, `updatePreview()`, `renderConfigSection()`, `renderSessionSection()`
|
||
* `createConfigArrayElement()`, `createModelElement()`, `renderUpscalingSection()` (line ~4437)
|
||
* **Migration logic** for old single-config upscaling format to array format
|
||
* All UI rendering for the config builder widget
|
||
|
||
* **`conf-builder-ui-components.js`** (944 lines)
|
||
* `createSearchableSelect()`, `createSlider()`, `createInputGroup()`, `createTopBar()`, `createSidebar()`, `createSectionHeader()`, `getStyles()`
|
||
|
||
* **`conf-builder-distribution.js`** (474 lines)
|
||
* `renderDistributionSection()`, `renderDistributionSettingsSection()`
|
||
* Worker URL management, connection testing, status indicators
|
||
|
||
* **`smart_json_text.js`** (239 lines)
|
||
* Registers `SmartJSONText` node with syntax highlighting and validation
|
||
|
||
---
|
||
|
||
### **4. The Frontend: Dashboard SPA (`resources/`)**
|
||
|
||
The Dashboard is a standalone single-page app rendered inside an iframe. All files are inlined by `html_generator.py`.
|
||
|
||
* **`template.html`** (662 lines)
|
||
* HTML skeleton with settings panel (cog icon), export options, analytics
|
||
* Export checkboxes (lines 228-261): pack-metadata, export-prompt-txt, copy-manifest, pack-workflow, pack-nodes-workflow
|
||
* DELETE ALL NON FAVORITED ITEMS button (line 257)
|
||
|
||
* **`report.css`** (1642 lines)
|
||
* Infinite canvas styling, card grid, modals, mobile responsive
|
||
* Dark theme with accent colors
|
||
|
||
* **`logic_init.js`** (104 lines)
|
||
* `initDashboard()` — entry point, detects `fullManifest` (injected by Python)
|
||
* Lifecycle: `initLogicState()` → `applyPipeline()` → `initVirtualScroller()` → `initGlobalEvents()`
|
||
|
||
* **`logic_state.js`** (104 lines)
|
||
* Central state store (Redux-style): `items`, `filteredItems`, `selection`, `favorites`
|
||
* Triple JSON bars: Accepted, Favorites, Rejected
|
||
* Saves user preferences (sort order, column count) to `localStorage`
|
||
|
||
* **`logic_pipeline.js`** (510 lines)
|
||
* `updateDataPipeline()`, `executePipeline()`, `processNewData()`, `incrementalFilter()`
|
||
* Filtering and sorting pipeline before virtual scroller
|
||
|
||
* **`logic_virtual.js`** (925 lines)
|
||
* Virtual scroller rendering only ~20 visible cards out of thousands
|
||
* `renderDOM()`, `updateVisibleItems()`, `calculateVisibleRange()`, `autoFitZoom()`, `goToImage()`
|
||
|
||
* **`logic_ui.js`** (1780 lines)
|
||
* DOM manipulation: card HTML, Revise modal, Settings panel, Analytics modals
|
||
* `toggleCogMenu()`, `toggleFiltersPopup()`, `initFilters()`
|
||
* `buildComfyNodesWorkflow(d)` (reusable function) — builds workflow JSON from item data
|
||
* `copyConfigsAsComfyNodes(id)` — calls `buildComfyNodesWorkflow()` internally
|
||
* Analytics modal with "Copy Favorited Prompts as JSON" button
|
||
|
||
* **`logic_events.js`** (91 lines)
|
||
* Keyboard shortcuts: Arrow Keys (Pan), Space (Scroll), +/- (Zoom), F (Fit), Escape (close modals)
|
||
* Message handler for `progress_update` and `update_data` from parent frame
|
||
|
||
* **`logic_utils.js`** (599 lines)
|
||
* `loadSession()`, `saveState()`, `exportFavorites()`, `scanDirectory()`, `triggerGen()`, `toggleFullscreen()`
|
||
* `rejectItem()`, `selectJSON()`, `deleteNonFavorites()`
|
||
* Export favorites reads all checkboxes, captures workflow via `app.graph.serialize()`, builds per-image nodes workflows
|
||
|
||
---
|
||
|
||
### **Key Data Shapes**
|
||
|
||
**Config Array State** (`node.state.config_arrays[n]` in JS, parsed from `lora_config` widget in Python):
|
||
```json
|
||
{
|
||
"name": "Config 1",
|
||
"samplers": ["euler", "dpmpp_2m"],
|
||
"schedulers": ["normal", "karras"],
|
||
"steps": "20, 30",
|
||
"cfg": "7.0",
|
||
"models": [{"path": "model.safetensors", "type": "checkpoint"}],
|
||
"vaes": ["None"],
|
||
"loras": ["lora.safetensors:1.00:1.00"],
|
||
"lora_bypass_states": {"lora_name": true},
|
||
"model_bypass_states": {"model_path": true},
|
||
"vae_bypass_states": {"vae_name": true},
|
||
"te_bypass_states": {"te_name": true},
|
||
"lora_omit_triggers": [],
|
||
"lora_triggerwords_append_settings": {},
|
||
"lora_strength_lock": {},
|
||
"combine": false,
|
||
"positive_prompt_groups": [],
|
||
"negative_prompt": "",
|
||
"use_custom_prompts": false,
|
||
"model_prompt_prefix": "",
|
||
"model_prompt_suffix": "",
|
||
"attention_modes": ["default"],
|
||
"resolutions": [],
|
||
"seed_behavior": "fixed",
|
||
"full_run_seed_behavior": "fixed",
|
||
"full_run_seed": 0,
|
||
"text_encoders": [],
|
||
"clip_type": "stable_diffusion",
|
||
"gguf_options": {},
|
||
"model_sampling_override": "none",
|
||
"model_sampling_shift": "1.73",
|
||
"model_sampling_flux_max_shift": "1.15",
|
||
"model_sampling_flux_base_shift": "0.5",
|
||
"use_advanced_sampling": false,
|
||
"advanced_guider": "cfg_guider",
|
||
"advanced_scheduler": "basic",
|
||
"use_flux_guidance": false,
|
||
"flux_guidance_value": "3.5"
|
||
}
|
||
```
|
||
|
||
**Session Settings** (embedded as `_session_settings` in config output):
|
||
```json
|
||
{
|
||
"upscaling": {
|
||
"enabled": true,
|
||
"configs": [{
|
||
"mode": "hires_only",
|
||
"upscale_models": ["RealESRGAN_x4plus.pth"],
|
||
"upscale_ratios": "1.5, 2.0",
|
||
"hires_denoise": "0.3, 0.5",
|
||
"hires_steps": 10,
|
||
"tiled_vae": false,
|
||
"tile_size": 512
|
||
}]
|
||
},
|
||
"cooldown": {
|
||
"enabled": true,
|
||
"seconds": 5,
|
||
"every_n": 1,
|
||
"clear_vram": false
|
||
}
|
||
}
|
||
```
|
||
|
||
**Distribution Config** (embedded as `_distribution` in config output):
|
||
```json
|
||
{
|
||
"enabled": true,
|
||
"worker_urls": ["http://192.168.1.100:8188", "http://192.168.1.101:8188"],
|
||
"claim_timeout": 600,
|
||
"use_master_encoding": false,
|
||
"sync_models_to_workers": false
|
||
}
|
||
```
|
||
|
||
**Top-level state fields** (outside `config_arrays`):
|
||
```json
|
||
{
|
||
"session_name": "my_session",
|
||
"config_name": "my_config",
|
||
"auto_save": false,
|
||
"include_none": false,
|
||
"label_mode": false,
|
||
"global_positive_groups": [],
|
||
"global_negative": "",
|
||
"distribution_enabled": false,
|
||
"worker_urls": [],
|
||
"claim_timeout": 600,
|
||
"use_master_encoding": false,
|
||
"sync_models_to_workers": false,
|
||
"upscaling": { "enabled": false, "configs": [...] },
|
||
"cooldown": { "enabled": false, "seconds": 5, "every_n": 1, "clear_vram": false }
|
||
}
|
||
```
|
||
|
||
**Manifest Item** (`manifest.json` `items[n]`):
|
||
```json
|
||
{
|
||
"id": 170941234567890,
|
||
"seed": 12345, "cfg": 7.0, "steps": 20,
|
||
"sampler": "euler", "scheduler": "normal",
|
||
"model": "model.safetensors", "lora": "lora:1.0:1.0",
|
||
"vae": "vae.safetensors", "attention_mode": "default",
|
||
"positive": "prompt text", "negative": "negative text",
|
||
"config_positive": "raw prompt without triggers",
|
||
"config_negative": "raw negative without triggers",
|
||
"denoise": 1.0, "width": 1024, "height": 1024,
|
||
"batch_idx": 0, "gen_index": 0, "duration": 45.2,
|
||
"file": "/view?filename=img_170941234567890.webp&type=output&subfolder=benchmarks/session/images",
|
||
"favorited": false, "rejected": false, "note": ""
|
||
}
|
||
```
|
||
|
||
**Upscaled Manifest Item** (additional fields):
|
||
```json
|
||
{
|
||
"upscaled": true,
|
||
"upscale_mode": "hires_only",
|
||
"upscale_ratio": 1.5,
|
||
"upscale_denoise": 0.3,
|
||
"upscale_model": "RealESRGAN_x4plus.pth"
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### **Communication Patterns**
|
||
|
||
**Widget Bridge (Config Builder → Python):**
|
||
```
|
||
JS node.state → JSON.stringify() → lora_config widget.value → Python json.loads(lora_config)
|
||
```
|
||
All other widget params in INPUT_TYPES (samplers, schedulers, steps, cfg) are ignored by Python.
|
||
|
||
**Config Output Wrapping (Config Builder → Sampler):**
|
||
```
|
||
config_builder_node.py generate_config()
|
||
→ {"configs": [...], "_distribution": {...}, "_session_settings": {...}}
|
||
→ json string via configs_json output
|
||
→ sampler_node.py run_tests()
|
||
→ Unwraps: configs_json = json.dumps(parsed["configs"])
|
||
→ Extracts: dist_config = parsed["_distribution"]
|
||
→ Extracts: session_settings = parsed["_session_settings"]
|
||
→ Passes all to run_generation_loop()
|
||
```
|
||
|
||
**Live Dashboard Updates (Server → Dashboard iframe):**
|
||
```
|
||
generation_orchestrator → PromptServer.send_sync("ultimate_grid.update")
|
||
→ dashboard.js api.addEventListener → postMessage({type: 'update_data'})
|
||
→ iframe logic_events.js message handler
|
||
```
|
||
|
||
**Progress Updates (Server → Dashboard iframe):**
|
||
```
|
||
generation_orchestrator → PromptServer.send_sync("ultimate_grid.progress")
|
||
→ dashboard.js → postMessage({type: 'progress_update'})
|
||
→ iframe #eta-bar update
|
||
```
|
||
|
||
**Dashboard → Parent Frame:**
|
||
```
|
||
iframe postMessage({type: 'toggle_fullscreen', node_id})
|
||
→ dashboard.js listener → toggle .dashboard-fullscreen class
|
||
```
|
||
|
||
**Distribution Job Flow (Master ↔ Worker):**
|
||
```
|
||
Master:
|
||
1. generation_orchestrator → DistributionManager.populate_jobs()
|
||
2. [Optional] _preencode_all_conditionings() → manager.set_encoded_conditionings()
|
||
3. POST /uscg-distribution/register_worker on each worker
|
||
4. Workers poll GET /uscg-distribution/claim_job
|
||
5. manager._job_to_dict() attaches encoded_positive/encoded_negative if available
|
||
6. Worker processes job → POST /uscg-distribution/submit_result (multipart: metadata + image)
|
||
7. Master saves image + updates manifest
|
||
```
|
||
|
||
**Distribution Status Updates (Server → Dashboard iframe):**
|
||
```
|
||
generation_orchestrator → PromptServer.send_sync("ultimate_grid.distribution_status")
|
||
→ dashboard.js api.addEventListener → postMessage({type: 'distribution_status'})
|
||
→ iframe displays worker activity
|
||
```
|
||
|
||
---
|
||
|
||
### **Upscaling System (Array-Based with Cartesian Expansion)**
|
||
|
||
The upscaling system allows testing multiple upscale configurations per generated image.
|
||
|
||
**UI State** (`node.state.upscaling` in `conf-builder-main.js` line 129):
|
||
```json
|
||
{
|
||
"enabled": false,
|
||
"configs": [{
|
||
"mode": "hires_only", // "hires_only" | "model_only" | "model_then_hires"
|
||
"upscale_models": [], // Array of upscale model names
|
||
"upscale_ratios": "1.5", // CSV string of ratios
|
||
"hires_denoise": "0.3", // CSV string of denoise values
|
||
"hires_steps": 0, // 0 = use config steps
|
||
"tiled_vae": false,
|
||
"tile_size": 512
|
||
}]
|
||
}
|
||
```
|
||
|
||
**Data Flow:**
|
||
1. JS `convertStateToConfigs()` embeds upscaling in `_session_settings`
|
||
2. Python `config_builder_node.py` line 706-715 embeds into output JSON
|
||
3. `sampler_node.py` line 277 extracts `session_settings`
|
||
4. `generation_orchestrator.py` line 1096-1204 processes after each generation
|
||
|
||
**Cartesian Expansion** (orchestrator lines 1119-1127):
|
||
- `hires_only`: Product of `ratios × denoises`
|
||
- `model_only`: Product of `models × [1.0] × [0.0]`
|
||
- `model_then_hires`: Product of `models × ratios × denoises`
|
||
|
||
**Important Implementation Details:**
|
||
- `effective_size = up_ratio if up_ratio > 1.0 else 2.0` — prevents model_only mode from resizing back to 1x
|
||
- Skip guard: `if show_model and not up_model_name: continue` — avoids crash on empty model name
|
||
- Upscaled images use format `upscaled_{gen_idx}_{ucfg_idx}_{combo_idx}.webp`
|
||
- Base images are skipped when upscaling produces results: `if not upscale_produced:` (line 1236)
|
||
- Upscaled manifest entries include extra fields: `upscaled`, `upscale_mode`, `upscale_ratio`, `upscale_denoise`, `upscale_model`
|
||
|
||
---
|
||
|
||
### **Favorites & Workflow Packing System**
|
||
|
||
**Dashboard Export Options** (template.html lines 228-261):
|
||
1. **Pack CivitAI Metadata** — converts images to PNG with A1111-compatible metadata
|
||
2. **Export Prompt .txt** — saves positive prompt alongside each image
|
||
3. **Copy Cleaned Favorites Manifest** — only favorited items, not full manifest
|
||
4. **Pack Full ComfyUI Workflow** — embeds `app.graph.serialize()` workflow into image metadata
|
||
5. **Pack Config Data as Nodes Workflow** — per-image workflow from `buildComfyNodesWorkflow()`
|
||
6. **DELETE ALL NON FAVORITED ITEMS** — removes non-favorited images and updates manifest
|
||
|
||
**Workflow Priority** (in `__init__.py` `export_favorites` line 586-592):
|
||
- Per-image nodes workflow (from `nodes_workflows` dict keyed by file URL) takes priority
|
||
- Falls back to full ComfyUI graph workflow
|
||
- Both embedded via `pack_metadata_into_image(... workflow_data=item_workflow)`
|
||
|
||
**Key Functions:**
|
||
- `logic_utils.js` `exportFavorites()` — reads checkboxes, captures workflow, builds per-image workflows, POSTs to endpoint
|
||
- `logic_ui.js` `buildComfyNodesWorkflow(d)` — reusable function generating pure-nodes workflow from item data
|
||
- `__init__.py` `export_favorites()` (line 435) — handles all export options
|
||
- `__init__.py` `delete_non_favorites()` (line 662) — deletes non-favorited images and updates manifest
|
||
|
||
---
|
||
|
||
### **File Dependency Graph (Python)**
|
||
|
||
```
|
||
sampler_node.py
|
||
→ generation_orchestrator.py (main entry)
|
||
→ config_utils.py (expand_configs, prepare_input_jobs)
|
||
→ model_loader.py (load_checkpoint, load_loras)
|
||
→ image_generation.py (generate_image, flush_batch, upscale_image)
|
||
→ batch_encoding.py (encode_prompt_with_combinators)
|
||
→ trigger_words.py (build_prompt_with_triggers)
|
||
→ manifest_utils.py (save_manifest, load_existing_manifest)
|
||
→ model_cache.py (ModelCache)
|
||
→ conditioning_cache.py (ConditioningCache)
|
||
→ remote_vae.py (RemoteVAEDecodeWorker)
|
||
→ html_generator.py (get_html_template)
|
||
→ distribution_manager.py (DistributionManager)
|
||
→ distribution_worker.py (WorkerThread)
|
||
|
||
config_builder_node.py
|
||
→ lora_utils.py (trigger word lookup, CivitAI API)
|
||
|
||
__init__.py
|
||
→ all node classes (sampler, dashboard, config_builder, json_text)
|
||
→ distribution_routes.py (distribution API endpoints)
|
||
→ html_generator.py (on-demand HTML generation)
|
||
→ metadata_packer.py (export favorites)
|
||
→ directory_scanner.py (scan external directories)
|
||
→ manifest_utils.py (save/load manifests — note: NOT directly imported, uses inline logic)
|
||
```
|
||
|
||
---
|
||
|
||
### **Server Event Types**
|
||
|
||
| Event Name | Sender | Data Fields | Purpose |
|
||
|---|---|---|---|
|
||
| `ultimate_grid.update` | `generation_orchestrator.py` / `image_generation.py` | `session_name, node, manifest, meta, new_items` | New images generated |
|
||
| `ultimate_grid.progress` | `generation_orchestrator.py` | `session_name, node, progress_pct, eta_str, current_job, total_jobs, avg_duration, last_duration, finish_time` | ETA/progress bar |
|
||
| `ultimate_grid.distribution_status` | `generation_orchestrator.py` | `session_name, workers, stats` | Distribution worker status |
|
||
|
||
---
|
||
|
||
### **Summary of Data Flow**
|
||
|
||
1. **User Config:** Set parameters in the **Config Builder** (visual UI) or **Sampler Node** (JSON text).
|
||
2. **Config Output:** `config_builder_node.py` wraps configs in `{"configs": [...], "_distribution": {...}, "_session_settings": {...}}`.
|
||
3. **Unwrapping:** `sampler_node.py` unwraps `"configs"` key, extracts distribution and session settings.
|
||
4. **Expansion:** `generation_orchestrator.py` calls `expand_configs()` + `prepare_input_jobs()` to expand configs into a full list of jobs (Cartesian product of samplers × schedulers × steps × cfg × models × loras × prompts × resolutions × attention_modes).
|
||
5. **Distribution (optional):** If enabled, `_run_distributed_generation()` populates the job queue, optionally pre-encodes conditionings, and notifies workers.
|
||
6. **Generation:** The backend loops through its share of jobs, using **ModelCache** and **ConditioningCache** for speed.
|
||
7. **Upscaling (optional):** After each image, if upscaling enabled, runs Cartesian product of upscale configs.
|
||
8. **Storage:** Images saved to disk as WebP; metadata appended to `manifest.json`.
|
||
9. **Compilation:** `html_generator.py` reads `manifest.json` and all `/resources/` files, baking into a single `dashboard_html` string.
|
||
10. **Display:** The **Dashboard Node** (JS) renders this HTML in an iframe. Auto-loads on node creation.
|
||
11. **Boot:** `logic_init.js` calls `initDashboard()` → `showSessionLandingIfEmpty()`. If data exists, `logic_virtual.js` renders the grid.
|
||
12. **Interaction:** Click "Favorite" → `logic_events.js` → `save_changes` API → Python updates `manifest.json` on disk.
|
||
13. **GPU Cooldown (optional):** After every N generations, pauses for configured seconds.
|
||
|
||
---
|
||
|
||
### **Critical Development Gotchas**
|
||
|
||
> **GOLDEN RULE: DO NOT REMOVE ANY CODE. DO NOT REMOVE ANY COMMENTS. ONLY CHANGE WHAT IS NECESSARY.**
|
||
|
||
1. **Dual Data Path** — Frontend `convertStateToConfigs()` (in `conf-builder-utilities.js` line 497) and Python `generate_config()` (in `config_builder_node.py` line 465) are **independent implementations** that both transform `node.state` into config JSON. Both must stay in sync when adding new config fields or the UI preview will mismatch the actual output.
|
||
|
||
2. **Widget Bridge** — The `lora_config` widget (STRING type) holds the **entire** node state as serialized JSON. Python `generate_config()` ignores all other widget params and reads only `lora_config`. The `samplers`, `schedulers`, `steps`, `cfg` widgets in `INPUT_TYPES` are vestigial (kept for ComfyUI schema compliance only).
|
||
|
||
3. **onNodeCreated Must Be Synchronous** — In `conf-builder-main.js` line 51, `onNodeCreated` MUST remain synchronous. Async initialization runs inside a fire-and-forget `(async () => { ... })()` IIFE. Making `onNodeCreated` itself async breaks widget registration.
|
||
|
||
4. **Resources Are Inlined** — `html_generator.py` reads all `/resources/` files and bakes them into one HTML string. Zero external HTTP requests. Changes to dashboard JS/CSS require re-generating the HTML (re-run the sampler node or call `/config_tester/get_session_html`).
|
||
|
||
5. **Manifest Merge on Save** — `manifest_utils.py` `save_manifest()` calls `merge_manifest_user_changes()` which reloads from disk before saving. This preserves user favorites/rejected/notes that changed concurrently while generation was running. Also, `flush_batch_with_vae()` (image_generation.py line 657) does its own disk sync.
|
||
|
||
6. **Optional Inputs Disable Caches** — `sampler_node.py` disables conditioning cache when `optional_model`/`optional_clip`/`optional_positive`/`optional_negative` are connected. `IS_CHANGED()` returns `NaN` (forces re-execution). `check_if_job_completed()` skips model/lora/prompt matching.
|
||
|
||
7. **Folder Expansion Syntax** — `"folder/"` = expand to individual entries (one combination each). `"folder/*"` = stack ALL files in folder together as a single combined entry. Weight array syntax `"lora:[0.5, 0.8]:1.0"` creates grid search over strengths. No trailing slash = single file match.
|
||
|
||
8. **LoRA String Format** — `"name:model_str:clip_str"`, stacked with `" + "` separator. `parseLoraString()` in JS, `parse_lora_definition()` in Python. Default strengths: 1.0 for both if colon-separated parts are missing.
|
||
|
||
9. **Cache Busting** — `conf-builder-main.js` uses `Date.now()` timestamp on `import()` calls for module loading. Also intercepts `window.fetch` to detect `/object_info` calls and trigger cache refresh. Static JS served via ComfyUI's `/extensions/` path.
|
||
|
||
10. **Config Presets Path** — Saved to `output/ultimate-configs/` (defined in `__init__.py` line 34), NOT `output/benchmarks/configs/`. Session data goes under `output/benchmarks/{session_name}/`.
|
||
|
||
11. **Field Name Inconsistency: `favorite` vs `favorited`** — `manifest_utils.py` `merge_manifest_user_changes()` uses the key `"favorite"`. But `__init__.py` routes like `save_manifest` and `export_favorites` check `"favorited"`. When touching favorite/unfavorite logic, check which key name the specific code path uses.
|
||
|
||
12. **Prompt Expansion is Recursive** — `parse_prompt_input_nested()` in `config_utils.py` supports arbitrarily deep nested arrays. Flat list = OR (options). List containing sub-lists = AND (Cartesian product). This is used by both the node text inputs and per-config prompt groups from the builder UI.
|
||
|
||
13. **Config Builder Output is ALWAYS Wrapped** — `config_builder_node.py` always outputs `{"configs": [...]}` (with optional `"_distribution"` and `"_session_settings"` keys). The `sampler_node.py` MUST unwrap the `"configs"` key before passing to `expand_configs()`. Both the `_distribution` present AND absent cases must be handled (see lines 269-293 of `sampler_node.py`).
|
||
|
||
14. **Dashboard Auto-Load on Creation** — `web/dashboard.js` calls `forceLoadSession()` with 500ms delay when a dashboard node is created. The `get_session_html` endpoint in `__init__.py` returns a full HTML template with empty manifest for non-existent sessions (NOT a 404). This ensures the landing page shows immediately.
|
||
|
||
15. **`logic_pipeline.js` Legacy Init Guard** — Lines 489-505 have a `DOMContentLoaded` handler that calls `loadSession()` ONLY when `fullManifest.items.length > 0`. Without this guard, `loadSession()` triggers an alert popup for non-existent sessions. The proper initialization path is `logic_init.js` → `init()` → `showSessionLandingIfEmpty()`.
|
||
|
||
16. **Distribution Default Values Must Stay in Sync** — `claim_timeout` default (600) appears in: `distribution_manager.py` (constructor), `config_builder_node.py` (fallback), `conf-builder-distribution.js` (slider default), `conf-builder-main.js` (default state). All four must match. Same for `use_master_encoding` default (false).
|
||
|
||
17. **Conditioning Serialization Uses np.copy()** — `_serializable_to_conditioning()` in `generation_orchestrator.py` must call `np.frombuffer(...).copy()` before `torch.from_numpy()` because `np.frombuffer()` returns read-only arrays incompatible with GPU tensor operations.
|
||
|
||
18. **Multi-Entry Conditioning (AND combinator)** — The AND combinator creates multiple `[tensor, {pooled_output}]` entries in a conditioning list. Serialization in `_conditioning_to_serializable()` must iterate ALL entries, not just `[0]`. Deserialization must reconstruct the full list.
|
||
|
||
19. **State Migration for New Fields** — When adding new fields to the Config Builder default state in `conf-builder-main.js` (around line 80), also add a migration check (around line 363 in the `onConfigure` handler) to backfill the default for existing saved workflows. Without migration, old workflows will have `undefined` for the new field.
|
||
|
||
20. **ComfyUI Registry Security** — No `import requests`, no `subprocess`/`os.system`/`eval()`/`exec()`, no runtime `pip install`, no custom file-serving endpoints, no code obfuscation. Use `urllib.request` for HTTP. Use ComfyUI's `/view` endpoint for serving files. Sanitize all user-supplied paths. See top of this file for full rules.
|
||
|
||
21. **Image File URL Format** — Must be `/view?filename={name}&type=output&subfolder=benchmarks/{session}/images` for ComfyUI's built-in view endpoint. Both `flush_batch_with_vae()` (image_generation.py line 649) and upscaling (orchestrator line 1191) must use this exact format. Missing `type` or `subfolder` params causes 404s.
|
||
|
||
22. **Manifest Entry Required Fields** — Each item needs `id` (timestamp-based integer), `file` (full /view? URL), `rejected` (boolean) for dashboard display. Missing `id` causes holes in the grid. The `id` format is `int(time.time() * 100000) + random.randint(0, 1000)`.
|
||
|
||
23. **Base Image Skip on Upscale** — When upscaling is enabled and produces results, the base (non-upscaled) image must NOT be saved to the manifest. Controlled by `upscale_produced` flag and `if not upscale_produced:` guard (orchestrator line 1236).
|
||
|
||
24. **Session Settings Architecture** — `_session_settings` is a special key embedded in the JSON alongside configs. It's NOT per-config — it's session-level. Contains `upscaling` and `cooldown` settings. Extracted by `sampler_node.py` (line 277) and passed to `run_generation_loop()` as `session_settings` param. The orchestrator receives it as a function parameter, not from configs.
|
||
|
||
25. **Model Discovery Pattern** — `folder_paths.get_filename_list("key")` returns available models. API endpoint `/configbuilder/model_lists` returns all lists. JS side: `conf-builder-utilities.js` `getModelLists()` fetches and stores in global vars. `conf-builder-main.js` passes to renderers as `modelLists` object.
|
||
|
||
26. **Bypass States Are UI-Only** — `vae_bypass_states` and `te_bypass_states` control filtering in `convertStateToConfigs()` but should NOT appear in config output. They're internal UI state persisted only by `node.saveState()`. `lora_bypass_states` and `model_bypass_states` DO appear in output for smart-skip matching.
|
||
|
||
27. **Manifest Save from Dashboard** — Two save mechanisms exist:
|
||
- `save_changes` (line 234 in `__init__.py`): Delta save — only changed items by ID. Fast. Used for individual star/reject.
|
||
- `save_manifest` (line 308): Full merge save — preserves server-side additions. Used for bulk operations.
|
||
|
||
28. **Path Security** — All filesystem endpoints use `_is_path_within()` (line 27 in `__init__.py`) to validate paths stay within `benchmarks/` base directory. Session names sanitized with `re.sub(r'[^\w\-]', '', name)`.
|
||
|
||
---
|
||
|
||
### **Testing Changes**
|
||
|
||
- **Python backend files:** Restart ComfyUI
|
||
- **`web/` JS files:** Refresh browser (Ctrl+F5) — served via `/extensions/` with cache-busting
|
||
- **`resources/` JS/CSS/HTML files:** Dashboard HTML must be regenerated — re-run the sampler node or call `/config_tester/get_session_html`
|
||
- **Config Builder state fields:** Also update migration checks in `conf-builder-main.js` `onConfigure`
|
||
- **New config fields:** Update BOTH `convertStateToConfigs()` in JS AND `generate_config()` in Python
|
||
|
||
---
|
||
|
||
### **Key Functions Quick Reference**
|
||
|
||
**Python (by file):**
|
||
|
||
| File | Key Functions | Line Numbers |
|
||
|---|---|---|
|
||
| `sampler_node.py` | `run_tests()`, `find_existing_match()`, `get_latent_channels()`, `IS_CHANGED()` | 252, 201, 169, 103 |
|
||
| `config_builder_node.py` | `generate_config()`, `process_lora_array()`, `get_available_sessions()`, `expand_lora_folders()` | 465, 424, 360, 370 |
|
||
| `generation_orchestrator.py` | `run_generation_loop()`, `check_if_job_completed()`, `setup_session_directories()`, `_preencode_all_conditionings()`, `_conditioning_to_serializable()`, `_run_distributed_generation()` | -, -, 43, -, -, - |
|
||
| `image_generation.py` | `generate_image()`, `create_image_metadata()`, `flush_batch_with_vae()`, `upscale_image()`, `decode_latent_with_vae()`, `calculate_eta()` | 16, 483, 605, 265, -, 540 |
|
||
| `batch_encoding.py` | `encode_prompt_with_combinators()`, `batch_encode_prompts()` | |
|
||
| `config_utils.py` | `expand_configs()`, `parse_prompt_input_nested()`, `prepare_input_jobs()`, `parse_lora_definition()`, `sanitize_session_name()` | |
|
||
| `model_loader.py` | `load_checkpoint()`, `load_loras()`, `load_vae_by_name()`, `load_diffusion_model_and_clip()` | |
|
||
| `model_cache.py` | `ModelCache.get_base_model()`, `.put_base_model()`, `.preload_lora_model()`, `.register_schedule()` | |
|
||
| `trigger_words.py` | `collect_unique_prompts_with_triggers()`, `build_prompt_with_triggers()`, `get_filtered_lora_triggers()` | |
|
||
| `manifest_utils.py` | `load_existing_manifest()`, `save_manifest()`, `merge_manifest_user_changes()` | |
|
||
| `metadata_packer.py` | `pack_metadata_into_image()`, `extract_metadata_from_image()`, `calculate_file_hash()` | 181, -, - |
|
||
| `directory_scanner.py` | `scan_directory_for_images()`, `parse_a1111_parameters()` | |
|
||
| `distribution_manager.py` | `DistributionManager.populate_jobs()`, `.claim_job()`, `.complete_job()`, `.set_encoded_conditionings()` | |
|
||
| `distribution_worker.py` | `WorkerThread.run()`, `._process_job()`, `._download_model_from_master()` | |
|
||
|
||
**JavaScript (by file):**
|
||
|
||
| File | Key Functions |
|
||
|---|---|
|
||
| `conf-builder-main.js` | `ensureModulesLoaded()`, `node.saveState()`, `node.renderUI()`, `node.loadSession()`, `node.triggerAutoSave()` |
|
||
| `conf-builder-utilities.js` | `convertStateToConfigs()`, `getIterationCount()`, `parseLoraString()`, `buildLoraString()`, `refreshAllConfigBuilders()`, `getModelLists()` |
|
||
| `conf-builder-config-management.js` | `renderUI()`, `updatePreview()`, `createConfigArrayElement()`, `createModelElement()`, `renderUpscalingSection()` |
|
||
| `conf-builder-ui-components.js` | `createSearchableSelect()`, `createSlider()`, `getStyles()` |
|
||
| `conf-builder-distribution.js` | `renderDistributionSection()`, `renderDistributionSettingsSection()` |
|
||
| `dashboard.js` | `forceLoadSession()`, event listeners for update/progress/distribution_status |
|
||
| `logic_ui.js` | `toggleCogMenu()`, `toggleFiltersPopup()`, `initFilters()`, `buildComfyNodesWorkflow()`, `copyConfigsAsComfyNodes()` |
|
||
| `logic_utils.js` | `loadSession()`, `exportFavorites()`, `scanDirectory()`, `triggerGen()`, `toggleFullscreen()`, `deleteNonFavorites()` |
|
||
| `logic_virtual.js` | `renderDOM()`, `updateVisibleItems()`, `calculateVisibleRange()`, `autoFitZoom()`, `goToImage()` |
|
||
| `logic_pipeline.js` | `updateDataPipeline()`, `executePipeline()`, `processNewData()`, `incrementalFilter()` |
|
||
| `logic_events.js` | Message handler for `progress_update`, `update_data`, `distribution_status` |
|