Files
slahiriandClaude Opus 4.5 5d914f7e01 feat: add SID_LLM_Local_Text node for text-only local models
New node for text-only Qwen models (no vision):
- Qwen2.5 series: 1.5B, 3B, 7B, 14B
- Qwen3 series: 1.7B, 4B, 8B
- Abliterated (uncensored): 7B, 14B

Features:
- VRAM-efficient text-only inference
- 4-bit, 8-bit, FP16 quantization
- Model caching for fast repeated calls
- OpenAI-compatible interface

Updated SID_PromptModifier to support local_text provider.
Ideal for prompt modification workflows without vision.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-31 08:52:18 +05:30

595 lines
21 KiB
Python

"""
ComfyUI-AI-Photography-Toolkit
LLM Provider nodes for ComfyUI - Cloud and Local model support.
All nodes are prefixed with SID_ for easy identification.
Author: Siddhartha Lahiri
Version: 4.2.1
"""
__version__ = "4.2.1"
import sys
import subprocess
import importlib.util
# Track installation status for welcome message
_dependency_status = {}
_v3_api_available = False
def check_and_install_dependencies():
"""
Check and install required dependencies. Track status for logging.
"""
global _dependency_status
# Required dependencies (auto-installed)
dependencies = {
# LLM API dependencies
"anthropic": {
"import_name": "anthropic",
"pip_spec": "anthropic>=0.39.0",
"description": "Anthropic Claude API SDK",
},
"openai": {
"import_name": "openai",
"pip_spec": "openai>=1.0.0",
"description": "OpenAI API SDK (also used for Grok, LM Studio, etc.)",
},
"pyyaml": {
"import_name": "yaml",
"pip_spec": "pyyaml>=6.0",
"description": "YAML configuration parser",
},
"requests": {
"import_name": "requests",
"pip_spec": "requests>=2.28.0",
"description": "HTTP library (for Ollama API)",
},
"typing_extensions": {
"import_name": "typing_extensions",
"pip_spec": "typing_extensions>=4.0.0",
"description": "Extended typing support",
},
# Tagger dependencies - Core
"onnxruntime": {
"import_name": "onnxruntime",
"pip_spec": "onnxruntime>=1.16.0",
"description": "ONNX Runtime for WD14/PixAI taggers",
},
"huggingface_hub": {
"import_name": "huggingface_hub",
"pip_spec": "huggingface_hub>=0.20.0",
"description": "HuggingFace Hub for model downloads",
},
"opencv-python": {
"import_name": "cv2",
"pip_spec": "opencv-python>=4.8.0",
"description": "OpenCV for image processing and face detection",
},
# Tagger dependencies - ML Models
"transformers": {
"import_name": "transformers",
"pip_spec": "transformers>=4.36.0",
"description": "HuggingFace Transformers for CLIP, JoyTag, etc.",
},
"scikit-learn": {
"import_name": "sklearn",
"pip_spec": "scikit-learn>=1.3.0",
"description": "Scikit-learn for color clustering",
},
"safetensors": {
"import_name": "safetensors",
"pip_spec": "safetensors>=0.4.0",
"description": "Safe model weight format",
},
# NudeNet for body detection
"nudenet": {
"import_name": "nudenet",
"pip_spec": "nudenet>=3.0.0",
"description": "NudeNet for body part detection",
},
# MediaPipe for pose detection (lightweight alternative)
"mediapipe": {
"import_name": "mediapipe",
"pip_spec": "mediapipe>=0.10.0",
"description": "MediaPipe for pose detection",
},
}
python_exe = sys.executable
install_count = 0
# Check and install required dependencies
for pkg_name, pkg_info in dependencies.items():
import_name = pkg_info["import_name"]
pip_spec = pkg_info["pip_spec"]
description = pkg_info["description"]
if importlib.util.find_spec(import_name) is not None:
_dependency_status[pkg_name] = "installed"
else:
# Try to install
print(f"[SID Photography Toolkit] Installing {pkg_name}: {description}...")
try:
subprocess.check_call(
[python_exe, "-m", "pip", "install", pip_spec],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE
)
_dependency_status[pkg_name] = "just_installed"
install_count += 1
print(f"[SID Photography Toolkit] {pkg_name} installed successfully")
except subprocess.CalledProcessError as e:
_dependency_status[pkg_name] = "failed"
print(f"[SID Photography Toolkit] Failed to install {pkg_name}: {e}")
if install_count > 0:
print(f"[SID Photography Toolkit] Installed {install_count} new dependencies")
# Run dependency check on module load
print("[SID Photography Toolkit] Checking dependencies...")
check_and_install_dependencies()
# Try to import V3 API (may not be available in older ComfyUI versions)
try:
from typing_extensions import override
from comfy_api.latest import ComfyExtension, io
_v3_api_available = True
except ImportError:
_v3_api_available = False
print("[SID Photography Toolkit] V3 API not available, using V1 fallback")
# Import new Prompt Generator (modular implementation) - V1 style
from .core.prompt_generator.generator import SID_PromptGenerator
# Import LLM model type for V1 wrappers
from .llm_providers.llm_model_type import LLMModelConfig
# V3 API imports (conditional)
if _v3_api_available:
# Import LLM provider nodes (unified cloud API + local models)
from .llm_providers.sid_llm_api import SID_LLM_API
from .llm_providers.sid_llm_local import SID_LLM_Local
# Import Z-Image Prompt Generator (unified node)
from .sid_zimage_prompt_generator import SID_ZImagePromptGenerator
class SIDPhotographyToolkitExtension(ComfyExtension):
"""
Extension class for SID Photography Toolkit.
Registers SID_ prefixed LLM nodes with ComfyUI.
"""
@override
async def get_node_list(self) -> list[type[io.ComfyNode]]:
"""
Return list of all nodes in this extension.
"""
return [
SID_LLM_API, # Cloud LLM (Anthropic, OpenAI, Gemini, Grok, Ollama, LM Studio)
SID_LLM_Local, # Local models (Florence-2, Moondream2, SmolVLM, Phi-3.5, QwenVL)
SID_ZImagePromptGenerator, # Z-Image prompt generator (auto Single-Shot/Agentic)
]
# =============================================================================
# V1-Style LLM Node Wrappers (fallback for older ComfyUI versions)
# =============================================================================
class SID_LLM_API_V1:
"""
V1-style wrapper for SID_LLM_API.
Cloud LLM provider supporting Anthropic, OpenAI, Gemini, Grok, Groq, etc.
"""
CATEGORY = "SID Photography Toolkit/LLM Providers"
RETURN_TYPES = ("LLM_MODEL",)
RETURN_NAMES = ("llm_model",)
FUNCTION = "execute"
# Provider configurations
PROVIDERS = {
"Anthropic": {
"api_url": "https://api.anthropic.com",
"models": ["claude-sonnet-4-5-20250929", "claude-haiku-4-5-20251001", "claude-opus-4-5-20251101"],
"default": "claude-sonnet-4-5-20250929",
},
"OpenAI": {
"api_url": "https://api.openai.com/v1",
"models": ["gpt-4.1", "gpt-4.1-mini", "gpt-4o", "gpt-4o-mini"],
"default": "gpt-4o",
},
"Google Gemini": {
"api_url": "https://generativelanguage.googleapis.com/v1beta",
"models": ["gemini-2.5-flash", "gemini-2.5-pro", "gemini-2.0-flash"],
"default": "gemini-2.5-flash",
},
"Groq (Free)": {
"api_url": "https://api.groq.com/openai/v1",
"models": ["meta-llama/llama-4-scout-17b-16e-instruct", "meta-llama/llama-4-maverick-17b-128e-instruct"],
"default": "meta-llama/llama-4-scout-17b-16e-instruct",
},
"Together AI": {
"api_url": "https://api.together.xyz/v1",
"models": ["meta-llama/Llama-Vision-Free", "meta-llama/Llama-3.2-11B-Vision-Instruct-Turbo"],
"default": "meta-llama/Llama-Vision-Free",
},
"OpenRouter": {
"api_url": "https://openrouter.ai/api/v1",
"models": ["google/gemini-2.0-flash-exp:free", "google/gemma-3-27b-it:free"],
"default": "google/gemini-2.0-flash-exp:free",
},
"Ollama (Local)": {
"api_url": "http://localhost:11434/v1",
"models": ["(use custom_model)"],
"default": "(use custom_model)",
},
"LM Studio (Local)": {
"api_url": "http://localhost:1234/v1",
"models": ["(use custom_model)"],
"default": "(use custom_model)",
},
}
@classmethod
def INPUT_TYPES(cls):
providers = list(cls.PROVIDERS.keys())
all_models = []
for p, cfg in cls.PROVIDERS.items():
for m in cfg["models"]:
all_models.append(f"[{p}] {m}")
return {
"required": {
"provider": (providers, {"default": "Anthropic"}),
"model": (all_models, {"default": "[Anthropic] claude-sonnet-4-5-20250929"}),
"api_key": ("STRING", {"default": "", "multiline": False}),
},
"optional": {
"custom_model": ("STRING", {"default": "", "tooltip": "Override model name"}),
"api_url": ("STRING", {"default": "", "tooltip": "Custom API URL"}),
"temperature": ("FLOAT", {"default": 0.3, "min": 0.0, "max": 2.0, "step": 0.1}),
"max_tokens": ("INT", {"default": 2048, "min": 128, "max": 32000}),
},
}
def execute(self, provider, model, api_key, custom_model="", api_url="", temperature=0.3, max_tokens=2048):
# Parse model selection
if model.startswith("["):
end = model.find("]")
model_provider = model[1:end]
model_name = model[end + 2:]
else:
model_provider = provider
model_name = model
# Use custom model if provided
if custom_model.strip():
model_name = custom_model.strip()
# Determine API URL
if api_url.strip():
actual_url = api_url.strip()
else:
actual_url = self.PROVIDERS.get(provider, {}).get("api_url", "")
# Determine provider name for API calls
provider_map = {
"Anthropic": "anthropic",
"OpenAI": "openai",
"Google Gemini": "gemini",
"Groq (Free)": "groq",
"Together AI": "together",
"OpenRouter": "openrouter",
"Ollama (Local)": "ollama",
"LM Studio (Local)": "lmstudio",
}
provider_name = provider_map.get(provider, "openai")
# Check for reasoning support
reasoning_models = ["claude-sonnet-4", "claude-opus-4", "o1", "o3", "deepseek-r1"]
supports_reasoning = any(rm in model_name.lower() for rm in reasoning_models)
config = LLMModelConfig(
provider=provider_name,
model=model_name,
api_key=api_key.strip(),
api_url=actual_url,
max_tokens=max_tokens,
temperature=temperature,
supports_vision=True,
supports_system_prompt=True,
supports_reasoning=supports_reasoning,
)
print(f"[SID_LLM_API] Configured: {provider} - {model_name}")
return (config,)
class SID_LLM_Local_V1:
"""
V1-style wrapper for SID_LLM_Local.
Local vision models with VRAM management.
"""
CATEGORY = "SID Photography Toolkit/LLM Providers"
RETURN_TYPES = ("LLM_MODEL",)
RETURN_NAMES = ("llm_model",)
FUNCTION = "execute"
LOCAL_MODELS = [
"Qwen3-VL-2B-Abliterated",
"Qwen3-VL-2B-Instruct",
"Qwen3-VL-4B-Abliterated",
"Qwen3-VL-4B-Instruct",
"Qwen2.5-VL-3B-Instruct",
"Qwen2.5-VL-7B-Abliterated",
"Qwen2.5-VL-7B-Instruct",
"Qwen3-VL-8B-Abliterated",
"Qwen3-VL-8B-Instruct",
]
MODEL_REPOS = {
"Qwen3-VL-2B-Abliterated": "huihui-ai/Huihui-Qwen3-VL-2B-Instruct-abliterated",
"Qwen3-VL-2B-Instruct": "Qwen/Qwen3-VL-2B-Instruct",
"Qwen3-VL-4B-Abliterated": "huihui-ai/Huihui-Qwen3-VL-4B-Instruct-abliterated",
"Qwen3-VL-4B-Instruct": "Qwen/Qwen3-VL-4B-Instruct",
"Qwen2.5-VL-3B-Instruct": "Qwen/Qwen2.5-VL-3B-Instruct",
"Qwen2.5-VL-7B-Abliterated": "huihui-ai/Qwen2.5-VL-7B-Instruct-abliterated",
"Qwen2.5-VL-7B-Instruct": "Qwen/Qwen2.5-VL-7B-Instruct",
"Qwen3-VL-8B-Abliterated": "huihui-ai/Huihui-Qwen3-VL-8B-Instruct-abliterated",
"Qwen3-VL-8B-Instruct": "Qwen/Qwen3-VL-8B-Instruct",
}
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"model": (cls.LOCAL_MODELS, {"default": "Qwen3-VL-4B-Abliterated"}),
},
"optional": {
"quantization": (["Auto", "4-bit", "8-bit", "FP16"], {"default": "Auto"}),
"device": (["auto", "cuda", "cpu"], {"default": "auto"}),
"temperature": ("FLOAT", {"default": 0.3, "min": 0.0, "max": 1.0, "step": 0.1}),
"max_tokens": ("INT", {"default": 1024, "min": 128, "max": 4096}),
"keep_model_loaded": ("BOOLEAN", {"default": True}),
"hf_token": ("STRING", {"default": "", "tooltip": "HuggingFace token for gated models"}),
},
}
def execute(self, model, quantization="Auto", device="auto", temperature=0.3,
max_tokens=1024, keep_model_loaded=True, hf_token=""):
# Map quantization
quant_map = {"Auto": "4-bit", "4-bit": "4-bit", "8-bit": "8-bit", "FP16": "None (FP16)"}
quant = quant_map.get(quantization, "4-bit")
repo_id = self.MODEL_REPOS.get(model, "")
config = LLMModelConfig(
provider="local",
model=model,
api_key="",
api_url="",
max_tokens=max_tokens,
temperature=temperature,
supports_vision=True,
supports_system_prompt=True,
supports_reasoning=False,
extra_params={
"quantization": quant,
"device": device,
"attention_mode": "auto",
"keep_model_loaded": keep_model_loaded,
"repo_id": repo_id,
"family": "qwenvl",
"repetition_penalty": 1.2,
"top_p": 0.9,
"use_torch_compile": False,
"hf_token": hf_token if hf_token else None,
},
)
print(f"[SID_LLM_Local] Configured: {model} ({quant})")
return (config,)
class SID_LLM_Local_Text_V1:
"""
V1-style wrapper for SID_LLM_Local_Text.
Local text-only models for prompt modification (no vision).
Optimized for text generation tasks like prompt rewriting.
"""
CATEGORY = "SID Photography Toolkit/LLM Providers"
RETURN_TYPES = ("LLM_MODEL",)
RETURN_NAMES = ("llm_model",)
FUNCTION = "execute"
# Text-only Qwen models (smaller, faster for text tasks)
LOCAL_MODELS = [
# Qwen2.5 Text Models
"Qwen2.5-1.5B-Instruct",
"Qwen2.5-3B-Instruct",
"Qwen2.5-7B-Instruct",
"Qwen2.5-14B-Instruct",
# Qwen3 Text Models
"Qwen3-1.7B",
"Qwen3-4B",
"Qwen3-8B",
# Abliterated variants (uncensored)
"Qwen2.5-7B-Abliterated",
"Qwen2.5-14B-Abliterated",
]
MODEL_REPOS = {
# Qwen2.5 Text
"Qwen2.5-1.5B-Instruct": "Qwen/Qwen2.5-1.5B-Instruct",
"Qwen2.5-3B-Instruct": "Qwen/Qwen2.5-3B-Instruct",
"Qwen2.5-7B-Instruct": "Qwen/Qwen2.5-7B-Instruct",
"Qwen2.5-14B-Instruct": "Qwen/Qwen2.5-14B-Instruct",
# Qwen3 Text
"Qwen3-1.7B": "Qwen/Qwen3-1.7B",
"Qwen3-4B": "Qwen/Qwen3-4B",
"Qwen3-8B": "Qwen/Qwen3-8B",
# Abliterated
"Qwen2.5-7B-Abliterated": "huihui-ai/Qwen2.5-7B-Instruct-abliterated",
"Qwen2.5-14B-Abliterated": "huihui-ai/Qwen2.5-14B-Instruct-abliterated",
}
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"model": (cls.LOCAL_MODELS, {"default": "Qwen2.5-3B-Instruct"}),
},
"optional": {
"quantization": (["Auto", "4-bit", "8-bit", "FP16"], {"default": "Auto"}),
"device": (["auto", "cuda", "cpu"], {"default": "auto"}),
"temperature": ("FLOAT", {"default": 0.7, "min": 0.0, "max": 2.0, "step": 0.1}),
"max_tokens": ("INT", {"default": 2048, "min": 128, "max": 8192}),
"keep_model_loaded": ("BOOLEAN", {"default": True}),
"hf_token": ("STRING", {"default": "", "tooltip": "HuggingFace token for gated models"}),
},
}
def execute(self, model, quantization="Auto", device="auto", temperature=0.7,
max_tokens=2048, keep_model_loaded=True, hf_token=""):
# Map quantization
quant_map = {"Auto": "4-bit", "4-bit": "4-bit", "8-bit": "8-bit", "FP16": "None (FP16)"}
quant = quant_map.get(quantization, "4-bit")
repo_id = self.MODEL_REPOS.get(model, "")
config = LLMModelConfig(
provider="local_text",
model=model,
api_key="",
api_url="",
max_tokens=max_tokens,
temperature=temperature,
supports_vision=False, # Text-only model
supports_system_prompt=True,
supports_reasoning=False,
extra_params={
"quantization": quant,
"device": device,
"attention_mode": "auto",
"keep_model_loaded": keep_model_loaded,
"repo_id": repo_id,
"family": "qwen",
"repetition_penalty": 1.1,
"top_p": 0.9,
"use_torch_compile": False,
"hf_token": hf_token if hf_token else None,
},
)
print(f"[SID_LLM_Local_Text] Configured: {model} ({quant})")
return (config,)
def print_welcome_message():
"""
Print welcome message with dependency status and available nodes.
"""
global _dependency_status
# Header
print("")
print("=" * 65)
print(" SID Photography Toolkit for ComfyUI")
print(f" Version: {__version__}")
print(" Author: Siddhartha Lahiri")
print("=" * 65)
# Dependencies status
print("")
print(" Dependencies:")
# Required dependencies
required_pkgs = ["anthropic", "openai", "pyyaml", "requests", "typing_extensions"]
pkg_descriptions = {
"anthropic": "Anthropic Claude API",
"openai": "OpenAI/Grok/LM Studio API",
"pyyaml": "YAML config parser",
"requests": "HTTP library (Ollama)",
"typing_extensions": "Type hints",
}
for pkg in required_pkgs:
status = _dependency_status.get(pkg, "unknown")
desc = pkg_descriptions.get(pkg, pkg)
if status == "installed":
print(f" [OK] {desc}")
elif status == "just_installed":
print(f" [INSTALLED] {desc} (just installed)")
elif status == "failed":
print(f" [FAILED] {desc} - please install manually")
else:
print(f" [?] {desc}")
# Available nodes
print("")
print("-" * 65)
print(" Available Nodes:")
print("")
print(" LLM Providers:")
print(" - SID_LLM_API Cloud APIs (Anthropic, OpenAI, Gemini, Grok, Ollama, LM Studio)")
print(" - SID_LLM_Local Local models (Florence-2, Moondream, SmolVLM, Phi-3.5, QwenVL)")
print("")
print(" Prompt Generators:")
print(" - SID_ZImagePromptGenerator Z-Image prompts (auto Single-Shot/Agentic)")
print(" - SID_PromptGenerator New modular prompt generator (4.2.1)")
print("")
print("=" * 65)
print("")
# Import User Prompt node
from .core.prompt_generator.nodes.user_prompt import SID_UserPrompt
# Import Prompt Modifier node
from .core.prompt_generator.nodes.prompt_modifier import SID_PromptModifier
# V1-style node exports (always available, works with all ComfyUI versions)
NODE_CLASS_MAPPINGS = {
"SID_PromptGenerator": SID_PromptGenerator,
"SID_UserPrompt": SID_UserPrompt,
"SID_PromptModifier": SID_PromptModifier,
"SID_LLM_API": SID_LLM_API_V1,
"SID_LLM_Local": SID_LLM_Local_V1,
"SID_LLM_Local_Text": SID_LLM_Local_Text_V1,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"SID_PromptGenerator": "SID Prompt Generator",
"SID_UserPrompt": "SID User Prompt",
"SID_PromptModifier": "SID Prompt Modifier",
"SID_LLM_API": "SID LLM API",
"SID_LLM_Local": "SID LLM Local",
"SID_LLM_Local_Text": "SID LLM Local Text",
}
# Web directory for JavaScript extensions (node colors, widget control)
WEB_DIRECTORY = "./web"
# V3 entrypoint (only if V3 API available)
if _v3_api_available:
async def comfy_entrypoint() -> SIDPhotographyToolkitExtension:
"""
ComfyUI calls this function to load the extension and its nodes.
This is the entry point for the custom node package.
"""
print_welcome_message()
return SIDPhotographyToolkitExtension()
else:
# Print welcome message for V1-only mode
print_welcome_message()