Compare commits
1 Commits
master
...
corridorke
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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