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:
parent
4bc2f56d34
commit
d5217575eb
23
server/operators/corridorkey_local/manifest.json
Normal file
23
server/operators/corridorkey_local/manifest.json
Normal 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)"}
|
||||
}
|
||||
}
|
||||
}
|
||||
115
server/operators/corridorkey_local/run.py
Normal file
115
server/operators/corridorkey_local/run.py
Normal 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()
|
||||
Loading…
Reference in New Issue
Block a user