Compare commits

...

9 Commits

Author SHA1 Message Date
Juggernaut
80c36f51a9
Merge af1c698117 into e4eb87cc38 2026-01-20 21:58:04 +09:00
Dr.Lt.Data
e4eb87cc38 update DB 2026-01-20 21:57:54 +09:00
bkpaine1
ca8c925fb9
Add Halo-Lipsy: AMD unified memory lip sync node (#2514)
Halo-Lipsy is a native Wav2Lip implementation for ComfyUI that finally works
on AMD APUs with unified memory (Strix Halo, etc.).

Key features:
- No subprocess hacks - runs natively in ComfyUI process
- Face detection on CPU to prevent memory conflicts
- Safe tensor casting for unified memory compatibility
- Sync tuning and edge blending options
- Works on ROCm, AMD APUs, and NVIDIA

Tested on Ryzen AI Max+ 395 (Strix Halo) with 64GB unified memory and ROCm 7.11.

Repository: https://github.com/bkpaine1/Halo-Lipsy

Co-authored-by: bkpaine1 <bkpaine1@users.noreply.github.com>
2026-01-20 21:56:48 +09:00
Dr.Lt.Data
0edd6607ae update DB 2026-01-20 21:55:07 +09:00
Aditya Mundhalia
44aa47126e
Add ComfyUI-ollama-aditya node (#2498) 2026-01-20 21:54:19 +09:00
Dr.Lt.Data
2f40e125be update DB 2026-01-20 21:53:40 +09:00
Dr.Lt.Data
e6a2ae829c update DB 2026-01-20 21:34:04 +09:00
Evgeniy Andriyanoff
9390270879
Add ComfyUI Remote Upscale node (#2500) 2026-01-20 21:32:31 +09:00
ashish
af1c698117 dependency analysis 2026-01-18 16:54:22 +05:30
11 changed files with 3689 additions and 2692 deletions

View File

@ -35882,7 +35882,7 @@
"https://github.com/rookiestar28/ComfyUI-TranslateGemma"
],
"install_type": "git-clone",
"description": "ComfyUI node for TranslateGemma — Google's open source translation models with 55 languages & multimodal image translation."
"description": "A ComfyUI custom node that translates text (including prompts) using Google's open-weight TranslateGemma models."
},
{
"author": "MoonMoon82",
@ -38435,6 +38435,16 @@
"install_type": "git-clone",
"description": "Fail-safe Google Translate prompt node for ComfyUI (retry + caching)."
},
{
"author": "ah-kun",
"title": "ComfyUI-OTP-Auth",
"reference": "https://github.com/ah-kun/ComfyUI-OTP-Auth",
"files": [
"https://github.com/ah-kun/ComfyUI-OTP-Auth"
],
"install_type": "git-clone",
"description": "Adds simple One-Time Password (OTP) authentication using Google Authenticator to protect publicly accessible ComfyUI instances."
},
{
"author": "Kazama-Suichiku",
"title": "ComfyUI-Meshy",
@ -41429,14 +41439,15 @@
"description": "High-performance ComfyUI nodes optimized for RTX 4090: batch processing, memory management, GPU monitoring"
},
{
"author": "aadityamundhalia",
"title": "ComfyUI-ollama-aditya",
"author": "Aditya Mundhalia",
"title": "ComfyUI Ollama by Aditya",
"id": "comfyui-ollama-aditya",
"reference": "https://github.com/aadityamundhalia/ComfyUI-ollama-aditya",
"files": [
"https://github.com/aadityamundhalia/ComfyUI-ollama-aditya"
],
"install_type": "git-clone",
"description": "Custom ComfyUI node for integrating Ollama LLMs into your image generation workflows."
"description": "ComfyUI nodes for integrating Ollama local LLM models with image or text generation workflows."
},
{
"author": "maximilianwicen",
@ -41683,12 +41694,13 @@
{
"author": "bkpaine1",
"title": "Halo-Lipsy",
"id": "halo-lipsy",
"reference": "https://github.com/bkpaine1/Halo-Lipsy",
"files": [
"https://github.com/bkpaine1/Halo-Lipsy"
],
"install_type": "git-clone",
"description": "Native AMD Unified Memory Lip Sync for ComfyUI"
"description": "AMD unified memory lip sync for ComfyUI. Native Wav2Lip inference with no subprocesses - finally works on Strix Halo, ROCm, and all AMD APUs. Features sync tuning, edge blending, and safe tensor casting for unified memory systems."
},
{
"author": "kianthos",
@ -41740,6 +41752,28 @@
"install_type": "git-clone",
"description": "HTTP API for ComfyUI with webhook-based workflow execution."
},
{
"author": "gen1nya",
"title": "ComfyUI Remote Upscale",
"id": "remote-upscale",
"reference": "https://github.com/gen1nya/ComfyUI-Remote-Upscale",
"files": [
"https://github.com/gen1nya/ComfyUI-Remote-Upscale"
],
"install_type": "git-clone",
"description": "Offload image upscaling to a remote server with a dedicated GPU. Real-time progress streaming via SSE."
},
{
"author": "Setmaster",
"title": "ModelPulse",
"id": "modelpulse",
"reference": "https://github.com/Setmaster/ComfyUI-ModelPulse",
"files": [
"https://github.com/Setmaster/ComfyUI-ModelPulse"
],
"install_type": "git-clone",
"description": "Track model usage frequency to identify abandoned or underutilized models. Shows usage stats, file sizes, and highlights stale models in a sidebar panel."
},

View File

@ -693,11 +693,12 @@
],
"https://github.com/5agado/ComfyUI-Sagado-Nodes": [
[
"Film Grain",
"Get Num Frames",
"Get Resolution",
"Image Loader",
"Video Loader"
"SGD_Call_Ollama",
"SGD_Film_Grain",
"SGD_Get_Num_Frames",
"SGD_Get_Resolution",
"SGD_Image_Loader",
"SGD_Video_Loader"
],
{
"title_aux": "Sagado Nodes for ComfyUI"
@ -3319,6 +3320,9 @@
"Vigenere",
"X963KDF_Derive",
"X963KDF_Verify",
"XExchange",
"XPrivateKeyFormat",
"XPublicKeyFormat",
"Zigzag"
],
{
@ -6507,6 +6511,7 @@
"BoyoVideoPairedSaver",
"BoyoVideoSaver",
"BoyoVision",
"BoyoVoiceEnhancer",
"BoyoWhileLoopEnd",
"BoyoWhileLoopStart",
"Boyolatent",
@ -6903,7 +6908,8 @@
],
"https://github.com/Enferlain/ComfyUI-A1111-cond": [
[
"A1111Prompt"
"A1111Prompt",
"A1111PromptNegative"
],
{
"title_aux": "ComfyUI-A1111-cond"
@ -8465,7 +8471,8 @@
],
"https://github.com/GavChap/ComfyUI-SD3LatentSelectRes": [
[
"SD3LatentSelectRes"
"SD3LatentSelectRes",
"SD3LatentSelectResV2"
],
{
"title_aux": "ComfyUI-SD3LatentSelectRes"
@ -18759,6 +18766,7 @@
"Basic data handling: IntPower",
"Basic data handling: IntSubtract",
"Basic data handling: IntToBytes",
"Basic data handling: IsConnected",
"Basic data handling: IsNull",
"Basic data handling: LessThan",
"Basic data handling: LessThanOrEqual",
@ -22785,7 +22793,7 @@
"OllamaPromptGenerator"
],
{
"title_aux": "ComfyUI-ollama-aditya"
"title_aux": "ComfyUI Ollama by Aditya"
}
],
"https://github.com/abdozmantar/ComfyUI-DeepExtract": [
@ -23561,6 +23569,7 @@
"GetImagesFromBatchIndexed",
"GetLatentRangeFromBatch",
"GetLatentSizeAndCount",
"IO_save_image",
"ImageAddMulti",
"ImageAndMaskPreview",
"ImageBatchExtendWithOverlap",
@ -27287,7 +27296,7 @@
],
"https://github.com/capitan01R/ComfyUI-Flux2Klein-Enhancer": [
[
"Flux2KleinEditController",
"Flux2KleinDetailController",
"Flux2KleinEnhancer"
],
{
@ -29732,6 +29741,7 @@
"BatchLatentsNode",
"BatchMasksNode",
"BetaSamplingScheduler",
"BriaImageEditNode",
"ByteDanceFirstLastFrameNode",
"ByteDanceImageEditNode",
"ByteDanceImageNode",
@ -30234,6 +30244,7 @@
"TextEncodeHunyuanVideo_ImageToVideo",
"TextEncodeQwenImageEdit",
"TextEncodeQwenImageEditPlus",
"TextEncodeZImageOmni",
"TextProcessingNode",
"ThresholdMask",
"TomePatchModel",
@ -32815,6 +32826,8 @@
],
"https://github.com/ethanfel/ComfyUI-Sharp-Selector": [
[
"FastAbsoluteSaver",
"ParallelSharpnessLoader",
"SharpFrameSelector",
"SharpnessAnalyzer"
],
@ -34431,6 +34444,15 @@
"title_aux": "ComfyUI_GMIC"
}
],
"https://github.com/gen1nya/ComfyUI-Remote-Upscale": [
[
"RefreshRemoteModels",
"RemoteUpscaleImage"
],
{
"title_aux": "ComfyUI Remote Upscale"
}
],
"https://github.com/geocine/geocine-comfyui": [
[
"Image Scale",
@ -35456,6 +35478,8 @@
"AIIA_CosyVoice_TTS",
"AIIA_CosyVoice_VoiceConversion",
"AIIA_Dialogue_TTS",
"AIIA_DittoLoader",
"AIIA_DittoSampler",
"AIIA_E2E_Speaker_Diarization",
"AIIA_EchoMimicLoader",
"AIIA_EchoMimicSampler",
@ -38096,6 +38120,7 @@
],
"https://github.com/joeriben/ai4artsed_comfyui_nodes": [
[
"ai4artsed_clean_prompt_randomizer",
"ai4artsed_conditioning_fusion",
"ai4artsed_image_analysis",
"ai4artsed_openrouter_key",
@ -39618,6 +39643,7 @@
"Intrinsic_lora_sampling",
"JoinStringMulti",
"JoinStrings",
"LTX2AttentionTunerPatch",
"LTX2AudioLatentNormalizingSampling",
"LTX2SamplingPreviewOverride",
"LTX2_NAG",
@ -39639,6 +39665,7 @@
"MaskBatchMulti",
"MaskOrImageToWeight",
"MergeImageChannels",
"ModelMemoryUseReportPatch",
"ModelPassThrough",
"ModelPatchTorchSettings",
"ModelSaveKJ",
@ -47543,7 +47570,8 @@
],
"https://github.com/princepainter/ComfyUI-PainterLTXV2": [
[
"PainterLTXVtoVideo",
"PainterLTX2V",
"PainterLTX2VPlus",
"PainterSamplerLTXV"
],
{
@ -49912,6 +49940,7 @@
"CloseUpImageNode",
"CloseUpNode",
"ComfyAddSoundtrack",
"ComfyImageAudioCSV",
"ImageSequenceOverlay",
"ImageTransitionNode",
"MergeVideoAudioNode",
@ -51692,6 +51721,7 @@
"https://github.com/sonnybox/ComfyUI-SuperNodes": [
[
"FaceBBoxToMask",
"GetCommonAspectRatio",
"ImageMaskCrop",
"ImageSizeCalculator",
"RestoreMaskCrop",
@ -57204,6 +57234,7 @@
],
"https://github.com/zeeoale/PromptCreatorNode": [
[
"IdentityMixerNode",
"PromptCreatorNode"
],
{

File diff suppressed because it is too large Load Diff

View File

@ -921,7 +921,7 @@ class UnifiedManager:
except:
return version.parse("0.0.0")
def execute_install_script(self, url, repo_path, instant_execution=False, lazy_mode=False, no_deps=False):
def execute_install_script(self, url, repo_path, instant_execution=False, lazy_mode=False, no_deps=False, selected_dependencies=None):
install_script_path = os.path.join(repo_path, "install.py")
requirements_path = os.path.join(repo_path, "requirements.txt")
@ -933,8 +933,19 @@ class UnifiedManager:
if os.path.exists(requirements_path) and not no_deps:
print("Install: pip packages")
pip_fixer = manager_util.PIPFixer(manager_util.get_installed_packages(), comfy_path, manager_files_path)
# Create a set of selected dependency lines for quick lookup
selected_lines = set()
if selected_dependencies:
for dep in selected_dependencies:
selected_lines.add(dep.get('line', '').strip())
lines = manager_util.robust_readlines(requirements_path)
for line in lines:
# Skip if selected_dependencies is provided and this line is not in the selected list
if selected_dependencies is not None and line.strip() not in selected_lines:
continue
package_name = remap_pip_package(line.strip())
if package_name and not package_name.startswith('#') and package_name not in self.processed_install:
self.processed_install.add(package_name)
@ -1342,7 +1353,7 @@ class UnifiedManager:
return result
def repo_install(self, url: str, repo_path: str, instant_execution=False, no_deps=False, return_postinstall=False):
def repo_install(self, url: str, repo_path: str, instant_execution=False, no_deps=False, return_postinstall=False, selected_dependencies=None):
result = ManagedResult('install-git')
result.append(url)
@ -1369,7 +1380,7 @@ class UnifiedManager:
repo.close()
def postinstall():
return self.execute_install_script(url, repo_path, instant_execution=instant_execution, no_deps=no_deps)
return self.execute_install_script(url, repo_path, instant_execution=instant_execution, no_deps=no_deps, selected_dependencies=selected_dependencies)
if return_postinstall:
return result.with_postinstall(postinstall)
@ -1468,7 +1479,7 @@ class UnifiedManager:
else:
return self.cnr_switch_version(node_id, instant_execution=instant_execution, no_deps=no_deps, return_postinstall=return_postinstall).with_ver('cnr')
async def install_by_id(self, node_id: str, version_spec=None, channel=None, mode=None, instant_execution=False, no_deps=False, return_postinstall=False):
async def install_by_id(self, node_id: str, version_spec=None, channel=None, mode=None, instant_execution=False, no_deps=False, return_postinstall=False, selected_dependencies=None):
"""
priority if version_spec == None
1. CNR latest
@ -1519,7 +1530,7 @@ class UnifiedManager:
self.unified_disable(node_id, False)
to_path = os.path.abspath(os.path.join(get_default_custom_nodes_path(), node_id))
res = self.repo_install(repo_url, to_path, instant_execution=instant_execution, no_deps=no_deps, return_postinstall=return_postinstall)
res = self.repo_install(repo_url, to_path, instant_execution=instant_execution, no_deps=no_deps, return_postinstall=return_postinstall, selected_dependencies=selected_dependencies)
if res.result:
if version_spec == 'unknown':
self.unknown_active_nodes[node_id] = repo_url, to_path
@ -1968,7 +1979,7 @@ def __win_check_git_pull(path):
process.wait()
def execute_install_script(url, repo_path, lazy_mode=False, instant_execution=False, no_deps=False):
def execute_install_script(url, repo_path, lazy_mode=False, instant_execution=False, no_deps=False, selected_dependencies=None):
# import ipdb; ipdb.set_trace()
install_script_path = os.path.join(repo_path, "install.py")
requirements_path = os.path.join(repo_path, "requirements.txt")
@ -1980,6 +1991,13 @@ def execute_install_script(url, repo_path, lazy_mode=False, instant_execution=Fa
if os.path.exists(requirements_path) and not no_deps:
print("Install: pip packages")
pip_fixer = manager_util.PIPFixer(manager_util.get_installed_packages(), comfy_path, manager_files_path)
# Create a set of selected dependency lines for quick lookup
selected_lines = set()
if selected_dependencies:
for dep in selected_dependencies:
selected_lines.add(dep.get('line', '').strip())
with open(requirements_path, "r") as requirements_file:
for line in requirements_file:
#handle comments
@ -1990,6 +2008,10 @@ def execute_install_script(url, repo_path, lazy_mode=False, instant_execution=Fa
else:
line = line.split('#')[0].strip()
# Skip if selected_dependencies is provided and this line is not in the selected list
if selected_dependencies is not None and line.strip() not in selected_lines:
continue
package_name = remap_pip_package(line.strip())
if package_name and not package_name.startswith('#'):

View File

@ -443,7 +443,12 @@ async def task_worker():
global tasks_in_progress
async def do_install(item) -> str:
ui_id, node_spec_str, channel, mode, skip_post_install = item
if len(item) == 6:
ui_id, node_spec_str, channel, mode, skip_post_install, selected_dependencies = item
else:
# Backward compatibility
ui_id, node_spec_str, channel, mode, skip_post_install = item
selected_dependencies = []
try:
node_spec = core.unified_manager.resolve_node_spec(node_spec_str)
@ -452,7 +457,7 @@ async def task_worker():
return f"Cannot resolve install target: '{node_spec_str}'"
node_name, version_spec, is_specified = node_spec
res = await core.unified_manager.install_by_id(node_name, version_spec, channel, mode, return_postinstall=skip_post_install)
res = await core.unified_manager.install_by_id(node_name, version_spec, channel, mode, return_postinstall=skip_post_install, selected_dependencies=selected_dependencies)
# discard post install if skip_post_install mode
if res.action not in ['skip', 'enable', 'install-git', 'install-cnr', 'switch-cnr']:
@ -1303,7 +1308,9 @@ async def install_custom_node(request):
logging.error(SECURITY_MESSAGE_GENERAL)
return web.Response(status=404, text="A security error has occurred. Please check the terminal logs")
install_item = json_data.get('ui_id'), node_spec_str, json_data['channel'], json_data['mode'], skip_post_install
# Get selected dependencies if provided
selected_dependencies = json_data.get('selectedDependencies', [])
install_item = json_data.get('ui_id'), node_spec_str, json_data['channel'], json_data['mode'], skip_post_install, selected_dependencies
task_queue.put(("install", install_item))
return web.Response(status=200)
@ -1383,6 +1390,272 @@ async def install_custom_node_pip(request):
return web.Response(status=200)
@routes.post("/customnode/analyze_dependencies")
async def analyze_dependencies(request):
"""
Analyze dependencies for a custom node from git URL.
Fetches requirements.txt, checks installed packages, and returns dependency list with status.
"""
try:
json_data = await request.json()
url = json_data.get('url')
commit_id = json_data.get('commitId')
branch = json_data.get('branch')
if not url:
return web.json_response({'error': 'URL is required'}, status=400)
# Fetch requirements.txt from git repository
requirements_content = await fetch_requirements_from_git(url, commit_id, branch)
if requirements_content is None:
return web.json_response({
'success': True,
'requirements': None,
'dependencies': [],
'noRequirementsFile': True
})
# Parse requirements
dependencies = parse_requirements(requirements_content)
# Get installed packages
installed_packages = manager_util.get_installed_packages()
# Analyze each dependency with subdependencies
analyzed_dependencies = []
for dep_line in dependencies:
if not dep_line.strip() or dep_line.strip().startswith('#'):
continue
# Parse dependency line
parsed = manager_util.parse_requirement_line(dep_line)
if not parsed:
continue
package_name = parsed.get('package')
if not package_name:
# Fallback: extract from line if package is missing
import re
match = re.match(r'^([a-zA-Z0-9_.-]+)', dep_line.strip())
package_name = match.group(1) if match else "Unknown"
version_spec = parsed.get('version')
# Convert version_spec to string if it's a StrictVersion object
if version_spec is not None:
version_spec = str(version_spec)
normalized_name = package_name.lower().replace('-', '_')
# Check if already installed
installed_version = installed_packages.get(normalized_name)
status = 'new'
if installed_version:
status = 'installed'
# Convert version to string if it's not already (handle StrictVersion objects)
current_version_str = str(installed_version) if installed_version else None
# Get subdependencies using pip install --dry-run
# This is optional and failures should not block the main flow
subdependencies = []
# Skip subdependency analysis for already installed packages (not needed)
if status != 'installed':
try:
import subprocess
import sys
# Run pip install --dry-run to get subdependencies
# Some packages like pymeshlab can take longer due to complex dependency resolution
# Use a reasonable timeout - if it times out, we'll continue without subdependencies
result = subprocess.run(
[sys.executable, '-m', 'pip', 'install', '--dry-run', dep_line.strip()],
capture_output=True,
text=True,
timeout=45 # Increased timeout to 45 seconds
)
output = result.stdout + result.stderr
if output:
subdependencies = parse_dry_run_output(output, package_name, installed_packages)
except subprocess.TimeoutExpired:
# Timeout is not critical - continue without subdependencies
logging.debug(f"Subdependency analysis timed out for {package_name} (skipping subdependencies)")
subdependencies = []
except Exception as e:
# Any other error is not critical - continue without subdependencies
logging.debug(f"Failed to analyze subdependencies for {package_name}: {e}")
subdependencies = []
# Add main dependency (always add, even if subdependency analysis failed)
# Ensure all fields are properly set and clean
clean_package_name = str(package_name).strip() if package_name else "Unknown"
# Remove any None/null strings that might have been concatenated
clean_package_name = clean_package_name.replace('None', '').replace('null', '').strip()
if not clean_package_name:
clean_package_name = "Unknown"
analyzed_dependencies.append({
'name': clean_package_name,
'version': str(version_spec) if version_spec else None,
'line': dep_line.strip(),
'status': status,
'currentVersion': current_version_str,
'selected': status != 'installed', # Deselect if already installed
'subdependencies': subdependencies
})
return web.json_response({
'success': True,
'requirements': requirements_content,
'dependencies': analyzed_dependencies,
'noRequirementsFile': False
})
except Exception as e:
logging.error(f"Error analyzing dependencies: {e}")
traceback.print_exc()
return web.json_response({'error': str(e)}, status=500)
def parse_requirements(content):
"""Parse requirements.txt content into list of dependency lines."""
lines = []
for line in content.split('\n'):
line = line.strip()
if line and not line.startswith('#'):
lines.append(line)
return lines
def parse_dry_run_output(output, parent_name, installed_packages):
"""Parse pip install --dry-run output to extract subdependencies."""
import re
subdependencies = []
subdeps_map = {}
lines = output.split('\n')
for line in lines:
line = line.strip()
# Look for "Collecting package==version" lines
if 'Collecting ' in line and 'Using cached' not in line:
# Match: "Collecting package==version" or "Collecting package"
match = re.search(r'Collecting\s+([a-zA-Z0-9_.-]+(?:\[[^\]]+\])?)(?:\s*==\s*([^\s\(]+))?', line)
if match:
dep_name = match.group(1).split('[')[0].strip()
# Clean the name - remove any None/null strings
if dep_name:
dep_name = dep_name.replace('None', '').replace('null', '').strip()
dep_version = match.group(2).strip() if match.group(2) else None
# Clean version too
if dep_version:
dep_version = dep_version.replace('None', '').replace('null', '').strip() or None
# Skip the parent package itself
if dep_name.lower() == parent_name.lower():
continue
# Normalize name
normalized_name = dep_name.lower().replace('-', '_')
# Check if already in map (avoid duplicates)
if normalized_name not in subdeps_map:
# Check if already installed
installed_version = installed_packages.get(normalized_name)
status = 'installed' if installed_version else 'new'
current_version_str = str(installed_version) if installed_version else None
# Ensure name is always a string, not None
if not dep_name:
dep_name = "Unknown"
# Clean the name - remove any None/null strings
clean_dep_name = str(dep_name).strip().replace('None', '').replace('null', '').strip()
if not clean_dep_name:
clean_dep_name = "Unknown"
subdeps_map[normalized_name] = {
'name': clean_dep_name,
'version': str(dep_version) if dep_version else None,
'status': status,
'currentVersion': current_version_str,
'selected': status != 'installed'
}
# Also look for "Would install" lines which have more accurate version info
if 'Would install' in line:
# Match: "Would install package-version"
match = re.search(r'Would install\s+([a-zA-Z0-9_.-]+)-([\d.]+)', line)
if match:
dep_name = match.group(1)
dep_version = match.group(2)
normalized_name = dep_name.lower().replace('-', '_')
if normalized_name in subdeps_map:
# Update with more accurate version
subdeps_map[normalized_name]['version'] = dep_version
# Convert map to list
for normalized_name, dep_info in subdeps_map.items():
subdependencies.append(dep_info)
return subdependencies
async def fetch_requirements_from_git(url, commit_id=None, branch=None):
"""
Fetch requirements.txt from a git repository URL.
Supports GitHub URLs by converting to raw.githubusercontent.com.
"""
try:
# Extract GitHub repo info
if 'github.com' in url:
# Convert to raw GitHub URL
url = url.rstrip('/')
if url.endswith('.git'):
url = url[:-4]
# Extract owner/repo
match = re.search(r'github\.com[:/]([^/]+)/([^/]+)', url)
if not match:
return None
owner = match.group(1)
repo = match.group(2)
# Build raw URL
if commit_id:
raw_url = f"https://raw.githubusercontent.com/{owner}/{repo}/{commit_id}/requirements.txt"
elif branch:
raw_url = f"https://raw.githubusercontent.com/{owner}/{repo}/{branch}/requirements.txt"
else:
raw_url = f"https://raw.githubusercontent.com/{owner}/{repo}/main/requirements.txt"
# Try to fetch using aiohttp
async with aiohttp.ClientSession() as session:
async with session.get(raw_url) as response:
if response.status == 200:
return await response.text()
# Try with master branch if main fails
if 'main' in raw_url:
raw_url = raw_url.replace('/main/', '/master/')
async with session.get(raw_url) as response2:
if response2.status == 200:
return await response2.text()
else:
# For non-GitHub URLs, we'd need to clone temporarily
# For now, return None (can be enhanced later)
logging.warning(f"Non-GitHub URL not fully supported for dependency analysis: {url}")
return None
return None
except Exception as e:
logging.error(f"Error fetching requirements from git: {e}")
return None
@routes.post("/manager/queue/uninstall")
async def uninstall_custom_node(request):
if not is_allowed_security_level('middle'):

View File

@ -67,6 +67,7 @@ export class CustomNodesManager {
this.filter = '';
this.keywords = '';
this.restartMap = {};
this.analyzeDependenciesBeforeInstall = false; // Default: false
this.init();
@ -77,6 +78,36 @@ export class CustomNodesManager {
}
init() {
// Create checkbox for dependency analysis
const analyzeDepsCheckbox = $el("input", {
type: "checkbox",
id: "cn-analyze-deps-checkbox",
checked: this.analyzeDependenciesBeforeInstall,
onchange: (e) => {
this.analyzeDependenciesBeforeInstall = e.target.checked;
},
style: {
marginRight: "6px",
cursor: "pointer"
}
});
const analyzeDepsLabel = $el("label", {
for: "cn-analyze-deps-checkbox",
style: {
display: "flex",
alignItems: "center",
cursor: "pointer",
color: "#fff",
fontSize: "12px",
marginRight: "10px",
whiteSpace: "nowrap"
}
}, [
analyzeDepsCheckbox,
$el("span", { textContent: "Analyse dependencies before node installation" })
]);
const header = $el("div.cn-manager-header.px-2", {}, [
// $el("label", {}, [
// $el("span", { textContent: "Filter" }),
@ -84,6 +115,7 @@ export class CustomNodesManager {
// ]),
createSettingsCombo("Filter", $el("select.cn-manager-filter")),
$el("input.cn-manager-keywords.p-inputtext.p-component", { type: "search", placeholder: "Search" }),
analyzeDepsLabel,
$el("div.cn-manager-status"),
$el("div.cn-flex-auto"),
$el("div.cn-manager-channel")
@ -105,6 +137,421 @@ export class CustomNodesManager {
this.initGrid();
}
showDependencySelectorDialog(dependencies, onSelect) {
const dialog = new ComfyDialog();
dialog.element.style.zIndex = 1100;
dialog.element.style.width = "900px";
dialog.element.style.maxHeight = "80vh";
dialog.element.style.padding = "0";
dialog.element.style.backgroundColor = "#2a2a2a";
dialog.element.style.border = "1px solid #3a3a3a";
dialog.element.style.borderRadius = "8px";
dialog.element.style.boxSizing = "border-box";
dialog.element.style.overflow = "hidden";
const contentStyle = {
width: "100%",
display: "flex",
flexDirection: "column",
padding: "20px",
boxSizing: "border-box",
gap: "15px"
};
// Create scrollable table container with sticky header
const tableContainer = $el("div", {
style: {
maxHeight: "500px",
overflowY: "auto",
border: "1px solid #4a4a4a",
borderRadius: "4px",
backgroundColor: "#1a1a1a",
position: "relative"
}
});
// Create table
const table = $el("table", {
style: {
width: "100%",
borderCollapse: "separate",
borderSpacing: "0",
fontSize: "14px"
}
});
// Create table header with sticky positioning
const thead = $el("thead", {
style: {
position: "sticky",
top: "0",
zIndex: "10",
backgroundColor: "#2a2a2a",
boxShadow: "0 2px 4px rgba(0,0,0,0.3)"
}
}, [
$el("tr", {
style: {
backgroundColor: "#2a2a2a",
borderBottom: "2px solid #4a4a4a"
}
}, [
$el("th", {
textContent: "",
style: {
padding: "10px",
textAlign: "left",
width: "40px",
color: "#fff"
}
}),
$el("th", {
textContent: "Dependency Name",
style: {
padding: "10px",
textAlign: "left",
color: "#fff",
fontWeight: "bold"
}
}),
$el("th", {
textContent: "Current Version",
style: {
padding: "10px",
textAlign: "left",
color: "#fff",
fontWeight: "bold"
}
}),
$el("th", {
textContent: "Incoming Version",
style: {
padding: "10px",
textAlign: "left",
color: "#fff",
fontWeight: "bold"
}
})
])
]);
// Create table body
const tbody = $el("tbody", {});
// Create table rows for each dependency and its subdependencies
let rowIndex = 0;
dependencies.forEach((dep) => {
// Ensure name is not null/undefined and clean it
let depName = dep.name;
if (!depName || depName === 'null' || depName === 'None') {
// Fallback: extract from line
if (dep.line) {
depName = dep.line.split(/[=<>!~]/)[0].trim();
} else {
depName = "Unknown";
}
}
// Remove any "null" suffix that might have been appended
depName = String(depName).replace(/null$/i, '').trim();
const isInstalled = dep.status === 'installed';
const incomingVersion = dep.version || "NA";
const currentVersion = dep.currentVersion || "NA";
// Main dependency row
const row = $el("tr", {
style: {
backgroundColor: rowIndex % 2 === 0 ? "#1a1a1a" : "#222222",
borderBottom: "1px solid #3a3a3a"
}
}, [
$el("td", {
style: {
padding: "10px",
textAlign: "center"
}
}, [
$el("input", {
type: "checkbox",
checked: dep.selected,
onchange: (e) => {
dep.selected = e.target.checked;
},
style: {
cursor: "pointer",
width: "18px",
height: "18px"
}
})
]),
$el("td", {
style: {
padding: "10px",
color: isInstalled ? "#888" : "#fff"
}
}, [
$el("span", {
textContent: depName,
style: {
fontWeight: "500",
marginRight: isInstalled ? "8px" : "0"
}
}),
isInstalled ? $el("span", {
textContent: "Installed",
style: {
display: "inline-block",
backgroundColor: "#2a4a2a",
color: "#4a9",
padding: "2px 6px",
borderRadius: "3px",
fontSize: "10px",
fontWeight: "bold",
border: "1px solid #4a9"
}
}) : ''
]),
$el("td", {
textContent: currentVersion,
style: {
padding: "10px",
color: isInstalled ? "#4a9" : "#aaa",
fontFamily: "monospace"
}
}),
$el("td", {
textContent: incomingVersion,
style: {
padding: "10px",
color: "#fff",
fontFamily: "monospace"
}
})
]);
tbody.appendChild(row);
rowIndex++;
// Add subdependencies as indented rows
if(dep.subdependencies && dep.subdependencies.length > 0) {
dep.subdependencies.forEach((subdep) => {
// Ensure subdependency name is not null/undefined and clean it
let subdepName = subdep.name;
if (!subdepName || subdepName === 'null' || subdepName === 'None') {
subdepName = "Unknown";
}
// Remove any "null" suffix that might have been appended
subdepName = String(subdepName).replace(/null$/i, '').trim();
const subIsInstalled = subdep.status === 'installed';
const subIncomingVersion = subdep.version || "NA";
const subCurrentVersion = subdep.currentVersion || "NA";
const subRow = $el("tr", {
style: {
backgroundColor: rowIndex % 2 === 0 ? "#1a1a1a" : "#222222",
borderBottom: "1px solid #3a3a3a"
}
}, [
$el("td", {
style: {
padding: "10px",
textAlign: "center"
}
}, [
$el("input", {
type: "checkbox",
checked: subdep.selected,
onchange: (e) => {
subdep.selected = e.target.checked;
},
style: {
cursor: "pointer",
width: "18px",
height: "18px"
}
})
]),
$el("td", {
style: {
padding: "10px 10px 10px 30px",
color: subIsInstalled ? "#888" : "#aaa",
fontSize: "13px"
}
}, [
$el("span", {
textContent: "└─ " + subdepName,
style: {
fontWeight: "400",
marginRight: subIsInstalled ? "8px" : "0"
}
}),
subIsInstalled ? $el("span", {
textContent: "Installed",
style: {
display: "inline-block",
backgroundColor: "#2a4a2a",
color: "#4a9",
padding: "2px 6px",
borderRadius: "3px",
fontSize: "10px",
fontWeight: "bold",
border: "1px solid #4a9"
}
}) : ''
]),
$el("td", {
textContent: subCurrentVersion,
style: {
padding: "10px",
color: subIsInstalled ? "#4a9" : "#666",
fontFamily: "monospace",
fontSize: "13px"
}
}),
$el("td", {
textContent: subIncomingVersion,
style: {
padding: "10px",
color: "#aaa",
fontFamily: "monospace",
fontSize: "13px"
}
})
]);
tbody.appendChild(subRow);
rowIndex++;
});
}
});
table.appendChild(thead);
table.appendChild(tbody);
tableContainer.appendChild(table);
const content = $el("div", {
style: contentStyle
}, [
$el("h3", {
textContent: "Select Dependencies to Install",
style: {
color: "#ffffff",
backgroundColor: "#1a1a1a",
padding: "10px 15px",
margin: "0 0 10px 0",
width: "100%",
textAlign: "center",
borderRadius: "4px",
boxSizing: "border-box"
}
}),
$el("div", {
textContent: `${dependencies.filter(d => d.status === 'installed').length} already installed, ${dependencies.filter(d => d.status !== 'installed').length} to install`,
style: {
color: "#aaa",
fontSize: "12px",
marginBottom: "5px"
}
}),
tableContainer,
$el("div", {
style: {
display: "flex",
justifyContent: "space-between",
width: "100%",
gap: "10px",
marginTop: "10px"
}
}, [
$el("button", {
textContent: "Cancel",
onclick: () => {
onSelect(null); // Pass null to indicate cancellation
dialog.close();
},
style: {
flex: "1",
padding: "8px",
backgroundColor: "#4a4a4a",
color: "#ffffff",
border: "none",
borderRadius: "4px",
cursor: "pointer"
}
}),
$el("button", {
textContent: "Select All",
onclick: () => {
dependencies.forEach(dep => {
if (dep.status !== 'installed') {
dep.selected = true;
}
// Also select subdependencies
if(dep.subdependencies) {
dep.subdependencies.forEach(subdep => {
if(subdep.status !== 'installed') {
subdep.selected = true;
}
});
}
});
// Update checkboxes in the table
const checkboxes = tableContainer.querySelectorAll('input[type="checkbox"]');
checkboxes.forEach((checkbox) => {
if(!checkbox.disabled) {
checkbox.checked = true;
}
});
},
style: {
padding: "8px 15px",
backgroundColor: "#4a6a4a",
color: "#ffffff",
border: "none",
borderRadius: "4px",
cursor: "pointer"
}
}),
$el("button", {
textContent: "Install Selected",
onclick: () => {
// Collect all selected dependencies (main + subdependencies)
const selected = [];
dependencies.forEach(d => {
if(d.selected) {
selected.push(d);
}
// Also include selected subdependencies
if(d.subdependencies) {
d.subdependencies.forEach(subdep => {
if(subdep.selected) {
selected.push(subdep);
}
});
}
});
onSelect(selected);
dialog.close();
},
style: {
flex: "1",
padding: "8px",
backgroundColor: "#4CAF50",
color: "#ffffff",
border: "none",
borderRadius: "4px",
cursor: "pointer"
}
}),
])
]);
console.log('[Dependency Dialog] Showing dialog with', dependencies.length, 'dependencies');
dialog.show(content);
console.log('[Dependency Dialog] Dialog shown');
}
showVersionSelectorDialog(versions, onSelect) {
const dialog = new ComfyDialog();
dialog.element.style.zIndex = 1100;
@ -1470,6 +1917,101 @@ export class CustomNodesManager {
}
}
// For install mode, analyze dependencies BEFORE starting installation
let selectedDependencies = [];
let dependencyDialogShown = false; // Track if dialog was shown
if(mode === "install" && this.analyzeDependenciesBeforeInstall) {
// Analyze dependencies for all items first (only if checkbox is enabled)
for (const hash of list) {
const item = this.grid.getRowItemBy("hash", hash);
if (!item) {
console.log('[Dependency Analysis] Item not found for hash:', hash);
continue;
}
const data = item.originalData;
console.log('[Dependency Analysis] Item data:', {
title: item.title,
files: data.files,
repository: data.repository,
hasFiles: !!data.files,
filesLength: data.files ? data.files.length : 0
});
// Try multiple ways to get the git URL
let gitUrl = null;
if(data.files && data.files.length > 0) {
gitUrl = data.files[0];
} else if(data.repository) {
gitUrl = data.repository;
}
if(gitUrl && (gitUrl.includes('github.com') || gitUrl.includes('.git'))) {
try {
this.showStatus(`Analyzing dependencies for ${item.title}...`);
console.log('[Dependency Analysis] Fetching dependencies for:', gitUrl);
const analyzeRes = await api.fetchApi('/customnode/analyze_dependencies', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
url: gitUrl,
commitId: data.commit_id,
branch: data.branch
})
});
console.log('[Dependency Analysis] Response status:', analyzeRes.status);
if(analyzeRes.status === 200) {
const analyzeData = await analyzeRes.json();
console.log('[Dependency Analysis] Response data:', {
success: analyzeData.success,
hasDependencies: !!analyzeData.dependencies,
dependenciesCount: analyzeData.dependencies ? analyzeData.dependencies.length : 0,
noRequirementsFile: analyzeData.noRequirementsFile
});
if(analyzeData.success && analyzeData.dependencies && analyzeData.dependencies.length > 0) {
console.log('[Dependency Analysis] Showing dialog with', analyzeData.dependencies.length, 'dependencies');
dependencyDialogShown = true;
// Show dependency selection dialog and wait for user
const userSelection = await new Promise((resolve) => {
this.showDependencySelectorDialog(analyzeData.dependencies, (selected) => {
console.log('[Dependency Analysis] User selected:', selected);
resolve(selected);
});
});
// If user cancelled (null), stop installation
if(userSelection === null) {
console.log('[Dependency Analysis] User cancelled installation');
this.showStatus("Installation cancelled");
return;
}
selectedDependencies = userSelection || [];
console.log('[Dependency Analysis] Selected dependencies:', selectedDependencies.length);
} else if(analyzeData.noRequirementsFile) {
console.log('[Dependency Analysis] No requirements.txt file found');
} else {
console.log('[Dependency Analysis] No dependencies to show');
}
} else {
const errorText = await analyzeRes.text();
console.error('[Dependency Analysis] API error:', analyzeRes.status, errorText);
}
} catch(e) {
console.error('[Dependency Analysis] Exception:', e);
// Continue with installation even if dependency analysis fails
}
} else {
console.log('[Dependency Analysis] Not a GitHub URL or no URL found:', gitUrl);
}
}
}
target.classList.add("cn-btn-loading");
this.showError("");
@ -1505,6 +2047,46 @@ export class CustomNodesManager {
data.mode = this.mode;
data.ui_id = hash;
// Add selected dependencies to data (including subdependencies)
// Only install selected dependencies - respect user's selection
const allSelected = [];
if(selectedDependencies.length > 0) {
selectedDependencies.forEach(d => {
// Add main dependency if selected
if(d.selected) {
allSelected.push({
name: d.name,
version: d.version,
line: d.line
});
}
// Add selected subdependencies
if(d.subdependencies) {
d.subdependencies.forEach(subdep => {
if(subdep.selected) {
allSelected.push({
name: subdep.name,
version: subdep.version,
line: `${subdep.name}${subdep.version ? '==' + subdep.version : ''}`
});
}
});
}
});
}
// Set selectedDependencies:
// - If dialog was shown: always set (even if empty) to respect user's selection
// - If dialog was not shown: don't set (install all dependencies - original behavior)
if(dependencyDialogShown) {
// User saw the dialog, respect their selection (even if empty)
data.selectedDependencies = allSelected;
} else if(allSelected.length > 0) {
// Dialog wasn't shown but we have selections (shouldn't happen, but just in case)
data.selectedDependencies = allSelected;
}
// If dialog wasn't shown and no selections, don't set selectedDependencies
// This means backend will install all dependencies (original behavior)
let install_mode = mode;
if(mode == 'switch') {
install_mode = 'install';

View File

@ -1,15 +1,5 @@
{
"custom_nodes": [
{
"author": "ah-kun",
"title": "ComfyUI-OTP-Auth",
"reference": "https://github.com/ah-kun/ComfyUI-OTP-Auth",
"files": [
"https://github.com/ah-kun/ComfyUI-OTP-Auth"
],
"install_type": "git-clone",
"description": "A custom node for ComfyUI that adds simple One-Time Password (OTP) authentication using Google Authenticator to prevent unauthorized use on publicly accessible servers."
},
{
"author": "pollockjj",
"title": "ComfyUI-StabilityTest",

View File

@ -2681,6 +2681,7 @@
"RK_Excel_File_State_Looper",
"RK_ImageViewer",
"RK_Read_Excel_Row",
"RK_Ultra",
"RK_Write_Text",
"RK_seed",
"rk_save_image",
@ -4717,6 +4718,8 @@
"AddVideoTextWatermark",
"GetDeviceType",
"LoadAudioByUrl",
"LoadImageByUrl",
"LoadVideoByUrl",
"SeparateVideoAudio",
"Wav2Srt"
],
@ -6948,6 +6951,7 @@
"BatchLatentsNode",
"BatchMasksNode",
"BetaSamplingScheduler",
"BriaImageEditNode",
"ByteDanceFirstLastFrameNode",
"ByteDanceImageEditNode",
"ByteDanceImageNode",
@ -7450,6 +7454,7 @@
"TextEncodeHunyuanVideo_ImageToVideo",
"TextEncodeQwenImageEdit",
"TextEncodeQwenImageEditPlus",
"TextEncodeZImageOmni",
"TextProcessingNode",
"ThresholdMask",
"TomePatchModel",
@ -8714,7 +8719,8 @@
"XIS_ShapeData",
"XIS_StringListMerger",
"XIS_StringSwitch",
"XIS_UnpackImages"
"XIS_UnpackImages",
"XIS_VGMOrchestrator"
],
{
"title_aux": "Xiser_Nodes [WIP]"
@ -12800,6 +12806,7 @@
],
"https://github.com/sprited-ai/sprited-comfyui-nodes": [
[
"BiRefNetBackgroundRemoval",
"FlattenImageList",
"LoopExtractorNodeV2",
"LoopExtractorNodeV3",

View File

@ -1,5 +1,16 @@
{
"custom_nodes": [
{
"author": "gen1nya",
"title": "ComfyUI Remote Upscale",
"id": "remote-upscale",
"reference": "https://github.com/gen1nya/ComfyUI-Remote-Upscale",
"files": [
"https://github.com/gen1nya/ComfyUI-Remote-Upscale"
],
"install_type": "git-clone",
"description": "Offload image upscaling to a remote server with a dedicated GPU. Real-time progress streaming via SSE."
},
{
"author": "martin-rizzo",
"title": "ComfyUI-ZImagePowerNodes",
@ -99,7 +110,7 @@
"https://github.com/rookiestar28/ComfyUI-TranslateGemma"
],
"install_type": "git-clone",
"description": "ComfyUI node for TranslateGemma — Google's open source translation models with 55 languages & multimodal image translation."
"description": "A ComfyUI custom node that translates text (including prompts) using Google's open-weight TranslateGemma models."
},
{
"author": "BigStationW",
@ -338,12 +349,13 @@
{
"author": "bkpaine1",
"title": "Halo-Lipsy",
"id": "halo-lipsy",
"reference": "https://github.com/bkpaine1/Halo-Lipsy",
"files": [
"https://github.com/bkpaine1/Halo-Lipsy"
],
"install_type": "git-clone",
"description": "Native AMD Unified Memory Lip Sync for ComfyUI"
"description": "AMD unified memory lip sync for ComfyUI. Native Wav2Lip inference with no subprocesses - finally works on Strix Halo, ROCm, and all AMD APUs. Features sync tuning, edge blending, and safe tensor casting for unified memory systems."
},
{
"author": "kianthos",
@ -376,14 +388,26 @@
"description": "Comprehensive mobile UI enhancement for ComfyUI that transforms the desktop-focused interface into a touch-friendly experience optimized for iPhone, iPad, and Android devices."
},
{
"author": "aadityamundhalia",
"title": "ComfyUI-ollama-aditya",
"author": "Aditya Mundhalia",
"title": "ComfyUI Ollama by Aditya",
"id": "comfyui-ollama-aditya",
"reference": "https://github.com/aadityamundhalia/ComfyUI-ollama-aditya",
"files": [
"https://github.com/aadityamundhalia/ComfyUI-ollama-aditya"
],
"install_type": "git-clone",
"description": "Custom ComfyUI node for integrating Ollama LLMs into your image generation workflows."
"description": "ComfyUI nodes for integrating Ollama local LLM models with image or text generation workflows."
},
{
"author": "Setmaster",
"title": "ModelPulse",
"id": "modelpulse",
"reference": "https://github.com/Setmaster/ComfyUI-ModelPulse",
"files": [
"https://github.com/Setmaster/ComfyUI-ModelPulse"
],
"install_type": "git-clone",
"description": "Track model usage frequency to identify abandoned or underutilized models. Shows usage stats, file sizes, and highlights stale models in a sidebar panel."
},
{
"author": "maximilianwicen",

View File

@ -693,11 +693,12 @@
],
"https://github.com/5agado/ComfyUI-Sagado-Nodes": [
[
"Film Grain",
"Get Num Frames",
"Get Resolution",
"Image Loader",
"Video Loader"
"SGD_Call_Ollama",
"SGD_Film_Grain",
"SGD_Get_Num_Frames",
"SGD_Get_Resolution",
"SGD_Image_Loader",
"SGD_Video_Loader"
],
{
"title_aux": "Sagado Nodes for ComfyUI"
@ -3319,6 +3320,9 @@
"Vigenere",
"X963KDF_Derive",
"X963KDF_Verify",
"XExchange",
"XPrivateKeyFormat",
"XPublicKeyFormat",
"Zigzag"
],
{
@ -6507,6 +6511,7 @@
"BoyoVideoPairedSaver",
"BoyoVideoSaver",
"BoyoVision",
"BoyoVoiceEnhancer",
"BoyoWhileLoopEnd",
"BoyoWhileLoopStart",
"Boyolatent",
@ -6903,7 +6908,8 @@
],
"https://github.com/Enferlain/ComfyUI-A1111-cond": [
[
"A1111Prompt"
"A1111Prompt",
"A1111PromptNegative"
],
{
"title_aux": "ComfyUI-A1111-cond"
@ -8465,7 +8471,8 @@
],
"https://github.com/GavChap/ComfyUI-SD3LatentSelectRes": [
[
"SD3LatentSelectRes"
"SD3LatentSelectRes",
"SD3LatentSelectResV2"
],
{
"title_aux": "ComfyUI-SD3LatentSelectRes"
@ -18759,6 +18766,7 @@
"Basic data handling: IntPower",
"Basic data handling: IntSubtract",
"Basic data handling: IntToBytes",
"Basic data handling: IsConnected",
"Basic data handling: IsNull",
"Basic data handling: LessThan",
"Basic data handling: LessThanOrEqual",
@ -22785,7 +22793,7 @@
"OllamaPromptGenerator"
],
{
"title_aux": "ComfyUI-ollama-aditya"
"title_aux": "ComfyUI Ollama by Aditya"
}
],
"https://github.com/abdozmantar/ComfyUI-DeepExtract": [
@ -23561,6 +23569,7 @@
"GetImagesFromBatchIndexed",
"GetLatentRangeFromBatch",
"GetLatentSizeAndCount",
"IO_save_image",
"ImageAddMulti",
"ImageAndMaskPreview",
"ImageBatchExtendWithOverlap",
@ -27287,7 +27296,7 @@
],
"https://github.com/capitan01R/ComfyUI-Flux2Klein-Enhancer": [
[
"Flux2KleinEditController",
"Flux2KleinDetailController",
"Flux2KleinEnhancer"
],
{
@ -29732,6 +29741,7 @@
"BatchLatentsNode",
"BatchMasksNode",
"BetaSamplingScheduler",
"BriaImageEditNode",
"ByteDanceFirstLastFrameNode",
"ByteDanceImageEditNode",
"ByteDanceImageNode",
@ -30234,6 +30244,7 @@
"TextEncodeHunyuanVideo_ImageToVideo",
"TextEncodeQwenImageEdit",
"TextEncodeQwenImageEditPlus",
"TextEncodeZImageOmni",
"TextProcessingNode",
"ThresholdMask",
"TomePatchModel",
@ -32815,6 +32826,8 @@
],
"https://github.com/ethanfel/ComfyUI-Sharp-Selector": [
[
"FastAbsoluteSaver",
"ParallelSharpnessLoader",
"SharpFrameSelector",
"SharpnessAnalyzer"
],
@ -34431,6 +34444,15 @@
"title_aux": "ComfyUI_GMIC"
}
],
"https://github.com/gen1nya/ComfyUI-Remote-Upscale": [
[
"RefreshRemoteModels",
"RemoteUpscaleImage"
],
{
"title_aux": "ComfyUI Remote Upscale"
}
],
"https://github.com/geocine/geocine-comfyui": [
[
"Image Scale",
@ -35456,6 +35478,8 @@
"AIIA_CosyVoice_TTS",
"AIIA_CosyVoice_VoiceConversion",
"AIIA_Dialogue_TTS",
"AIIA_DittoLoader",
"AIIA_DittoSampler",
"AIIA_E2E_Speaker_Diarization",
"AIIA_EchoMimicLoader",
"AIIA_EchoMimicSampler",
@ -38096,6 +38120,7 @@
],
"https://github.com/joeriben/ai4artsed_comfyui_nodes": [
[
"ai4artsed_clean_prompt_randomizer",
"ai4artsed_conditioning_fusion",
"ai4artsed_image_analysis",
"ai4artsed_openrouter_key",
@ -39618,6 +39643,7 @@
"Intrinsic_lora_sampling",
"JoinStringMulti",
"JoinStrings",
"LTX2AttentionTunerPatch",
"LTX2AudioLatentNormalizingSampling",
"LTX2SamplingPreviewOverride",
"LTX2_NAG",
@ -39639,6 +39665,7 @@
"MaskBatchMulti",
"MaskOrImageToWeight",
"MergeImageChannels",
"ModelMemoryUseReportPatch",
"ModelPassThrough",
"ModelPatchTorchSettings",
"ModelSaveKJ",
@ -47543,7 +47570,8 @@
],
"https://github.com/princepainter/ComfyUI-PainterLTXV2": [
[
"PainterLTXVtoVideo",
"PainterLTX2V",
"PainterLTX2VPlus",
"PainterSamplerLTXV"
],
{
@ -49912,6 +49940,7 @@
"CloseUpImageNode",
"CloseUpNode",
"ComfyAddSoundtrack",
"ComfyImageAudioCSV",
"ImageSequenceOverlay",
"ImageTransitionNode",
"MergeVideoAudioNode",
@ -51692,6 +51721,7 @@
"https://github.com/sonnybox/ComfyUI-SuperNodes": [
[
"FaceBBoxToMask",
"GetCommonAspectRatio",
"ImageMaskCrop",
"ImageSizeCalculator",
"RestoreMaskCrop",
@ -57204,6 +57234,7 @@
],
"https://github.com/zeeoale/PromptCreatorNode": [
[
"IdentityMixerNode",
"PromptCreatorNode"
],
{

View File

@ -20,7 +20,7 @@ from pathlib import Path
from typing import Set, Dict, Optional
# Scanner version for cache invalidation
SCANNER_VERSION = "2.0.12" # Add dict comprehension + export list detection
SCANNER_VERSION = "2.0.13" # Add fallback for dynamic v3 node_id
# Cache for extract_nodes and extract_nodes_enhanced results
_extract_nodes_cache: Dict[str, Set[str]] = {}
@ -936,6 +936,9 @@ def extract_v3_nodes(code_text):
node_id = extract_node_id_from_schema(node)
if node_id:
nodes.add(node_id)
else:
# Fallback: use class name when node_id is dynamic/empty
nodes.add(node.name)
return nodes