wip(corridorkey_local): operator scaffold — BLOCKED on GVM inference crash

Full staging/zip/cleanup contract works; generate-alphas dies in GVM with
'MPSNDArray buffer is not large enough. Must be 59768832 bytes' on BOTH
m1ultra and m3ultra, any input dims, torch or mlx backend, gpu or cpu post.
GVM --device cpu wrote 0 frames in 3.5h (hung). Weights installed both nodes
(gvm_core/weights, 6GB). Next: either debug GVM's torch-MPS path or replace
the alpha-hint stage with the corridorkey-mlx fork / bg_remove_local hints.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
type-two 2026-07-18 22:30:39 +10:00
parent 4bc2f56d34
commit d5217575eb
2 changed files with 138 additions and 0 deletions

View File

@ -0,0 +1,23 @@
{
"id": "corridorkey_local",
"name": "CorridorKey Green-Screen Unmix (local)",
"category": "video-prep",
"description": "Green/blue-screen clip or frame → true un-multiplied foreground color + linear alpha via Corridor's neural keyer (the corridorkey-mrp-mlx fork, MLX on Apple silicon). Preserves hair, motion blur and translucency — no binary roto masks. Input: a video shot on green/blue, or a single frame — NOT square (GVM's resize rejects smaller-edge ≥1024-after-scale square plates; 16:9/9:16 is fine). Output: zipped frame sequence (straight color + alpha) plus a comp preview when enabled. Free, fully local.",
"accepts": ["video", "image"],
"produces": ["archive"],
"resources": "gpu",
"entry": "run.py",
"python": "vendor/corridorkey/.venv/bin/python",
"params_schema": {
"type": "object",
"properties": {
"screen_color": {"type": "string", "enum": ["auto", "green", "blue"], "default": "auto", "description": "Screen color (blue = torch backend only for now)"},
"backend": {"type": "string", "enum": ["auto", "mlx", "torch"], "default": "auto", "description": "Inference backend"},
"despill": {"type": "integer", "minimum": 0, "maximum": 10, "default": 5, "description": "Despill strength"},
"refiner": {"type": "number", "default": 1.0, "description": "Refiner strength multiplier"},
"comp": {"type": "boolean", "default": true, "description": "Also render a comp preview"},
"max_frames": {"type": "integer", "description": "Limit frames (quick tests)"},
"image_size": {"type": "integer", "description": "Inference size override (default: model native)"}
}
}
}

View File

@ -0,0 +1,115 @@
"""corridorkey_local — Corridor's neural green-screen unmixer, headless.
Shells out to the vendored CLI (vendor/corridorkey, the corridorkey-mrp-mlx fork):
stage the input as a clip generate-alphas (GVM coarse hint) run-inference with
every flag set (non-interactive) zip Output/<clip> as the result.
Clip staging uses the job's outdir basename as the clip name, so concurrent jobs
can't collide; the shared ClipsForInference/Output dirs are cleaned afterwards
(win or lose) so the vendor tree doesn't accumulate gigabytes of frames.
"""
import argparse
import json
import os
import shutil
import subprocess
import sys
import zipfile
from pathlib import Path
ap = argparse.ArgumentParser()
ap.add_argument("--input", action="append", default=[])
ap.add_argument("--outdir", required=True)
ap.add_argument("--params", default="{}")
a = ap.parse_args()
p = json.loads(a.params)
if not a.input:
print("ERROR: no input clip/frame")
sys.exit(1)
VENDOR = Path(__file__).resolve().parents[3] / "vendor" / "corridorkey"
CLI = VENDOR / ".venv" / "bin" / "corridorkey"
if not CLI.exists():
print(f"ERROR: {CLI} missing — run the corridorkey install script on this node")
sys.exit(1)
src = Path(a.input[0])
outdir = Path(a.outdir)
clip_name = "ck_" + outdir.name # unique per job
clip_dir = VENDOR / "ClipsForInference" / clip_name
out_clip = VENDOR / "Output" / clip_name
VIDEO_EXTS = {".mp4", ".mov", ".mkv", ".avi", ".webm", ".mxf", ".m4v"}
def run(cmd):
print("+", " ".join(str(c) for c in cmd), flush=True)
res = subprocess.run(cmd, cwd=str(VENDOR), stdout=sys.stdout, stderr=subprocess.STDOUT)
if res.returncode != 0:
cleanup()
sys.exit(res.returncode)
def cleanup():
shutil.rmtree(clip_dir, ignore_errors=True)
shutil.rmtree(out_clip, ignore_errors=True)
try:
clip_dir.mkdir(parents=True, exist_ok=True)
if src.suffix.lower() in VIDEO_EXTS:
shutil.copy(src, clip_dir / f"Input{src.suffix.lower()}")
else: # single frame → 1-frame sequence
(clip_dir / "Input").mkdir(exist_ok=True)
shutil.copy(src, clip_dir / "Input" / src.name)
run([CLI, "generate-alphas"])
hint = clip_dir / "AlphaHint"
if not hint.is_dir() or not any(hint.iterdir()): # generate-alphas exits 0 even when GVM fails
print("ERROR: no alpha hints generated — are the GVM weights installed? "
"(vendor/corridorkey: uv run hf download geyongtao/gvm --local-dir gvm_core/weights)")
cleanup()
sys.exit(1)
cmd = [CLI, "run-inference",
"--backend", str(p.get("backend", "auto")),
"--srgb", # camera clips; EXR pipelines can re-run --linear
"--despill", str(int(p.get("despill", 5))),
"--despeckle",
"--refiner", str(float(p.get("refiner", 1.0))),
"--screen-color", str(p.get("screen_color", "auto")),
"--comp" if p.get("comp", True) else "--no-comp",
# cpu-post default: gpu post-processing dies with an MPSNDArray buffer assertion
# on both backends (mac, 2026-07). Opt back in with {"gpu_post": true} to retest.
"--gpu-post" if p.get("gpu_post") else "--cpu-post",
"--skip-existing"]
if p.get("max_frames"):
cmd += ["--max-frames", str(int(p["max_frames"]))]
if p.get("image_size"):
cmd += ["--image-size", str(int(p["image_size"]))]
run(cmd)
if not out_clip.is_dir() or not any(out_clip.rglob("*")):
print("ERROR: inference produced no output")
cleanup()
sys.exit(1)
zpath = outdir / f"{src.stem}_corridorkey.zip"
outputs = []
with zipfile.ZipFile(zpath, "w", zipfile.ZIP_STORED) as z: # frames are already compressed
for f in sorted(out_clip.rglob("*")):
if f.is_file():
z.write(f, f.relative_to(out_clip))
outputs.append({"path": zpath.name, "meta": {"tool": "corridorkey-mrp-mlx"}})
previews = sorted(out_clip.rglob("*omp*.mp4")) or sorted(out_clip.rglob("*.mp4"))
if previews: # comp preview as its own asset
prev = outdir / f"{src.stem}_comp{previews[0].suffix}"
shutil.copy(previews[0], prev)
outputs.append({"path": prev.name, "meta": {"tool": "corridorkey-comp"}})
(outdir / "result.json").write_text(json.dumps({"outputs": outputs}))
print(f"done: {', '.join(o['path'] for o in outputs)}", flush=True)
finally:
cleanup()