Add custom nodes, Civitai loras (LFS), and vast.ai setup script
Some checks failed
Python Linting / Run Ruff (push) Has been cancelled
Python Linting / Run Pylint (push) Has been cancelled
Full Comfy CI Workflow Runs / test-stable (12.1, , linux, 3.10, [self-hosted Linux], stable) (push) Has been cancelled
Full Comfy CI Workflow Runs / test-stable (12.1, , linux, 3.11, [self-hosted Linux], stable) (push) Has been cancelled
Full Comfy CI Workflow Runs / test-stable (12.1, , linux, 3.12, [self-hosted Linux], stable) (push) Has been cancelled
Full Comfy CI Workflow Runs / test-unix-nightly (12.1, , linux, 3.11, [self-hosted Linux], nightly) (push) Has been cancelled
Execution Tests / test (macos-latest) (push) Has been cancelled
Execution Tests / test (ubuntu-latest) (push) Has been cancelled
Execution Tests / test (windows-latest) (push) Has been cancelled
Test server launches without errors / test (push) Has been cancelled
Unit Tests / test (macos-latest) (push) Has been cancelled
Unit Tests / test (ubuntu-latest) (push) Has been cancelled
Unit Tests / test (windows-2022) (push) Has been cancelled

Includes 30 custom nodes committed directly, 7 Civitai-exclusive
loras stored via Git LFS, and a setup script that installs all
dependencies and downloads HuggingFace-hosted models on vast.ai.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-09 00:55:26 +00:00
parent 2b70ab9ad0
commit f09734b0ee
2274 changed files with 748556 additions and 3 deletions

View File

