added new tests
This commit is contained in:
@@ -9,15 +9,19 @@ on:
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["3.10", "3.11", "3.12"]
|
||||
fail-fast: false
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.10"
|
||||
python-version: ${{ matrix.python-version }}
|
||||
cache: 'pip'
|
||||
|
||||
- name: Install dependencies
|
||||
@@ -44,5 +48,5 @@ jobs:
|
||||
- name: Test summary
|
||||
if: always()
|
||||
run: |
|
||||
echo "## Test Results" >> $GITHUB_STEP_SUMMARY
|
||||
echo "## Test Results (Python ${{ matrix.python-version }})" >> $GITHUB_STEP_SUMMARY
|
||||
echo "✅ Unit tests completed" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
name: Windows Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main, master, dev]
|
||||
pull_request:
|
||||
branches: [main, master, dev]
|
||||
|
||||
jobs:
|
||||
test-windows:
|
||||
runs-on: windows-latest
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["3.10", "3.11", "3.12"]
|
||||
fail-fast: false
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
cache: 'pip'
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install pytest pytest-cov pytest-mock pytest-asyncio
|
||||
pip install -r .github/requirements-ci.txt
|
||||
pip install -r requirements-dev.txt
|
||||
|
||||
- name: Run smoke test (import check)
|
||||
run: |
|
||||
python -c "import sys; sys.path.insert(0, '.'); print('Testing module import...'); exec(open('tests/smoke_test.py').read())"
|
||||
env:
|
||||
PYTHONPATH: ${{ github.workspace }}
|
||||
|
||||
- name: Run unit tests
|
||||
run: |
|
||||
pytest tests/unit/ -v --cov=nodes --cov-report=xml --cov-report=term-missing
|
||||
env:
|
||||
PYTHONPATH: ${{ github.workspace }}
|
||||
|
||||
- name: Upload coverage reports
|
||||
uses: codecov/codecov-action@v4
|
||||
with:
|
||||
file: ./coverage.xml
|
||||
flags: windows-tests
|
||||
name: codecov-windows-py${{ matrix.python-version }}
|
||||
fail_ci_if_error: false
|
||||
|
||||
- name: Test summary
|
||||
if: always()
|
||||
run: |
|
||||
echo "## Windows Test Results (Python ${{ matrix.python-version }})" >> $env:GITHUB_STEP_SUMMARY
|
||||
echo "✅ Windows tests completed" >> $env:GITHUB_STEP_SUMMARY
|
||||
+59
-13
@@ -7,23 +7,69 @@ Now includes SAM2 segmentation nodes
|
||||
# Only run initialization and imports when loaded by ComfyUI, not during pytest
|
||||
# This prevents relative import errors when pytest collects test modules
|
||||
import sys
|
||||
import traceback
|
||||
|
||||
# Track initialization status
|
||||
INIT_SUCCESS = False
|
||||
INIT_ERRORS = []
|
||||
|
||||
if 'pytest' not in sys.modules:
|
||||
from .grounding_init import init
|
||||
# Initialize web extensions for compatibility with older ComfyUI versions
|
||||
init()
|
||||
print("[ComfyUI-Grounding] Initializing custom node...")
|
||||
|
||||
# Step 1: Run initialization script
|
||||
try:
|
||||
from .grounding_init import init
|
||||
# Initialize web extensions for compatibility with older ComfyUI versions
|
||||
init()
|
||||
print("[ComfyUI-Grounding] ✓ Initialization script completed")
|
||||
except Exception as e:
|
||||
error_msg = f"Initialization script failed: {str(e)}"
|
||||
INIT_ERRORS.append(error_msg)
|
||||
print(f"[ComfyUI-Grounding] ⚠️ {error_msg}")
|
||||
print(f"[ComfyUI-Grounding] Traceback:\n{traceback.format_exc()}")
|
||||
|
||||
# Step 2: Import node classes
|
||||
try:
|
||||
from .nodes import (
|
||||
GroundingModelLoader,
|
||||
GroundingDetector,
|
||||
BboxVisualizer,
|
||||
DownLoadSAM2Model,
|
||||
Sam2Segment,
|
||||
GroundingMaskModelLoader,
|
||||
GroundingMaskDetector,
|
||||
BatchCropAndPadFromMask,
|
||||
)
|
||||
print("[ComfyUI-Grounding] ✓ Node classes imported successfully")
|
||||
INIT_SUCCESS = True
|
||||
except Exception as e:
|
||||
error_msg = f"Failed to import node classes: {str(e)}"
|
||||
INIT_ERRORS.append(error_msg)
|
||||
print(f"[ComfyUI-Grounding] ⚠️ {error_msg}")
|
||||
print(f"[ComfyUI-Grounding] Traceback:\n{traceback.format_exc()}")
|
||||
|
||||
# Set all to None if import failed
|
||||
GroundingModelLoader = None
|
||||
GroundingDetector = None
|
||||
BboxVisualizer = None
|
||||
DownLoadSAM2Model = None
|
||||
Sam2Segment = None
|
||||
GroundingMaskModelLoader = None
|
||||
GroundingMaskDetector = None
|
||||
BatchCropAndPadFromMask = None
|
||||
|
||||
# Report final status
|
||||
if INIT_SUCCESS:
|
||||
print("[ComfyUI-Grounding] ✅ Loaded successfully!")
|
||||
else:
|
||||
print(f"[ComfyUI-Grounding] ❌ Failed to load ({len(INIT_ERRORS)} error(s)):")
|
||||
for error in INIT_ERRORS:
|
||||
print(f" - {error}")
|
||||
print("[ComfyUI-Grounding] Please check the errors above and your installation.")
|
||||
|
||||
from .nodes import (
|
||||
GroundingModelLoader,
|
||||
GroundingDetector,
|
||||
BboxVisualizer,
|
||||
DownLoadSAM2Model,
|
||||
Sam2Segment,
|
||||
GroundingMaskModelLoader,
|
||||
GroundingMaskDetector,
|
||||
BatchCropAndPadFromMask,
|
||||
)
|
||||
else:
|
||||
# During testing, set dummy values
|
||||
print("[ComfyUI-Grounding] Running in pytest mode - skipping initialization")
|
||||
GroundingModelLoader = None
|
||||
GroundingDetector = None
|
||||
BboxVisualizer = None
|
||||
|
||||
+238
-56
@@ -3,23 +3,138 @@ ComfyUI-Grounding: Unified object detection nodes for ComfyUI
|
||||
Reorganized architecture with thin wrapper nodes delegating to model implementations
|
||||
"""
|
||||
|
||||
import torch
|
||||
import numpy as np
|
||||
import random
|
||||
import os
|
||||
import sys
|
||||
import traceback
|
||||
|
||||
# Track import failures for diagnostics
|
||||
IMPORT_ERRORS = []
|
||||
|
||||
def log_import_error(module_name, error):
|
||||
"""Log import errors for diagnostics"""
|
||||
error_msg = f"Failed to import {module_name}: {str(error)}"
|
||||
IMPORT_ERRORS.append(error_msg)
|
||||
print(f"[ComfyUI-Grounding] ⚠️ {error_msg}")
|
||||
|
||||
def log_import_success(module_name):
|
||||
"""Log successful imports"""
|
||||
print(f"[ComfyUI-Grounding] ✓ Imported {module_name}")
|
||||
|
||||
# Basic Python dependencies
|
||||
print("[ComfyUI-Grounding] Loading nodes module...")
|
||||
try:
|
||||
import torch
|
||||
import numpy as np
|
||||
import random
|
||||
import os
|
||||
log_import_success("basic dependencies (torch, numpy, etc.)")
|
||||
except ImportError as e:
|
||||
log_import_error("basic dependencies", e)
|
||||
raise
|
||||
|
||||
# Import configurations
|
||||
from .config import MODEL_REGISTRY, MASK_MODEL_REGISTRY
|
||||
from .utils.cache import MODEL_CACHE
|
||||
from .utils import draw_boxes, boxes_to_masks
|
||||
try:
|
||||
from .config import MODEL_REGISTRY, MASK_MODEL_REGISTRY
|
||||
log_import_success("config (MODEL_REGISTRY, MASK_MODEL_REGISTRY)")
|
||||
except Exception as e:
|
||||
log_import_error("config", e)
|
||||
print(f"[ComfyUI-Grounding] Traceback:\n{traceback.format_exc()}")
|
||||
raise
|
||||
|
||||
# Import model loaders and detectors
|
||||
from . import grounding_dino, owlv2, florence2, yolo_world, florence2_seg, sa2va
|
||||
from . import sam2, visualizer
|
||||
# Import utils
|
||||
try:
|
||||
from .utils.cache import MODEL_CACHE
|
||||
log_import_success("utils.cache")
|
||||
except Exception as e:
|
||||
log_import_error("utils.cache", e)
|
||||
print(f"[ComfyUI-Grounding] Traceback:\n{traceback.format_exc()}")
|
||||
raise
|
||||
|
||||
try:
|
||||
from .utils import draw_boxes, boxes_to_masks
|
||||
log_import_success("utils (draw_boxes, boxes_to_masks)")
|
||||
except Exception as e:
|
||||
log_import_error("utils", e)
|
||||
print(f"[ComfyUI-Grounding] Traceback:\n{traceback.format_exc()}")
|
||||
raise
|
||||
|
||||
# Import model modules - each wrapped separately for better diagnostics
|
||||
grounding_dino = None
|
||||
owlv2 = None
|
||||
florence2 = None
|
||||
yolo_world = None
|
||||
florence2_seg = None
|
||||
sa2va = None
|
||||
sam2 = None
|
||||
visualizer = None
|
||||
|
||||
try:
|
||||
from . import grounding_dino
|
||||
log_import_success("grounding_dino")
|
||||
except Exception as e:
|
||||
log_import_error("grounding_dino", e)
|
||||
print(f"[ComfyUI-Grounding] Traceback:\n{traceback.format_exc()}")
|
||||
|
||||
try:
|
||||
from . import owlv2
|
||||
log_import_success("owlv2")
|
||||
except Exception as e:
|
||||
log_import_error("owlv2", e)
|
||||
print(f"[ComfyUI-Grounding] Traceback:\n{traceback.format_exc()}")
|
||||
|
||||
try:
|
||||
from . import florence2
|
||||
log_import_success("florence2")
|
||||
except Exception as e:
|
||||
log_import_error("florence2", e)
|
||||
print(f"[ComfyUI-Grounding] Traceback:\n{traceback.format_exc()}")
|
||||
|
||||
try:
|
||||
from . import yolo_world
|
||||
log_import_success("yolo_world")
|
||||
except Exception as e:
|
||||
log_import_error("yolo_world", e)
|
||||
print(f"[ComfyUI-Grounding] Traceback:\n{traceback.format_exc()}")
|
||||
|
||||
try:
|
||||
from . import florence2_seg
|
||||
log_import_success("florence2_seg")
|
||||
except Exception as e:
|
||||
log_import_error("florence2_seg", e)
|
||||
print(f"[ComfyUI-Grounding] Traceback:\n{traceback.format_exc()}")
|
||||
|
||||
try:
|
||||
from . import sa2va
|
||||
log_import_success("sa2va")
|
||||
except Exception as e:
|
||||
log_import_error("sa2va", e)
|
||||
print(f"[ComfyUI-Grounding] Traceback:\n{traceback.format_exc()}")
|
||||
|
||||
try:
|
||||
from . import sam2
|
||||
log_import_success("sam2")
|
||||
except Exception as e:
|
||||
log_import_error("sam2", e)
|
||||
print(f"[ComfyUI-Grounding] Traceback:\n{traceback.format_exc()}")
|
||||
|
||||
try:
|
||||
from . import visualizer
|
||||
log_import_success("visualizer")
|
||||
except Exception as e:
|
||||
log_import_error("visualizer", e)
|
||||
print(f"[ComfyUI-Grounding] Traceback:\n{traceback.format_exc()}")
|
||||
|
||||
# Get the ComfyUI-Grounding root directory (parent of nodes/)
|
||||
script_directory = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
# Report import status
|
||||
if IMPORT_ERRORS:
|
||||
print(f"[ComfyUI-Grounding] ⚠️ {len(IMPORT_ERRORS)} import error(s) occurred:")
|
||||
for error in IMPORT_ERRORS:
|
||||
print(f" - {error}")
|
||||
print("[ComfyUI-Grounding] Some nodes may not function correctly.")
|
||||
else:
|
||||
print("[ComfyUI-Grounding] ✓ All modules imported successfully")
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Bbox Detection Nodes
|
||||
@@ -33,20 +148,22 @@ class GroundingModelLoader:
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
optional_params = {}
|
||||
# Only add model-specific params if modules loaded successfully
|
||||
if grounding_dino is not None:
|
||||
optional_params.update(grounding_dino.params.LOADER_PARAMS.get("optional", {}))
|
||||
if florence2 is not None:
|
||||
optional_params.update(florence2.params.LOADER_PARAMS.get("optional", {}))
|
||||
if yolo_world is not None:
|
||||
optional_params.update(yolo_world.params.LOADER_PARAMS.get("optional", {}))
|
||||
|
||||
return {
|
||||
"required": {
|
||||
"model": (list(MODEL_REGISTRY.keys()), {
|
||||
"default": "Florence-2: Base (0.23B params)",
|
||||
}),
|
||||
},
|
||||
"optional": {
|
||||
# GroundingDINO parameters
|
||||
**grounding_dino.params.LOADER_PARAMS.get("optional", {}),
|
||||
# Florence-2 parameters
|
||||
**florence2.params.LOADER_PARAMS.get("optional", {}),
|
||||
# YOLO-World parameters (none currently in loader)
|
||||
**yolo_world.params.LOADER_PARAMS.get("optional", {}),
|
||||
}
|
||||
"optional": optional_params
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("GROUNDING_MODEL",)
|
||||
@@ -69,12 +186,20 @@ class GroundingModelLoader:
|
||||
|
||||
# Route to appropriate loader
|
||||
if model_type == "grounding_dino":
|
||||
if grounding_dino is None:
|
||||
raise RuntimeError(f"Cannot load {model}: grounding_dino module failed to import. Check console for import errors.")
|
||||
model_dict = grounding_dino.load_grounding_dino(model, config)
|
||||
elif model_type == "owlv2":
|
||||
if owlv2 is None:
|
||||
raise RuntimeError(f"Cannot load {model}: owlv2 module failed to import. Check console for import errors.")
|
||||
model_dict = owlv2.load_owlv2(model, config)
|
||||
elif model_type == "florence2":
|
||||
if florence2 is None:
|
||||
raise RuntimeError(f"Cannot load {model}: florence2 module failed to import. Check console for import errors.")
|
||||
model_dict = florence2.load_florence2(model, config)
|
||||
elif model_type == "yolo_world":
|
||||
if yolo_world is None:
|
||||
raise RuntimeError(f"Cannot load {model}: yolo_world module failed to import. Check console for import errors.")
|
||||
model_dict = yolo_world.load_yolo_world(model, config)
|
||||
else:
|
||||
raise ValueError(f"Unknown model type: {model_type}")
|
||||
@@ -94,6 +219,35 @@ class GroundingDetector:
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
optional_params = {
|
||||
"single_box_mode": ("BOOLEAN", {
|
||||
"default": False,
|
||||
"tooltip": "Return only the highest-scoring detection. Use for referring expressions (e.g., 'the red car on the left')"
|
||||
}),
|
||||
"single_box_per_prompt_mode": ("BOOLEAN", {
|
||||
"default": False,
|
||||
"tooltip": "Return highest-scoring detection for each prompt/label (e.g., 'banana. orange' returns best banana and best orange). Ignored if single_box_mode is True"
|
||||
}),
|
||||
"bbox_output_format": (["list_only", "dict_with_data"], {
|
||||
"default": "list_only",
|
||||
"tooltip": "list_only: SAM2-compatible | dict_with_data: includes labels/scores"
|
||||
}),
|
||||
"seed": ("INT", {
|
||||
"default": 42,
|
||||
"min": 0,
|
||||
"max": 0xffffffff, # 2^32 - 1, max for numpy.random.seed
|
||||
"tooltip": "Fixed seed for reproducible results (affects mask visualization colors and model randomness)"
|
||||
}),
|
||||
}
|
||||
|
||||
# Only add model-specific params if modules loaded successfully
|
||||
if grounding_dino is not None:
|
||||
optional_params.update(grounding_dino.params.DETECTOR_PARAMS.get("optional", {}))
|
||||
if florence2 is not None:
|
||||
optional_params.update(florence2.params.DETECTOR_PARAMS.get("optional", {}))
|
||||
if yolo_world is not None:
|
||||
optional_params.update(yolo_world.params.DETECTOR_PARAMS.get("optional", {}))
|
||||
|
||||
return {
|
||||
"required": {
|
||||
"model": ("GROUNDING_MODEL",),
|
||||
@@ -111,32 +265,7 @@ class GroundingDetector:
|
||||
"tooltip": "Confidence threshold for detections. Typical: 0.2-0.35 (permissive), 0.35-0.5 (balanced), 0.5+ (strict)"
|
||||
}),
|
||||
},
|
||||
"optional": {
|
||||
"single_box_mode": ("BOOLEAN", {
|
||||
"default": False,
|
||||
"tooltip": "Return only the highest-scoring detection. Use for referring expressions (e.g., 'the red car on the left')"
|
||||
}),
|
||||
"single_box_per_prompt_mode": ("BOOLEAN", {
|
||||
"default": False,
|
||||
"tooltip": "Return highest-scoring detection for each prompt/label (e.g., 'banana. orange' returns best banana and best orange). Ignored if single_box_mode is True"
|
||||
}),
|
||||
"bbox_output_format": (["list_only", "dict_with_data"], {
|
||||
"default": "list_only",
|
||||
"tooltip": "list_only: SAM2-compatible | dict_with_data: includes labels/scores"
|
||||
}),
|
||||
# GroundingDINO parameters
|
||||
**grounding_dino.params.DETECTOR_PARAMS.get("optional", {}),
|
||||
# Florence-2 parameters
|
||||
**florence2.params.DETECTOR_PARAMS.get("optional", {}),
|
||||
# YOLO-World parameters
|
||||
**yolo_world.params.DETECTOR_PARAMS.get("optional", {}),
|
||||
"seed": ("INT", {
|
||||
"default": 42,
|
||||
"min": 0,
|
||||
"max": 0xffffffff, # 2^32 - 1, max for numpy.random.seed
|
||||
"tooltip": "Fixed seed for reproducible results (affects mask visualization colors and model randomness)"
|
||||
}),
|
||||
}
|
||||
"optional": optional_params
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("BBOX", "IMAGE", "STRING", "MASK")
|
||||
@@ -167,22 +296,30 @@ class GroundingDetector:
|
||||
|
||||
# Route to appropriate detection method
|
||||
if model_type == "grounding_dino":
|
||||
if grounding_dino is None:
|
||||
raise RuntimeError(f"Cannot detect with {model_type}: grounding_dino module failed to import. Check console for import errors.")
|
||||
return grounding_dino.detect_grounding_dino(
|
||||
model, image, prompt, confidence_threshold, text_threshold,
|
||||
single_box_mode, single_box_per_prompt_mode, bbox_output_format, output_masks, self._format_output
|
||||
)
|
||||
elif model_type == "owlv2":
|
||||
if owlv2 is None:
|
||||
raise RuntimeError(f"Cannot detect with {model_type}: owlv2 module failed to import. Check console for import errors.")
|
||||
return owlv2.detect_owlv2(
|
||||
model, image, prompt, confidence_threshold,
|
||||
single_box_mode, single_box_per_prompt_mode, bbox_output_format, output_masks, self._format_output
|
||||
)
|
||||
elif model_type == "florence2":
|
||||
if florence2 is None:
|
||||
raise RuntimeError(f"Cannot detect with {model_type}: florence2 module failed to import. Check console for import errors.")
|
||||
return florence2.detect_florence2(
|
||||
model, image, prompt, confidence_threshold, single_box_mode,
|
||||
single_box_per_prompt_mode, bbox_output_format, output_masks, florence2_max_tokens,
|
||||
florence2_num_beams, self._format_output
|
||||
)
|
||||
elif model_type == "yolo_world":
|
||||
if yolo_world is None:
|
||||
raise RuntimeError(f"Cannot detect with {model_type}: yolo_world module failed to import. Check console for import errors.")
|
||||
return yolo_world.detect_yolo_world(
|
||||
model, image, prompt, confidence_threshold, single_box_mode,
|
||||
single_box_per_prompt_mode, bbox_output_format, output_masks, yolo_iou, yolo_agnostic_nms,
|
||||
@@ -237,18 +374,20 @@ class GroundingMaskModelLoader:
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
optional_params = {}
|
||||
# Only add model-specific params if modules loaded successfully
|
||||
if florence2_seg is not None:
|
||||
optional_params.update(florence2_seg.params.LOADER_PARAMS.get("optional", {}))
|
||||
if sa2va is not None:
|
||||
optional_params.update(sa2va.params.LOADER_PARAMS.get("optional", {}))
|
||||
|
||||
return {
|
||||
"required": {
|
||||
"model": (list(MASK_MODEL_REGISTRY.keys()), {
|
||||
"default": "Florence-2: Base (Segmentation)",
|
||||
}),
|
||||
},
|
||||
"optional": {
|
||||
# Florence-2 Seg parameters
|
||||
**florence2_seg.params.LOADER_PARAMS.get("optional", {}),
|
||||
# SA2VA parameters
|
||||
**sa2va.params.LOADER_PARAMS.get("optional", {}),
|
||||
}
|
||||
"optional": optional_params
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("MASK_MODEL",)
|
||||
@@ -271,8 +410,12 @@ class GroundingMaskModelLoader:
|
||||
|
||||
# Route to appropriate loader
|
||||
if model_type == "florence2_seg":
|
||||
if florence2_seg is None:
|
||||
raise RuntimeError(f"Cannot load {model}: florence2_seg module failed to import. Check console for import errors.")
|
||||
model_dict = florence2_seg.load_florence2_seg(model, config)
|
||||
elif model_type == "sa2va":
|
||||
if sa2va is None:
|
||||
raise RuntimeError(f"Cannot load {model}: sa2va module failed to import. Check console for import errors.")
|
||||
model_dict = sa2va.load_sa2va(model, config, sa2va_dtype)
|
||||
elif model_type == "lisa":
|
||||
raise NotImplementedError("LISA support coming soon")
|
||||
@@ -375,12 +518,16 @@ class GroundingMaskDetector:
|
||||
|
||||
# Route to appropriate detection method
|
||||
if model_type == "florence2_seg":
|
||||
if florence2_seg is None:
|
||||
raise RuntimeError(f"Cannot detect with {model_type}: florence2_seg module failed to import. Check console for import errors.")
|
||||
return florence2_seg.detect_florence2_seg(
|
||||
model, image, prompt, confidence_threshold,
|
||||
florence2_max_tokens, florence2_num_beams,
|
||||
self._format_output
|
||||
)
|
||||
elif model_type == "sa2va":
|
||||
if sa2va is None:
|
||||
raise RuntimeError(f"Cannot detect with {model_type}: sa2va module failed to import. Check console for import errors.")
|
||||
return sa2va.detect_sa2va(
|
||||
model, image, prompt, confidence_threshold,
|
||||
sa2va_max_tokens, sa2va_num_beams, self._format_output
|
||||
@@ -452,15 +599,27 @@ class GroundingMaskDetector:
|
||||
class DownLoadSAM2Model:
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
if sam2 is None:
|
||||
# Return minimal params if sam2 failed to import
|
||||
return {
|
||||
"required": {
|
||||
"model": (["sam2_model"], {}),
|
||||
"segmentor": (["auto"], {}),
|
||||
"device": (["auto"], {}),
|
||||
"precision": (["auto"], {}),
|
||||
}
|
||||
}
|
||||
return sam2.params.LOADER_PARAMS
|
||||
|
||||
RETURN_TYPES = sam2.params.LOADER_RETURN_TYPES
|
||||
RETURN_NAMES = sam2.params.LOADER_RETURN_NAMES
|
||||
RETURN_TYPES = ("SAM2_MODEL",) if sam2 is None else sam2.params.LOADER_RETURN_TYPES
|
||||
RETURN_NAMES = ("model",) if sam2 is None else sam2.params.LOADER_RETURN_NAMES
|
||||
FUNCTION = "loadmodel"
|
||||
CATEGORY = "SAM2"
|
||||
|
||||
def loadmodel(self, model, segmentor, device, precision):
|
||||
"""Load SAM2 model"""
|
||||
if sam2 is None:
|
||||
raise RuntimeError("Cannot load SAM2 model: sam2 module failed to import. Check console for import errors.")
|
||||
sam2_model = sam2.load_sam2(model, segmentor, device, precision, script_directory)
|
||||
return (sam2_model,)
|
||||
|
||||
@@ -471,10 +630,19 @@ class Sam2Segment:
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
if sam2 is None:
|
||||
# Return minimal params if sam2 failed to import
|
||||
return {
|
||||
"required": {
|
||||
"image": ("IMAGE",),
|
||||
"sam2_model": ("SAM2_MODEL",),
|
||||
"keep_model_loaded": ("BOOLEAN", {"default": True}),
|
||||
}
|
||||
}
|
||||
return sam2.params.SEGMENTATION_PARAMS
|
||||
|
||||
RETURN_TYPES = sam2.params.SEGMENTATION_RETURN_TYPES
|
||||
RETURN_NAMES = sam2.params.SEGMENTATION_RETURN_NAMES
|
||||
RETURN_TYPES = ("MASK",) if sam2 is None else sam2.params.SEGMENTATION_RETURN_TYPES
|
||||
RETURN_NAMES = ("masks",) if sam2 is None else sam2.params.SEGMENTATION_RETURN_NAMES
|
||||
FUNCTION = "segment"
|
||||
CATEGORY = "SAM2"
|
||||
|
||||
@@ -482,6 +650,9 @@ class Sam2Segment:
|
||||
coordinates_negative=None, individual_objects=False, bboxes=None,
|
||||
mask=None, mask_threshold=0.0, max_hole_area=0.0, max_sprinkle_area=0.0):
|
||||
"""Perform SAM2 segmentation"""
|
||||
if sam2 is None:
|
||||
raise RuntimeError("Cannot perform SAM2 segmentation: sam2 module failed to import. Check console for import errors.")
|
||||
|
||||
# Create a reference dict to hold inference_state
|
||||
inference_state_ref = {'state': self.inference_state}
|
||||
|
||||
@@ -508,15 +679,26 @@ class BboxVisualizer:
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
if visualizer is None:
|
||||
# Return minimal params if visualizer failed to import
|
||||
return {
|
||||
"required": {
|
||||
"image": ("IMAGE",),
|
||||
"bboxes": ("BBOX",),
|
||||
"line_width": ("INT", {"default": 3, "min": 1, "max": 20}),
|
||||
}
|
||||
}
|
||||
return visualizer.params.VISUALIZER_PARAMS
|
||||
|
||||
RETURN_TYPES = visualizer.params.RETURN_TYPES
|
||||
RETURN_NAMES = visualizer.params.RETURN_NAMES
|
||||
RETURN_TYPES = ("IMAGE",) if visualizer is None else visualizer.params.RETURN_TYPES
|
||||
RETURN_NAMES = ("image",) if visualizer is None else visualizer.params.RETURN_NAMES
|
||||
FUNCTION = "visualize"
|
||||
CATEGORY = "grounding"
|
||||
|
||||
def visualize(self, image, bboxes, line_width=3):
|
||||
"""Draw bounding boxes on image (supports batches)"""
|
||||
if visualizer is None:
|
||||
raise RuntimeError("Cannot visualize bboxes: visualizer module failed to import. Check console for import errors.")
|
||||
return visualizer.visualize_bboxes(image, bboxes, line_width)
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,164 @@
|
||||
"""
|
||||
Smoke test for ComfyUI-Grounding
|
||||
Tests basic module import functionality without requiring ComfyUI to be installed
|
||||
"""
|
||||
|
||||
import sys
|
||||
import traceback
|
||||
|
||||
def test_basic_imports():
|
||||
"""Test that basic Python dependencies can be imported"""
|
||||
print("Testing basic dependencies...")
|
||||
try:
|
||||
import torch
|
||||
print(f" ✓ torch {torch.__version__}")
|
||||
except ImportError as e:
|
||||
print(f" ✗ torch: {e}")
|
||||
return False
|
||||
|
||||
try:
|
||||
import numpy as np
|
||||
print(f" ✓ numpy {np.__version__}")
|
||||
except ImportError as e:
|
||||
print(f" ✗ numpy: {e}")
|
||||
return False
|
||||
|
||||
try:
|
||||
from PIL import Image
|
||||
print(f" ✓ PIL")
|
||||
except ImportError as e:
|
||||
print(f" ✗ PIL: {e}")
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def test_config_imports():
|
||||
"""Test that config modules can be imported"""
|
||||
print("\nTesting config imports...")
|
||||
try:
|
||||
from nodes.config import MODEL_REGISTRY, MASK_MODEL_REGISTRY
|
||||
print(f" ✓ Config imported ({len(MODEL_REGISTRY)} bbox models, {len(MASK_MODEL_REGISTRY)} mask models)")
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f" ✗ Config import failed: {e}")
|
||||
traceback.print_exc()
|
||||
return False
|
||||
|
||||
def test_utils_imports():
|
||||
"""Test that utility modules can be imported"""
|
||||
print("\nTesting utils imports...")
|
||||
try:
|
||||
from nodes.utils.cache import MODEL_CACHE
|
||||
print(f" ✓ Cache imported")
|
||||
except Exception as e:
|
||||
print(f" ✗ Cache import failed: {e}")
|
||||
traceback.print_exc()
|
||||
return False
|
||||
|
||||
try:
|
||||
from nodes.utils import draw_boxes, boxes_to_masks
|
||||
print(f" ✓ Utils imported")
|
||||
except Exception as e:
|
||||
print(f" ✗ Utils import failed: {e}")
|
||||
traceback.print_exc()
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def test_model_module_imports():
|
||||
"""Test that model modules can be imported"""
|
||||
print("\nTesting model module imports...")
|
||||
modules = ['grounding_dino', 'owlv2', 'florence2', 'yolo_world',
|
||||
'florence2_seg', 'sa2va', 'sam2', 'visualizer']
|
||||
|
||||
failed = []
|
||||
for module_name in modules:
|
||||
try:
|
||||
module = __import__(f'nodes.{module_name}', fromlist=[''])
|
||||
print(f" ✓ {module_name}")
|
||||
except Exception as e:
|
||||
print(f" ✗ {module_name}: {e}")
|
||||
failed.append(module_name)
|
||||
|
||||
return len(failed) == 0
|
||||
|
||||
def test_node_class_definitions():
|
||||
"""Test that node classes are defined"""
|
||||
print("\nTesting node class definitions...")
|
||||
try:
|
||||
from nodes import (
|
||||
GroundingModelLoader,
|
||||
GroundingDetector,
|
||||
BboxVisualizer,
|
||||
DownLoadSAM2Model,
|
||||
Sam2Segment,
|
||||
GroundingMaskModelLoader,
|
||||
GroundingMaskDetector,
|
||||
BatchCropAndPadFromMask,
|
||||
)
|
||||
|
||||
# Check they're not None
|
||||
classes = {
|
||||
'GroundingModelLoader': GroundingModelLoader,
|
||||
'GroundingDetector': GroundingDetector,
|
||||
'BboxVisualizer': BboxVisualizer,
|
||||
'DownLoadSAM2Model': DownLoadSAM2Model,
|
||||
'Sam2Segment': Sam2Segment,
|
||||
'GroundingMaskModelLoader': GroundingMaskModelLoader,
|
||||
'GroundingMaskDetector': GroundingMaskDetector,
|
||||
'BatchCropAndPadFromMask': BatchCropAndPadFromMask,
|
||||
}
|
||||
|
||||
failed = []
|
||||
for name, cls in classes.items():
|
||||
if cls is None:
|
||||
print(f" ✗ {name} is None!")
|
||||
failed.append(name)
|
||||
else:
|
||||
print(f" ✓ {name}")
|
||||
|
||||
return len(failed) == 0
|
||||
|
||||
except Exception as e:
|
||||
print(f" ✗ Node import failed: {e}")
|
||||
traceback.print_exc()
|
||||
return False
|
||||
|
||||
def main():
|
||||
"""Run all smoke tests"""
|
||||
print("="*60)
|
||||
print("ComfyUI-Grounding Smoke Test")
|
||||
print("="*60)
|
||||
|
||||
results = []
|
||||
|
||||
# Run tests
|
||||
results.append(("Basic imports", test_basic_imports()))
|
||||
results.append(("Config imports", test_config_imports()))
|
||||
results.append(("Utils imports", test_utils_imports()))
|
||||
results.append(("Model modules", test_model_module_imports()))
|
||||
results.append(("Node classes", test_node_class_definitions()))
|
||||
|
||||
# Summary
|
||||
print("\n" + "="*60)
|
||||
print("SUMMARY")
|
||||
print("="*60)
|
||||
|
||||
all_passed = True
|
||||
for name, passed in results:
|
||||
status = "✓ PASS" if passed else "✗ FAIL"
|
||||
print(f"{status}: {name}")
|
||||
if not passed:
|
||||
all_passed = False
|
||||
|
||||
print("="*60)
|
||||
|
||||
if all_passed:
|
||||
print("✓ All smoke tests passed!")
|
||||
sys.exit(0)
|
||||
else:
|
||||
print("✗ Some smoke tests failed!")
|
||||
sys.exit(1)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user