Files
ComfyUI/custom_nodes/ComfyUI-Impact-Pack/docs/wildcards/WILDCARD_SYSTEM_DESIGN.md
jaidaken f09734b0ee
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
Add custom nodes, Civitai loras (LFS), and vast.ai setup script
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>
2026-02-09 00:56:42 +00:00

24 KiB

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:

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:

{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:

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__):

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:

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:

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:

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:

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:

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:

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:

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

# 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

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:

{
  "text": "a {red|blue} __flowers__",
  "seed": 42
}

Response:

{
  "text": "a red rose"
}

Implementation:

@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:

{
  "data": ["__dragon__", "__flowers__"],
  "on_demand_mode": true,
  "total_available": 1000
}

Implementation:

@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:

@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:

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:

# colors.yaml
warm:
  - red
  - orange
  - yellow

cold:
  - blue
  - green
  - purple

Usage: __colors/warm__, __colors/cold__

Parsing:

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:

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:

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:

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:

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

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