Files
rookiestar28-ComfyUI-Doctor/analyzer.py
T

302 lines
11 KiB
Python

"""
Error Analyzer for ComfyUI Runtime Diagnostics.
Analyzes Python tracebacks and provides human-readable suggestions for common errors.
"""
import re
import functools
from dataclasses import dataclass
from typing import Optional, Tuple, List, Dict, Any
from .i18n import get_suggestion, ERROR_KEYS
@dataclass
class NodeContext:
"""Context information about the node where an error occurred."""
node_id: Optional[str] = None
node_name: Optional[str] = None
node_class: Optional[str] = None
custom_node_path: Optional[str] = None
def to_dict(self) -> Dict[str, Any]:
"""Convert to dictionary for JSON serialization."""
return {
"node_id": self.node_id,
"node_name": self.node_name,
"node_class": self.node_class,
"custom_node_path": self.custom_node_path,
}
def is_valid(self) -> bool:
"""Check if any context was extracted."""
return any([self.node_id, self.node_name, self.node_class])
# Pattern definitions: (regex_pattern, error_key, has_groups)
# Patterns are checked in order, first match wins
PATTERNS: List[Tuple[str, str, bool]] = [
# SafeTensors Error
(r"safetensors_rust.SafetensorError: Error while deserializing header",
ERROR_KEYS["SAFETENSORS_ERROR"], False),
# CUDNN Error
(r"RuntimeError: cuDNN error: CUDNN_STATUS_EXECUTION_FAILED",
ERROR_KEYS["CUDNN_ERROR"], False),
# Missing InsightFace
(r"ModuleNotFoundError: No module named 'insightface'",
ERROR_KEYS["MISSING_INSIGHTFACE"], False),
# Model/VAE Mismatch (autograd generic but specific text)
(r"RuntimeError: element 0 of tensors does not require grad and does not have a grad_fn",
ERROR_KEYS["MODEL_VAE_MISMATCH"], False),
# MPS OOM
(r"MPS backend out of memory",
ERROR_KEYS["MPS_OOM"], False),
# Invalid Prompt (JSON)
(r"json.decoder.JSONDecodeError",
ERROR_KEYS["INVALID_PROMPT"], False),
# Type mismatch
(r"RuntimeError: expected scalar type (\w+) but found (\w+)",
ERROR_KEYS["TYPE_MISMATCH"], True),
# Dimension mismatch
(r"RuntimeError: The size of tensor ([a-z]) \((\d+)\) must match the size of tensor ([a-z]) \((\d+)\) at non-singleton dimension (\d+)",
ERROR_KEYS["DIMENSION_MISMATCH"], True),
# CUDA OOM (classic)
(r"CUDA out of memory",
ERROR_KEYS["OOM"], False),
# PyTorch OOM (newer format)
(r"torch\.OutOfMemoryError",
ERROR_KEYS["TORCH_OOM"], False),
# Matrix multiplication
(r"mat1 and mat2 shapes cannot be multiplied",
ERROR_KEYS["MATRIX_MULT"], False),
# Device/Type mismatch
(r"Input type \((\w+)\) and weight type \((\w+)\) should be the same",
ERROR_KEYS["DEVICE_TYPE"], True),
# Missing module (improved: supports submodules like 'pkg.submodule')
(r"(?:ModuleNotFoundError|ImportError): No module named ['\"]?([\w.]+)['\"]?",
ERROR_KEYS["MISSING_MODULE"], True),
# Assertion error
(r"AssertionError: (.+)",
ERROR_KEYS["ASSERTION"], True),
# Key error
(r"KeyError: ['\"](.+)['\"]",
ERROR_KEYS["KEY_ERROR"], True),
# Attribute error
(r"AttributeError: '(.+)' object has no attribute '(.+)'",
ERROR_KEYS["ATTRIBUTE_ERROR"], True),
# Shape mismatch (generic)
(r"ValueError: (.+) shape (.+) doesn't match (.+)",
ERROR_KEYS["SHAPE_MISMATCH"], True),
# File not found
(r"FileNotFoundError: \[Errno 2\] No such file or directory: '(.+)'",
ERROR_KEYS["FILE_NOT_FOUND"], True),
# ComfyUI Validation Error (Dynamic inputs mismatch)
# Capture only the first error line, not the entire block
(r"Failed to validate prompt for output \d+:[\s\S]*?\*\s+([^\n]+)\s+\d+:\s*\n\s*-\s+([^\n]+)",
ERROR_KEYS["VALIDATION_ERROR"], True),
# Debug Node: Tensor NaN/Inf
(r"❌ CRITICAL: Tensor contains (NaN|Inf)",
ERROR_KEYS["TENSOR_NAN_INF"], True),
# Debug Node: Meta Tensor
(r"⚠️ Meta Tensor",
ERROR_KEYS["META_TENSOR"], False),
]
# ComfyUI-specific patterns for extracting node context
NODE_CONTEXT_PATTERNS = [
# Pattern: Validation error format "* NodeName NodeID:" (highest priority)
r"\*\s+([A-Za-z0-9_]+)\s+(\d+):",
# Pattern: "Error occurred in node #123: NodeName" (common format)
r"(?:ERROR|error)\s*(?:occurred\s*)?(?:on|in)\s*node\s*[#:]?\s*(\d+)(?:\s*[:\-]\s*(.+?))?(?:\s*\n|\s*$)",
# Pattern: execution.py logs like "got prompt 123" with node info
r"Executing node (\d+),?\s*[tT]itle:\s*(.+?)(?:,|\n|$)",
# Pattern: "!!! Exception during processing node #123 !!!"
r"Exception during processing.*?node\s*[#:]?\s*(\d+)",
# Pattern: ComfyUI prompt error format
r"Prompt executed.*?node\s*[#:]?\s*(\d+)(?:\s*[:\-]\s*(.+?))?(?:\)|$)",
# Pattern: Node class from traceback File path
r"custom_nodes[/\\]([^/\\]+)[/\\].*?\.py",
# Pattern: Class name in traceback (self.<method> in <ClassName>)
r"in\s+(\w+Node)\.",
# Pattern: ComfyUI internal node class detection
r"class\s+'([^']+Node)'",
]
# P3: Pre-compile and cache regex patterns for performance
@functools.lru_cache(maxsize=64)
def _compile_pattern(pattern: str):
"""Compile and cache a regex pattern."""
return re.compile(pattern, re.IGNORECASE)
class ErrorAnalyzer:
"""
Analyzes Python tracebacks and provides human-readable suggestions
for common ComfyUI/PyTorch errors.
"""
@staticmethod
def extract_node_context(traceback_text: str) -> NodeContext:
"""
Extract ComfyUI node context from a traceback.
Attempts to find:
- Node ID (numeric identifier)
- Node Name (display name / title)
- Node Class (Python class name)
- Custom Node Path (which custom_nodes folder)
Args:
traceback_text: The full traceback text to analyze.
Returns:
NodeContext object with extracted information.
"""
context = NodeContext()
if not traceback_text:
return context
# Try to extract node ID and name
for idx, pattern in enumerate(NODE_CONTEXT_PATTERNS[:5]):
match = re.search(pattern, traceback_text, re.IGNORECASE)
if match:
groups = match.groups()
if groups:
# First pattern (* NodeName NodeID:) has reversed order
if idx == 0:
# First group is node name, second is node ID
if len(groups) > 0 and groups[0]:
context.node_name = groups[0].strip()
if len(groups) > 1 and groups[1] and groups[1].isdigit():
context.node_id = groups[1]
else:
# Other patterns: First group is usually node ID
if groups[0] and groups[0].isdigit():
context.node_id = groups[0]
# Second group (if exists) is usually node name
if len(groups) > 1 and groups[1]:
context.node_name = groups[1].strip()
break
# Extract custom node folder name
custom_node_match = re.search(NODE_CONTEXT_PATTERNS[5], traceback_text)
if custom_node_match:
context.custom_node_path = custom_node_match.group(1)
# Extract node class name
for pattern in NODE_CONTEXT_PATTERNS[6:]:
class_match = re.search(pattern, traceback_text)
if class_match:
context.node_class = class_match.group(1)
break
return context
@staticmethod
def analyze(traceback_text: str) -> Optional[str]:
"""
Scans the traceback for known error patterns and returns a suggestion.
Args:
traceback_text: The full traceback text to analyze.
Returns:
A formatted suggestion string, or None if no pattern matched.
"""
if not traceback_text:
return None
for pattern, error_key, has_groups in PATTERNS:
match = _compile_pattern(pattern).search(traceback_text)
if match:
try:
if has_groups and match.groups():
return get_suggestion(error_key, *match.groups())
else:
return get_suggestion(error_key)
except Exception:
return get_suggestion(error_key)
# Generic hints for unmatched errors
if "grad_fn" in traceback_text:
return get_suggestion(ERROR_KEYS["AUTOGRAD"])
return None
@staticmethod
def is_complete_traceback(text: str) -> bool:
"""
Check if the text contains a complete Python traceback.
A complete traceback starts with "Traceback (most recent call last):"
and ends with an Exception/Error line.
Args:
text: The text to check.
Returns:
True if a complete traceback is detected.
"""
# For standard Python tracebacks
if "Traceback (most recent call last):" in text:
# Check for standard Python error/exception ending
# Pattern: ErrorType: message (at the end of a line, not indented)
error_pattern = r'\n([A-Z][a-zA-Z0-9]*(?:Error|Exception|Warning|Interrupt)):.*'
has_python_error = bool(re.search(error_pattern, text))
if has_python_error:
return True
# For ComfyUI Validation Errors - check if we've reached a completion marker
if "Failed to validate prompt for output" in text:
# Validation errors end with "Executing prompt:" or continue with another "Failed to validate"
# Count how many validation errors we have vs how many completion markers
validation_count = text.count("Failed to validate prompt for output")
executing_marker = "Executing prompt:" in text
# If we see "Executing prompt:", the validation error block is complete
if executing_marker:
return True
# If we have validation errors with details, consider complete when we have at least one
# with details (* or -) and no new validation error is starting
has_details = bool(re.search(r'\n[*\-] ', text))
if has_details and validation_count >= 1:
# Check if the last line looks like a conclusion (not a new error starting)
lines = text.strip().split('\n')
if lines:
last_line = lines[-1].strip()
# Complete if last line is "Output will be ignored" or similar
if "Output will be ignored" in last_line or "Prompt executed" in last_line:
return True
return False