@@ -0,0 +1,817 @@
# Wildcard System - Design Document
**Document Type**: Technical Design Document
**Product**: ComfyUI Impact Pack Wildcard System
**Version**: 2.0 (Depth-Agnostic Matching)
**Last Updated**: 2025-11-18
**Status**: Released
---
## 1. System Architecture
### 1.1 High-Level Architecture
```
┌─────────────────────────────────────────────────────────────┐
│ ComfyUI Frontend │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ ImpactWildcardProcessor / ImpactWildcardEncode │ │
│ │ - Wildcard Prompt (editable) │ │
│ │ - Populated Prompt (read-only in Populate mode) │ │
│ │ - Mode: Populate / Fixed │ │
│ │ - UI Indicator: 🟢 Full Cache / 🔵 On-Demand │ │
│ └──────────────────────────────────────────────────────┘ │
└────────────────────────┬─────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ Impact Server (API) │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ POST /impact/wildcards │ │
│ │ GET /impact/wildcards/list │ │
│ │ GET /impact/wildcards/list/loaded │ │
│ │ GET /impact/wildcards/refresh │ │
│ └──────────────────────────────────────────────────────┘ │
└────────────────────────┬─────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ Wildcard Processing Engine │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ process() - Main entry point │ │
│ │ ├─ process_comment_out() │ │
│ │ ├─ replace_options() - {a|b|c} │ │
│ │ └─ replace_wildcard() - __wildcard__ │ │
│ │ │ │
│ │ get_wildcard_value() │ │
│ │ ├─ Direct lookup │ │
│ │ ├─ Depth-agnostic fallback ⭐ NEW │ │
│ │ └─ On-demand file loading │ │
│ │ │ │
│ │ get_wildcard_options() - {option1|__wild__|option3} │ │
│ │ └─ Pattern matching for wildcards in options │ │
│ └──────────────────────────────────────────────────────┘ │
└────────────────────────┬─────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ Loading System │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ Startup Phase │ │
│ │ ├─ calculate_directory_size() - Early termination │ │
│ │ ├─ Determine mode (Full Cache / On-Demand) │ │
│ │ └─ scan_wildcard_metadata() - TXT metadata only │ │
│ │ │ │
│ │ Full Cache Mode │ │
│ │ └─ load_wildcards() - Load all data │ │
│ │ │ │
│ │ On-Demand Mode ⭐ NEW │ │
│ │ ├─ Pre-load: YAML files (keys in content) │ │
│ │ └─ On-demand: TXT files (path = key) │ │
│ └──────────────────────────────────────────────────────┘ │
└────────────────────────┬─────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ Data Storage │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ wildcard_dict = {} │ │
│ │ - Full cache: All wildcard data │ │
│ │ - On-demand: Not used │ │
│ │ │ │
│ │ available_wildcards = {} ⭐ NEW │ │
│ │ - On-demand only: Metadata (path → file) │ │
│ │ - Example: {"dragon": "/path/dragon.txt"} │ │
│ │ │ │
│ │ loaded_wildcards = {} ⭐ NEW │ │
│ │ - On-demand only: Loaded data cache │ │
│ │ - Example: {"dragon": ["red dragon", "blue..."]} │ │
│ └──────────────────────────────────────────────────────┘ │
└────────────────────────┬─────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ File System │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ wildcards/ (bundled) │ │
│ │ custom_wildcards/ (user-defined) │ │
│ │ ├─ *.txt files (one option per line) │ │
│ │ └─ *.yaml files (nested structure) │ │
│ └──────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
```
---
## 2. Core Components
### 2.1 Processing Engine
#### 2.1.1 process()
**Purpose**: Main entry point for wildcard text processing
**Flow**:
```python
def process(text, seed=None):
1. process_comment_out(text) # Remove # comments
2. random.seed(seed) # Deterministic generation
3. replace_options(text) # Process {a|b|c}
4. replace_wildcard(text) # Process __wildcard__
5. return processed_text
```
**Features**:
- Maximum 100 iterations for nested expansion
- Deterministic with seed
- Supports transitive wildcards
---
#### 2.1.2 replace_options()
**Purpose**: Process dynamic prompts `{option1|option2}`
**Supported Syntax**:
```python
{a|b|c} # Random selection
{3::a|2::b|c} # Weighted (3:2:1 ratio)
{2$$, $$a|b|c|d} # Multi-select 2, comma-separated
{2-4$$; $$a|b|c|d} # Multi-select 2-4, semicolon-separated
{a|{b|c}|d} # Nested options
```
**Algorithm**:
1. Parse weight prefix (`::`)
2. Calculate normalized probabilities
3. Use `np.random.choice()` with probabilities
4. Handle multi-select with custom separators
---
#### 2.1.3 replace_wildcard()
**Purpose**: Process wildcard references `__wildcard__`
**Flow**:
```python
def replace_wildcard(string):
for each __match__:
1. keyword = normalize(match)
2. options = get_wildcard_value(keyword)
3. if options:
random select from options
elif '*' in keyword:
pattern matching (for __*/name__)
else:
keep unchanged
4. replace in string
```
**Pattern Matching** (`__*/name__`):
```python
if keyword.startswith('*/'):
base_name = keyword[2:] # "*/dragon" → "dragon"
for k in wildcards:
if matches_pattern(k, base_name):
collect options
combine all options
```
---
### 2.2 Depth-Agnostic Matching ⭐ NEW
#### 2.2.1 get_wildcard_value()
**Purpose**: Retrieve wildcard data with automatic depth-agnostic fallback
**Algorithm**:
```python
def get_wildcard_value(key):
# Phase 1: Direct lookup
if key in loaded_wildcards:
return loaded_wildcards[key]
# Phase 2: File discovery
file_path = find_wildcard_file(key)
if file_path:
load and cache
return data
# Phase 3: Depth-agnostic fallback ⭐ NEW
matched_keys = []
for k in available_wildcards:
if matches_depth_agnostic(k, key):
matched_keys.append(k)
if matched_keys:
# Combine all matched wildcards
all_options = []
for mk in matched_keys:
all_options.extend(get_wildcard_value(mk))
# Cache combined result
loaded_wildcards[key] = all_options
return all_options
return None
```
**Pattern Matching Logic**:
```python
def matches_depth_agnostic(stored_key, search_key):
"""
Examples:
search_key = "dragon"
stored_key = "dragon" → True (exact)
stored_key = "custom_wildcards/dragon" → True (ends with)
stored_key = "dragon/wizard" → True (starts with)
stored_key = "a/b/dragon/c/d" → True (contains)
"""
return (stored_key == search_key or
stored_key.endswith('/' + search_key) or
stored_key.startswith(search_key + '/') or
('/' + search_key + '/') in stored_key)
```
**Benefits**:
- Works with any directory structure
- No configuration needed
- Combines multiple sources for variety
- Cached for performance
---
### 2.3 Loading System
#### 2.3.1 Mode Detection
**Decision Algorithm**:
```python
def determine_loading_mode():
total_size = calculate_directory_size()
cache_limit = config.wildcard_cache_limit_mb * 1024 * 1024
if total_size >= cache_limit:
return ON_DEMAND_MODE
else:
return FULL_CACHE_MODE
```
**Early Termination**:
```python
def calculate_directory_size():
size = 0
for file in walk(directory):
size += file_size
if size >= cache_limit:
return size # Early termination
return size
```
**Performance**: < 1 second for 10GB+ collections
---
#### 2.3.2 Metadata Scanning ⭐ NEW
**Purpose**: Discover TXT wildcards without loading data
**Algorithm**:
```python
def scan_wildcard_metadata(path):
for file in walk(path):
if file.endswith('.txt'):
rel_path = relpath(file, path)
key = normalize(remove_extension(rel_path))
available_wildcards[key] = file # Store path only
```
**Storage**:
```python
available_wildcards = {
"dragon": "/path/custom_wildcards/dragon.txt",
"custom_wildcards/dragon": "/path/custom_wildcards/dragon.txt",
"dragon/wizard": "/path/dragon/wizard.txt",
...
}
```
**Memory**: ~50 bytes per file (path string)
---
#### 2.3.3 On-Demand Loading ⭐ NEW
**Purpose**: Load wildcard data only when accessed
**Flow**:
```
User request: __dragon__
get_wildcard_value("dragon")
Not in cache → find_wildcard_file("dragon")
File not found → Depth-agnostic fallback
Pattern match: ["custom_wildcards/dragon", "dragon/wizard", ...]
Load each matched file
Combine all options
Cache result: loaded_wildcards["dragon"] = combined_options
Return combined_options
```
**YAML Pre-Loading**:
```python
def load_yaml_wildcards():
"""
YAML wildcards CANNOT be on-demand because:
- Keys are inside file content, not file path
- Must parse entire file to discover keys
Example:
File: colors.yaml
Content:
warm: [red, orange, yellow]
cold: [blue, green, purple]
To know "__colors/warm__" exists, must parse entire file.
"""
for yaml_file in find_yaml_files():
data = yaml.load(yaml_file)
for key, value in data.items():
loaded_wildcards[key] = value
```
---
### 2.4 Data Structures
#### 2.4.1 Global State
```python
# Configuration
_on_demand_mode = False # True if on-demand mode active
wildcard_dict = {} # Full cache mode storage
available_wildcards = {} # On-demand metadata (key → file path)
loaded_wildcards = {} # On-demand loaded data (key → options)
# Thread safety
wildcard_lock = threading.Lock()
```
#### 2.4.2 Key Normalization
```python
def wildcard_normalize(x):
"""
Normalize wildcard keys for consistent lookup
Examples:
"Dragon""dragon" (lowercase)
"dragon.txt""dragon" (remove extension)
"folder/Dragon""folder/dragon" (lowercase)
"""
return x.lower().replace('\\', '/')
```
---
## 3. API Design
### 3.1 POST /impact/wildcards
**Purpose**: Process wildcard text
**Request**:
```json
{
"text": "a {red|blue} __flowers__",
"seed": 42
}
```
**Response**:
```json
{
"text": "a red rose"
}
```
**Implementation**:
```python
@app.post("/impact/wildcards")
def process_wildcards(request):
text = request.json["text"]
seed = request.json.get("seed")
result = process(text, seed)
return {"text": result}
```
---
### 3.2 GET /impact/wildcards/list/loaded ⭐ NEW
**Purpose**: Track progressive loading
**Response**:
```json
{
"data": ["__dragon__", "__flowers__"],
"on_demand_mode": true,
"total_available": 1000
}
```
**Implementation**:
```python
@app.get("/impact/wildcards/list/loaded")
def get_loaded_wildcards():
with wildcard_lock:
if _on_demand_mode:
return {
"data": [f"__{k}__" for k in loaded_wildcards.keys()],
"on_demand_mode": True,
"total_available": len(available_wildcards)
}
else:
return {
"data": [f"__{k}__" for k in wildcard_dict.keys()],
"on_demand_mode": False,
"total_available": len(wildcard_dict)
}
```
---
### 3.3 GET /impact/wildcards/refresh
**Purpose**: Reload all wildcards
**Implementation**:
```python
@app.get("/impact/wildcards/refresh")
def refresh_wildcards():
global wildcard_dict, loaded_wildcards, available_wildcards
with wildcard_lock:
# Clear all caches
wildcard_dict.clear()
loaded_wildcards.clear()
available_wildcards.clear()
# Re-initialize
wildcard_load()
return {"status": "ok"}
```
---
## 4. File Format Support
### 4.1 TXT Format
**Structure**:
```
# flowers.txt
rose
tulip
# Comments start with #
sunflower
```
**Parsing**:
```python
def load_txt_wildcard(file_path):
with open(file_path) as f:
lines = f.read().splitlines()
return [x for x in lines if not x.strip().startswith('#')]
```
**On-Demand**: ✅ Fully supported
---
### 4.2 YAML Format
**Structure**:
```yaml
# colors.yaml
warm:
- red
- orange
- yellow
cold:
- blue
- green
- purple
```
**Usage**: `__colors/warm__`, `__colors/cold__`
**Parsing**:
```python
def load_yaml_wildcard(file_path):
data = yaml.load(file_path)
for key, value in data.items():
if isinstance(value, list):
loaded_wildcards[key] = value
elif isinstance(value, dict):
# Recursive for nested structure
load_nested(key, value)
```
**On-Demand**: ⚠️ Always pre-loaded (keys in content)
---
## 5. UI Integration
### 5.1 ImpactWildcardProcessor Node
**Features**:
- **Wildcard Prompt**: User input with wildcard syntax
- **Populated Prompt**: Processed result
- **Mode Selector**: Populate / Fixed
- **Populate**: Process wildcards on queue, populate result
- **Fixed**: Use populated text as-is (for saved images)
**UI Indicator**:
- 🟢 **Full Cache**: All wildcards loaded
- 🔵 **On-Demand**: Progressive loading active (shows count)
---
### 5.2 ImpactWildcardEncode Node
**Additional Features**:
- **LoRA Loading**: `<lora:name:model_weight:clip_weight>`
- **LoRA Block Weight**: `<lora:name:1.0:1.0:LBW=spec;>`
- **BREAK Syntax**: Separate encoding with Concat
- **Clip Integration**: Returns processed model + clip
**Special Syntax**:
```
<lora:chunli:1.0:1.0:LBW=B11:0,0,0,0,0,0,0,0,0,0,A,0,0,0,0,0,0;A=0.;>
```
---
### 5.3 Detailer Wildcard Features
**Ordering**:
- `[ASC]`: Ascending order (x, y)
- `[DSC]`: Descending order (x, y)
- `[ASC-SIZE]`: Ascending by area
- `[DSC-SIZE]`: Descending by area
- `[RND]`: Random order
**Control**:
- `[SEP]`: Separate prompts per detection area
- `[SKIP]`: Skip detailing for this area
- `[STOP]`: Stop detailing (including current area)
- `[LAB]`: Label-based application
- `[CONCAT]`: Concatenate with positive conditioning
**Example**:
```
[ASC]
1girl, blue eyes, smile [SEP]
1boy, brown eyes [SEP]
```
---
## 6. Performance Optimization
### 6.1 Startup Optimization
**Techniques**:
1. **Early Termination**: Stop size calculation at cache limit
2. **Metadata Only**: Don't load TXT file content
3. **YAML Pre-loading**: Small files, pre-load is acceptable
**Results**:
- 10GB collection: 20-60 min → < 1 min (95%+ improvement)
---
### 6.2 Runtime Optimization
**Techniques**:
1. **Caching**: Store loaded wildcards in memory
2. **Depth-Agnostic Caching**: Cache combined pattern results
3. **NumPy Random**: Fast random generation
**Results**:
- First access: < 50ms
- Cached access: < 1ms
---
### 6.3 Memory Optimization
**Techniques**:
1. **Progressive Loading**: Load only accessed wildcards
2. **Metadata Storage**: Store paths, not data
3. **Combined Caching**: Cache pattern match results
**Results**:
- Initial: < 100MB (vs 1GB+ in old implementation)
- Growth: Linear with usage, not total size
---
## 7. Error Handling
### 7.1 File Not Found
**Scenario**: Wildcard file doesn't exist
**Handling**:
```python
def get_wildcard_value(key):
file_path = find_wildcard_file(key)
if file_path is None:
# Try depth-agnostic fallback
matched = find_pattern_matches(key)
if matched:
return combine_matched(matched)
# No match found - log warning, return None
logging.warning(f"Wildcard not found: {key}")
return None
```
**User Impact**: Wildcard remains unexpanded
---
### 7.2 File Read Error
**Scenario**: Cannot read file (permissions, encoding, etc.)
**Handling**:
```python
def load_txt_wildcard(file_path):
try:
with open(file_path, 'r', encoding="ISO-8859-1") as f:
return f.read().splitlines()
except Exception as e:
logging.error(f"Failed to load {file_path}: {e}")
return None
```
**User Impact**: Wildcard not loaded, error logged
---
### 7.3 Infinite Loop Protection
**Scenario**: Circular wildcard references
**Protection**:
```python
def process(text, seed=None):
max_iterations = 100
for i in range(max_iterations):
new_text = process_one_pass(text)
if new_text == text:
break # No changes, done
text = new_text
if i == max_iterations - 1:
logging.warning("Max iterations reached")
return text
```
**User Impact**: Processing stops after 100 iterations
---
## 8. Testing Strategy
### 8.1 Unit Tests
**Coverage**:
- `process()`: All syntax variations
- `replace_options()`: Weight, multi-select, nested
- `replace_wildcard()`: Direct, pattern, depth-agnostic
- `get_wildcard_value()`: Direct, fallback, caching
---
### 8.2 Integration Tests
**Scenarios**:
- Full cache mode activation
- On-demand mode activation
- Progressive loading tracking
- Depth-agnostic matching
- API endpoints
**Test Suite**: `tests/test_dragon_wildcard_expansion.sh`
---
### 8.3 Performance Tests
**Metrics**:
- Startup time (10GB collection)
- Memory usage (initial, after 100 accesses)
- First access latency
- Cached access latency
- Pattern matching latency
**Test Tool**: `/tmp/test_depth_agnostic.sh`
---
## 9. Security Considerations
### 9.1 Path Traversal
**Risk**: Malicious wildcard names could access files outside wildcard directory
**Mitigation**:
```python
def find_wildcard_file(key):
# Normalize and validate path
safe_key = os.path.normpath(key)
if '..' in safe_key or safe_key.startswith('/'):
logging.error(f"Invalid wildcard path: {key}")
return None
# Ensure result is within wildcard directory
file_path = os.path.join(wildcards_path, safe_key)
if not file_path.startswith(wildcards_path):
logging.error(f"Path traversal attempt: {key}")
return None
return file_path
```
---
### 9.2 Resource Exhaustion
**Risk**: Very large wildcards or infinite loops
**Mitigation**:
1. **Iteration Limit**: Max 100 expansions
2. **File Size Limit**: Reasonable file size checks
3. **Memory Monitoring**: Track loaded wildcard count
---
## 10. Future Enhancements
### 10.1 Planned Features
1. **LRU Cache**: Automatic eviction of least-used wildcards
2. **Background Preloading**: Preload frequently-used wildcards
3. **Persistent Cache**: Save loaded wildcards across restarts
4. **Usage Statistics**: Track wildcard access patterns
5. **Compression**: Compress infrequently-used wildcards
### 10.2 Performance Improvements
1. **Parallel Loading**: Load multiple wildcards concurrently
2. **Index Structure**: B-tree for faster lookups
3. **Memory Pooling**: Reduce allocation overhead
---
## 11. References
### 11.1 External Documentation
- [Product Requirements Document](WILDCARD_SYSTEM_PRD.md)
- [User Guide](WILDCARD_SYSTEM_OVERVIEW.md)
- [Testing Guide](WILDCARD_TESTING_GUIDE.md)
- [Tutorial](../../ComfyUI-extension-tutorials/ComfyUI-Impact-Pack/tutorial/ImpactWildcard.md)
### 11.2 Code References
- **Core Engine**: `modules/impact/wildcards.py`
- **API Server**: `modules/impact/impact_server.py`
- **UI Nodes**: `nodes.py` (ImpactWildcardProcessor, ImpactWildcardEncode)
---
**Document Approval**:
- Engineering Lead: ✅ Approved
- Architecture Review: ✅ Approved
- Security Review: ✅ Approved
**Last Review**: 2025-11-18