Files
DanielPFlorian-ComfyUI-Work…/utils/model_manager.py
T

487 lines
18 KiB
Python

"""
Model manager for resolving model paths and detecting devices.
Integrates with ComfyUI's model management system.
"""
import logging
import os
from pathlib import Path
# Try to import ComfyUI folder_paths for model path resolution
try:
import folder_paths
COMFYUI_AVAILABLE = True
except ImportError:
COMFYUI_AVAILABLE = False
logging.warning("ComfyUI folder_paths not available, using basic path resolution")
# Try to import ComfyUI model management for device detection
try:
from comfy import model_management
COMFYUI_MODEL_MGMT = True
except ImportError:
COMFYUI_MODEL_MGMT = False
def get_comfyui_base_path() -> Path | None:
"""
Get the base path of the ComfyUI installation.
Returns:
Path to ComfyUI base directory or None if not found
"""
# Common locations:
# 1. Current working directory (if running from ComfyUI root)
# 2. Parent of custom_nodes directory
# 3. Environment variable
current_path = Path.cwd()
# Case 1: running from inside ComfyUI directory
if (current_path / "main.py").exists() and (current_path / "nodes.py").exists():
return current_path
# Case 2: running from parent directory (portable version root)
# Check for "ComfyUI" directory
if (current_path / "ComfyUI").exists() and (current_path / "ComfyUI" / "main.py").exists():
return current_path / "ComfyUI"
# Check for "comfy" directory (unlikely but possible)
if (current_path / "comfy").exists() and (current_path / "comfy" / "main.py").exists():
return current_path / "comfy"
# Case 3: Check parents (if running from a subdir)
for parent in current_path.parents:
if (parent / "main.py").exists() and (parent / "nodes.py").exists():
return parent
if (parent / "ComfyUI").exists() and (parent / "ComfyUI" / "main.py").exists():
return parent / "ComfyUI"
comfyui_path = os.environ.get("COMFYUI_PATH")
if comfyui_path:
return Path(comfyui_path)
return None
def resolve_model_path(model_path: str, model_type: str = "llm", fallback_to_absolute: bool = True) -> str:
"""
Resolve a model path relative to ComfyUI's model directories.
Args:
model_path: Model path (can be relative or absolute)
model_type: Type of model ("llm", "gguf", etc.)
fallback_to_absolute: If True, return absolute path if not found in ComfyUI dirs
Returns:
Resolved absolute path to the model
"""
if os.path.isabs(model_path) and os.path.exists(model_path):
return model_path
comfyui_base = get_comfyui_base_path()
if comfyui_base:
for base_dir in ["models/LLM", f"models/{model_type}"]:
# Handle subdirectory paths (e.g., "GGUF/llama-joycaption-beta-one-hf-llava.IQ4_XS.gguf")
full_path = comfyui_base / base_dir / model_path
if full_path.exists():
return str(full_path)
# Also try with just the filename (for backward compatibility)
filename = os.path.basename(model_path)
full_path = comfyui_base / base_dir / filename
if full_path.exists():
return str(full_path)
full_path = comfyui_base / model_path
if full_path.exists():
return str(full_path)
if COMFYUI_AVAILABLE:
try:
if model_path.endswith(".gguf"):
model_dirs = folder_paths.get_folder_paths("unet_gguf")
if not model_dirs:
model_dirs = folder_paths.get_folder_paths("clip_gguf")
if not model_dirs:
model_dirs = folder_paths.get_folder_paths("llm")
# If "llm" path wasn't found via get_folder_paths, try accessing the registered paths directly
if not model_dirs and "llm" in folder_paths.folder_names_and_paths:
model_dirs = folder_paths.folder_names_and_paths["llm"][0]
if model_dirs:
for model_dir in model_dirs:
full_path = os.path.join(model_dir, model_path)
if os.path.exists(full_path):
return full_path
filename = os.path.basename(model_path)
full_path = os.path.join(model_dir, filename)
if os.path.exists(full_path):
return full_path
model_dirs = folder_paths.get_folder_paths("llm")
# If "llm" path wasn't found via get_folder_paths, try accessing the registered paths directly
if not model_dirs and "llm" in folder_paths.folder_names_and_paths:
model_dirs = folder_paths.folder_names_and_paths["llm"][0]
if model_dirs:
for model_dir in model_dirs:
full_path = os.path.join(model_dir, model_path)
if os.path.exists(full_path):
return full_path
filename = os.path.basename(model_path)
full_path = os.path.join(model_dir, filename)
if os.path.exists(full_path):
return full_path
except Exception as e:
logging.warning(f"Failed to resolve path using folder_paths: {e}")
# Manual fallback (always try this if folder_paths didn't find it)
comfyui_base = get_comfyui_base_path()
if comfyui_base:
llm_dir = comfyui_base / "models" / "LLM"
if llm_dir.exists():
# Try exact path in LLM dir
full_path = llm_dir / model_path
if full_path.exists():
return str(full_path)
# Try filename in LLM dir
filename = os.path.basename(model_path)
full_path = llm_dir / filename
if full_path.exists():
return str(full_path)
# Try recursive search in LLM dir for GGUF files
if model_path.endswith(".gguf"):
for root, _, files in os.walk(llm_dir):
if filename in files:
return os.path.join(root, filename)
if fallback_to_absolute:
abs_path = os.path.abspath(model_path)
if os.path.exists(abs_path):
return abs_path
# Only return absolute path if it actually exists, otherwise return the original path
# or try to return the most likely expected path (in models/LLM) so the error message is clearer
if comfyui_base:
expected_path = comfyui_base / "models" / "LLM" / model_path
return str(expected_path)
return abs_path
return model_path
def auto_detect_tokenizer_path(model_path: str, model_format: str = "gguf") -> str:
"""
Automatically detect tokenizer path based on model path.
For GGUF models: Tokenizer is typically in a directory with the same name
as the model (without .gguf extension) in the same parent directory.
Example: models/LLM/workflow-generator-q8_0.gguf -> models/LLM/workflow-generator/
For HuggingFace models: Tokenizer is in the same directory as the model.
Example: models/LLM/Qwen2.5-7B-Instruct/ -> models/LLM/Qwen2.5-7B-Instruct/
Args:
model_path: Resolved absolute path to the model file or directory
model_format: "gguf" or "huggingface"
Returns:
Resolved absolute path to the tokenizer directory
"""
import os
if not os.path.isabs(model_path):
model_path = os.path.abspath(model_path)
model_path = os.path.normpath(model_path)
model_path_obj = Path(model_path).resolve()
if model_format.lower() == "gguf":
# For GGUF: tokenizer is in a directory with the same name (without .gguf)
# Example: workflow-generator-q8_0.gguf -> workflow-generator/
model_name = model_path_obj.stem
# Remove quantization suffix if present (e.g., "workflow-generator-q8_0" -> "workflow-generator")
# Common suffixes: -q8_0, -q4_0, -q4_1, -q5_0, -q5_1, -q6_0, -f16, -f32
import re
model_name_clean = re.sub(r"-[qf]\d+[_\d]*$", "", model_name)
parent_dir = model_path_obj.parent
tokenizer_dir = parent_dir / model_name_clean
if tokenizer_dir.exists() and tokenizer_dir.is_dir():
tokenizer_files = ["tokenizer.json", "tokenizer_config.json", "vocab.json"]
if any((tokenizer_dir / f).exists() for f in tokenizer_files):
return str(tokenizer_dir)
# Fallback: try with full model name (including quantization suffix)
tokenizer_dir = parent_dir / model_name
if tokenizer_dir.exists() and tokenizer_dir.is_dir():
tokenizer_files = ["tokenizer.json", "tokenizer_config.json", "vocab.json"]
if any((tokenizer_dir / f).exists() for f in tokenizer_files):
return str(tokenizer_dir)
# If not found, return the expected path anyway (will be handled by error handling)
return str(tokenizer_dir)
else:
if model_path_obj.is_dir():
return str(model_path_obj)
else:
# Model path is a file, tokenizer should be in the same directory
# This shouldn't happen for HuggingFace, but handle it anyway
return str(model_path_obj.parent)
def get_device_info() -> tuple[str, bool]:
"""
Get device information using ComfyUI's model management.
Returns:
Tuple of (device_string, cuda_available)
"""
if COMFYUI_MODEL_MGMT:
try:
dev = model_management.get_torch_device()
if hasattr(dev, "type"):
if dev.type == "cuda":
return "cuda", True
elif dev.type == "cpu":
return "cpu", False
elif dev.type == "mps":
return "mps", False
except Exception as e:
logging.warning(f"Failed to get device from ComfyUI: {e}")
try:
import torch
if torch.cuda.is_available():
return "cuda", True
except ImportError:
pass
return "cpu", False
def register_llm_folder_type():
"""
Register "llm" folder type in ComfyUI's folder_paths.
This allows us to use folder_paths.get_filename_list("llm") and folder_paths.get_full_path("llm", ...).
"""
if not COMFYUI_AVAILABLE:
logging.warning("ComfyUI folder_paths not available, cannot register LLM folder type")
return
try:
if "llm" in folder_paths.folder_names_and_paths:
return
comfyui_base = get_comfyui_base_path()
if comfyui_base:
llm_dir = comfyui_base / "models" / "LLM"
if llm_dir.exists():
# folder_names_and_paths structure: {key: (list_of_paths, set_of_extensions)}
# For LLM, we want to accept both .gguf files and directories (no extension filter)
folder_paths.folder_names_and_paths["llm"] = ([str(llm_dir)], set())
logging.info(f"Registered LLM folder type: {llm_dir}")
else:
logging.warning(f"LLM directory not found: {llm_dir}")
else:
try:
# Look for existing model directories to infer the models path
if "checkpoints" in folder_paths.folder_names_and_paths:
checkpoints_paths, _ = folder_paths.folder_names_and_paths["checkpoints"]
if checkpoints_paths:
# models/LLM should be sibling to models/checkpoints
models_dir = Path(checkpoints_paths[0]).parent
llm_dir = models_dir / "LLM"
if llm_dir.exists():
folder_paths.folder_names_and_paths["llm"] = ([str(llm_dir)], set())
logging.info(f"Registered LLM folder type: {llm_dir}")
return
except Exception as e:
logging.warning(f"Failed to infer LLM directory from existing folders: {e}")
logging.warning("Could not determine ComfyUI base path, LLM folder type not registered")
except Exception as e:
logging.error(f"Failed to register LLM folder type: {e}")
def get_llm_model_list() -> list[str]:
"""
Get all models from ComfyUI/models/LLM/ (both .gguf files and directories).
Recursively scans subdirectories for .gguf files.
Returns only model names (not full paths).
Returns:
List of model names (e.g., ["workflow-generator-q8_0.gguf", "Qwen2.5-7B-Instruct"])
"""
model_names = []
def scan_directory(directory: Path, prefix: str = ""):
"""
Recursively scan directory for .gguf files and HuggingFace model directories.
Args:
directory: Directory to scan
prefix: Prefix to add to model names (for subdirectories)
"""
if not directory.exists():
return
try:
for item in directory.iterdir():
if item.is_file() and item.suffix == ".gguf":
if prefix:
model_names.append(f"{prefix}{item.name}")
else:
model_names.append(item.name)
elif item.is_dir():
has_gguf = False
try:
has_gguf = any(f.suffix == ".gguf" for f in item.iterdir() if f.is_file())
except (PermissionError, OSError) as e:
logging.debug(f"Cannot read directory {item}: {e}")
continue
if has_gguf:
# Directory contains .gguf files - scan recursively and add files with prefix
sub_prefix = f"{item.name}/" if prefix else f"{item.name}/"
scan_directory(item, sub_prefix)
else:
# Directory is likely a HuggingFace model - add directory name
if prefix:
model_names.append(f"{prefix}{item.name}")
else:
model_names.append(item.name)
except (PermissionError, OSError) as e:
logging.warning(f"Cannot scan directory {directory}: {e}")
# Always try to get models, even if folder_paths isn't registered
comfyui_base = get_comfyui_base_path()
if not COMFYUI_AVAILABLE:
if comfyui_base:
llm_dir = comfyui_base / "models" / "LLM"
scan_directory(llm_dir)
return sorted(model_names)
try:
# First try using folder_paths if registered
if "llm" in folder_paths.folder_names_and_paths:
llm_paths, _ = folder_paths.folder_names_and_paths["llm"]
for llm_path in llm_paths:
llm_dir = Path(llm_path)
scan_directory(llm_dir)
# Always also try direct path as fallback (in case folder_paths wasn't registered)
if comfyui_base:
llm_dir = comfyui_base / "models" / "LLM"
scan_directory(llm_dir)
except Exception as e:
logging.warning(f"Failed to get LLM model list: {e}")
# Fallback to direct path scan
if comfyui_base:
llm_dir = comfyui_base / "models" / "LLM"
scan_directory(llm_dir)
# Deduplicate list (folder_paths and manual scan might find same files)
return sorted(list(set(model_names)))
def get_workflow_generator_models() -> list[str]:
"""
Filter models suitable for workflow generator.
Includes both GGUF files and HuggingFace directories.
Returns:
List of model names suitable for workflow generator
"""
all_models = get_llm_model_list()
# For now, return all models (both GGUF and HuggingFace work for workflow generator)
# In the future, we could filter by naming patterns if needed
return all_models
def get_refine_agent_models() -> list[str]:
"""
Filter models suitable for refine_agent (node validator).
Includes both GGUF files and HuggingFace directories.
Returns:
List of model names suitable for refine_agent
"""
all_models = get_llm_model_list()
# For now, return all models (both GGUF and HuggingFace work for refine_agent)
# In the future, we could filter by naming patterns if needed
return all_models
def get_embedding_models() -> list[str]:
"""
Filter embedding models (SentenceTransformer/HuggingFace directories only).
Excludes .gguf files as embedding models are typically HuggingFace/SentenceTransformer.
Returns:
List of embedding model directory names
"""
all_models = get_llm_model_list()
embedding_models = [m for m in all_models if not m.endswith(".gguf")]
return embedding_models
def detect_model_format(model_name: str) -> str:
"""
Auto-detect model format from model name.
Args:
model_name: Model name (e.g., "workflow-generator-q8_0.gguf" or "Qwen2.5-7B-Instruct")
Returns:
"gguf" if model ends with .gguf, "huggingface" if it's a directory
"""
if model_name.endswith(".gguf"):
return "gguf"
else:
return "huggingface"
def get_model_full_path(model_name: str) -> str:
"""
Get full path to a model using ComfyUI's folder_paths.
Args:
model_name: Model name (e.g., "workflow-generator-q8_0.gguf" or "Qwen2.5-7B-Instruct")
Returns:
Full absolute path to the model file or directory
"""
from pathlib import Path
if not COMFYUI_AVAILABLE:
return resolve_model_path(model_name, model_type="llm")
try:
if "llm" in folder_paths.folder_names_and_paths:
# Check for exact match first to avoid fuzzy matching issues
llm_paths, _ = folder_paths.folder_names_and_paths["llm"]
for llm_path in llm_paths:
exact_path = Path(llm_path) / model_name
if exact_path.exists() and (
(not model_name.endswith(".gguf") and exact_path.is_dir()) or (model_name.endswith(".gguf") and exact_path.is_file())
):
return str(exact_path)
return folder_paths.get_full_path("llm", model_name)
else:
return resolve_model_path(model_name, model_type="llm")
except Exception as e:
logging.warning(f"Failed to get full path using folder_paths: {e}, falling back to resolve_model_path")
return resolve_model_path(model_name, model_type="llm")