487 lines
18 KiB
Python
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")
|