- Add SelectActorPreview node to load per-actor preview images as ComfyUI IMAGE tensors
- Fix SelectActorPreview return value: must return (tensor,) tuple, not bare tensor,
to prevent ComfyUI's merge_result_data from misinterpreting batch dimension as
output count (root cause of 1920-image bug)
- Change VideoActorExtractor third output from IMAGE tensor to INT actor_count
- Save top-5 preview JPGs per actor to {output_dir}/previews/ instead of
building a large preview tensor in memory
- Unify InsightFace model path to ComfyUI/models/video-actor-extract/buffalo_l/
- Add temporal gap penalty with grace gap to identity clustering
- Add temporal boost with Gaussian decay and spatial blocker check
- Add dual-signal track splitting (centroid displacement + bbox area ratio)
- Add spatial centroid proximity tiebreaker for identity merging
- Add debris mask filtering (min_mask_area=20000) in MaskTracker
- Clean up unused imports across modified files
- Add AGENTS.md project guidelines and test infrastructure
7.3 KiB
AGENTS.md — ComfyUI-VideoActorExtract
Guidelines for agentic coding agents working in this repository.
Project Overview
ComfyUI custom node plugin that detects, tracks, and extracts individual actors from video, outputting per-actor green-screen videos and structured JSON metadata.
- Language: Python 3.10+ (backend), JavaScript (ComfyUI widget)
- Framework: ComfyUI custom node plugin
- Runtime: Depends on ComfyUI host process; not a standalone application
Repository Structure
__init__.py # Plugin entry — dynamic importlib loading to avoid ComfyUI namespace conflicts
core/
config.py # Global constants (thresholds, defaults, codec settings)
video_reader.py # Video info extraction and frame sampling
nodes/
actor_extractor.py # Main ComfyUI node class (VideoActorExtractor) — pipeline orchestration
pipeline/
detector.py # YOLOv8 person detection → BoundingBox list
segmenter.py # YOLOv8-seg person segmentation → mask per person
mask_tracker.py # Centroid-distance mask tracking across frames
tracker.py # ByteTrack multi-object tracking (optional, fallback to greedy)
identity.py # InsightFace face embedding clustering for same-actor merging
cropper.py # Green-screen compositing per detected person
merger.py # FFmpeg encoding + JSON metadata generation
js/
widget.js # ComfyUI frontend extension (JSON display button)
Build & Install
# Install dependencies
pip install -r requirements.txt
# Install in dev mode (optional)
pip install -e ".[dev]"
No build step required — this is a pure-Python ComfyUI plugin loaded at runtime.
Linting
# Lint with ruff (uses default rules — no ruff.toml/config exists)
ruff check .
# Auto-fix where possible
ruff check --fix .
No formatter is configured. Keep existing style when making changes.
Testing
# Run all tests
pytest
# Run a single test file
pytest tests/test_foo.py
# Run a specific test by name
pytest tests/test_foo.py::test_specific_function -v
Note
: No test files exist yet.
pytestis a dev dependency inpyproject.toml. Integration testing is done manually via ComfyUI's HTTP API (see README.md lines 283–367). When adding tests, create atests/directory at the project root.
Code Style Guidelines
Naming Conventions (PEP 8)
| Element | Style | Example |
|---|---|---|
| Functions / methods | snake_case |
_build_continuous_segments, detect_masks |
| Variables | snake_case |
actor_frames, mask_resized |
| Classes | PascalCase |
VideoActorExtractor, MaskTracker |
| Constants | UPPER_SNAKE_CASE |
DEFAULT_MAX_ACTORS, GREEN_SCREEN_COLOR |
| Private members | _leading_underscore |
_ensure_loaded, _compute_centroid |
| ComfyUI node attributes | UPPER_SNAKE_CASE |
INPUT_TYPES, RETURN_TYPES, FUNCTION |
Import Order
Group imports in this order, separated by blank lines:
# 1. Standard library
import os
import sys
import json
# 2. Third-party
import torch
import numpy as np
from typing import List, Tuple, Optional
# 3. Local project (absolute paths from project root)
from core.config import DEFAULT_MAX_ACTORS
from pipeline.detector import PersonDetector
Lazy imports for heavy dependencies — load YOLO/InsightFace inside methods on first use, not at module top level. This keeps plugin import time fast:
def _ensure_loaded(self):
"""Lazy-load InsightFace model."""
if self._loaded:
return
import insightface
...
Type Annotations
- Use
typing.List,typing.Dict,typing.Tuple,typing.Optional(Python 3.10 compat). - Annotate public function signatures; private helpers may omit annotations for brevity.
- Use
@dataclassfor data containers (e.g.,FrameRecord,VideoInfo).
def detect(self, frame: np.ndarray) -> List[BoundingBox]:
"""Detect all persons in a single frame."""
...
Docstrings (Google style)
def _build_continuous_segments(
actor_frames: List[Tuple[int, np.ndarray, int]],
max_gap: int = 30,
) -> Tuple[List[List[np.ndarray]], List[dict]]:
"""
Group frames into continuous segments with gap interpolation.
Args:
actor_frames: List of (frame_idx, frame_array, mask_id) tuples.
max_gap: Maximum gap in frames before starting a new segment.
Returns:
Tuple of (segmented_frame_lists, segment_metadata).
"""
- Module-level docstrings: single-line summary.
- Class docstrings: one-line description of purpose.
- Public methods:
Args/Returnssections. - Skip docstrings for trivial private helpers.
Logging
Use print() with a bracketed prefix — no logging module:
print("[VideoActorExtract] Step 1: Initializing person segmenter...")
print("[MaskTracker] Lost track for actor_id=3 at frame 142")
Module prefix convention: [VideoActorExtract], [PersonSegmenter], [MaskTracker], [Identity].
Error Handling
- Graceful degradation: wrap optional heavy imports in
try/except ImportError, provide fallback behavior (e.g., greedy tracker when ByteTrack is unavailable). - No hard crashes in pipeline: return empty/error JSON rather than raising from the
main
extract()method. - ValueError for invalid user inputs (bad video path, missing frames).
- Silent
except Exception: passis acceptable only for truly optional features (e.g., proxy setup).
Data Containers
Prefer @dataclass for structured data; use __slots__ for performance-critical containers:
@dataclass
class FrameRecord:
frame_idx: int
x1: float
y1: float
x2: float
y2: float
class BoundingBox:
__slots__ = ['x1', 'y1', 'x2', 'y2', 'confidence']
ComfyUI Node Pattern
All nodes must expose these class attributes:
class MyNode:
@classmethod
def INPUT_TYPES(cls):
return {"required": {...}, "optional": {...}}
RETURN_TYPES = ("STRING", "STRING", "IMAGE")
RETURN_NAMES = ("result_json", "output_dir", "preview")
FUNCTION = "process" # method name for ComfyUI to call
CATEGORY = "video/actor"
OUTPUT_NODE = True # optional
JavaScript (ComfyUI Widget)
Minimal — single file in js/. Register via:
app.registerExtension({
name: "VideoActorExtract.widget",
// ...
});
Commit Messages
Conventional commits, lowercase prefix:
feat: add person segmentation pipeline
fix: resolve namespace conflict with ComfyUI nodes module
docs: comprehensive README with architecture guide
improve: identity clustering with connected components
chore: add .gitignore
Key Constraints
- No standalone execution: This plugin only runs inside ComfyUI. There is no CLI entry point.
__init__.pyuses importlib: All modules are loaded viaimportlib.utilwith prefixed names (vae_*) to avoid namespace collisions with ComfyUI's ownnodesmodule. Do not switch to regular imports in__init__.py.- Model weights (
*.pt) are gitignored. Models are loaded fromComfyUI/models/video-actor-extract/. - Apple Silicon:
onnxruntime-siliconis used on ARM64 macOS (see requirements.txt conditional). - Memory-sensitive: Process frames one at a time; avoid accumulating full-video tensors in memory.