Adds matanyone2_extended_demo.json with two parallel paths: - Interactive path: editor-based masking (same as basic demo) - Scripted path: SAMLoader, PromptStart, AddPoint, PromptFromText, SAMRefine, MergeMasks, PreviewMasks for programmatic workflows Updates install.py to copy the extended demo into ComfyUI workflows on install, and updates README with documentation for both demos. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
173 lines
5.6 KiB
Python
173 lines
5.6 KiB
Python
from __future__ import annotations
|
|
|
|
import hashlib
|
|
import shutil
|
|
from pathlib import Path
|
|
|
|
|
|
ROOT = Path(__file__).resolve().parent
|
|
VENDOR_ROOT = ROOT / "vendor"
|
|
COMMIT_MARKER = ".vendor_commit"
|
|
DEMO_VIDEO_NAME = "matanyone2-demo-input.mp4"
|
|
DEMO_VIDEO_SOURCE = ROOT / "assets" / DEMO_VIDEO_NAME
|
|
DEMO_WORKFLOW_SOURCE = ROOT / "workflows" / "matanyone2_demo.json"
|
|
DEMO_WORKFLOW_NAMES = (
|
|
"matanyone2_demo.json",
|
|
"matanyone2_full_demo.json",
|
|
"matanyone2_vhs_demo.json",
|
|
)
|
|
EXTENDED_DEMO_SOURCE = ROOT / "workflows" / "matanyone2_extended_demo.json"
|
|
EXTENDED_DEMO_NAME = "matanyone2_extended_demo.json"
|
|
|
|
VENDORS = (
|
|
{
|
|
"name": "MatAnyone2",
|
|
"path": ROOT / "vendor" / "MatAnyone2",
|
|
"commit": "4aed00afd91e108d8986b062b968678fd1c4f8f7",
|
|
"required_paths": (
|
|
"matanyone2/config/eval_matanyone_config.yaml",
|
|
"matanyone2/model/matanyone2.py",
|
|
),
|
|
},
|
|
{
|
|
"name": "segment-anything",
|
|
"path": ROOT / "vendor" / "segment-anything",
|
|
"commit": "dca509fe793f601edb92606367a655c15ac00fdf",
|
|
"required_paths": (
|
|
"segment_anything/build_sam.py",
|
|
"segment_anything/predictor.py",
|
|
),
|
|
},
|
|
)
|
|
|
|
|
|
def _read_marker(repo_path: Path) -> str | None:
|
|
marker = repo_path / COMMIT_MARKER
|
|
if not marker.exists():
|
|
return None
|
|
return marker.read_text(encoding="utf-8").strip() or None
|
|
|
|
|
|
def _detect_input_directory() -> Path | None:
|
|
try:
|
|
import folder_paths # type: ignore
|
|
|
|
return Path(folder_paths.get_input_directory()).resolve()
|
|
except Exception:
|
|
pass
|
|
|
|
for parent in ROOT.parents:
|
|
if parent.name == "custom_nodes":
|
|
return (parent.parent / "input").resolve()
|
|
return None
|
|
|
|
|
|
def _detect_workflow_directory() -> Path | None:
|
|
try:
|
|
import folder_paths # type: ignore
|
|
|
|
return (Path(folder_paths.get_user_directory()).resolve() / "default" / "workflows")
|
|
except Exception:
|
|
pass
|
|
|
|
for parent in ROOT.parents:
|
|
if parent.name == "custom_nodes":
|
|
return (parent.parent / "user" / "default" / "workflows").resolve()
|
|
return None
|
|
|
|
|
|
def _file_sha256(path: Path) -> str:
|
|
digest = hashlib.sha256()
|
|
with path.open("rb") as file:
|
|
for chunk in iter(lambda: file.read(1024 * 1024), b""):
|
|
digest.update(chunk)
|
|
return digest.hexdigest()
|
|
|
|
|
|
def ensure_demo_input_video() -> None:
|
|
if not DEMO_VIDEO_SOURCE.exists():
|
|
print(f"Bundled demo clip missing at {DEMO_VIDEO_SOURCE}, skipping input copy.")
|
|
return
|
|
|
|
input_dir = _detect_input_directory()
|
|
if input_dir is None:
|
|
print("Could not locate ComfyUI input directory, skipping demo clip copy.")
|
|
return
|
|
|
|
input_dir.mkdir(parents=True, exist_ok=True)
|
|
destination = input_dir / DEMO_VIDEO_NAME
|
|
if destination.exists():
|
|
if _file_sha256(destination) == _file_sha256(DEMO_VIDEO_SOURCE):
|
|
print(f"Using bundled demo clip at {destination}")
|
|
return
|
|
print(f"Refreshing bundled demo clip at {destination}")
|
|
else:
|
|
print(f"Copying bundled demo clip to {destination}")
|
|
|
|
shutil.copy2(DEMO_VIDEO_SOURCE, destination)
|
|
|
|
|
|
def ensure_demo_workflows() -> None:
|
|
if not DEMO_WORKFLOW_SOURCE.exists():
|
|
print(f"Bundled demo workflow missing at {DEMO_WORKFLOW_SOURCE}, skipping workflow sync.")
|
|
return
|
|
|
|
workflow_dir = _detect_workflow_directory()
|
|
if workflow_dir is None:
|
|
print("Could not locate ComfyUI workflow directory, skipping demo workflow sync.")
|
|
return
|
|
|
|
workflow_dir.mkdir(parents=True, exist_ok=True)
|
|
source_hash = _file_sha256(DEMO_WORKFLOW_SOURCE)
|
|
|
|
for workflow_name in DEMO_WORKFLOW_NAMES:
|
|
destination = workflow_dir / workflow_name
|
|
if destination.exists():
|
|
if _file_sha256(destination) == source_hash:
|
|
print(f"Using bundled demo workflow at {destination}")
|
|
continue
|
|
print(f"Refreshing bundled demo workflow at {destination}")
|
|
else:
|
|
print(f"Copying bundled demo workflow to {destination}")
|
|
shutil.copy2(DEMO_WORKFLOW_SOURCE, destination)
|
|
|
|
if EXTENDED_DEMO_SOURCE.exists():
|
|
ext_dest = workflow_dir / EXTENDED_DEMO_NAME
|
|
ext_hash = _file_sha256(EXTENDED_DEMO_SOURCE)
|
|
if ext_dest.exists() and _file_sha256(ext_dest) == ext_hash:
|
|
print(f"Using bundled demo workflow at {ext_dest}")
|
|
else:
|
|
action = "Refreshing" if ext_dest.exists() else "Copying"
|
|
print(f"{action} bundled demo workflow to {ext_dest}")
|
|
shutil.copy2(EXTENDED_DEMO_SOURCE, ext_dest)
|
|
|
|
|
|
def ensure_vendor_bundle(vendor: dict[str, str | Path | tuple[str, ...]]) -> None:
|
|
repo_path = Path(vendor["path"])
|
|
commit = str(vendor["commit"])
|
|
required_paths = tuple(str(path) for path in vendor["required_paths"])
|
|
|
|
missing_paths = [str(repo_path / relative_path) for relative_path in required_paths if not (repo_path / relative_path).exists()]
|
|
if missing_paths:
|
|
missing = ", ".join(missing_paths)
|
|
raise RuntimeError(
|
|
f"Bundled {vendor['name']} sources are incomplete. Missing: {missing}. "
|
|
"Reinstall the node package from the registry or GitHub."
|
|
)
|
|
|
|
marker = _read_marker(repo_path)
|
|
if marker != commit:
|
|
raise RuntimeError(
|
|
f"Bundled {vendor['name']} sources do not match the pinned commit. "
|
|
f"Expected {commit}, found {marker or 'missing marker'}."
|
|
)
|
|
|
|
print(f"Using bundled {vendor['name']} sources at {repo_path}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
for vendor in VENDORS:
|
|
ensure_vendor_bundle(vendor)
|
|
ensure_demo_input_video()
|
|
ensure_demo_workflows()
|