operator: qwen_layered_local — image -> N editable RGBA layers (Qwen-Image-Layered 20B, MLX)
Vendored mflux PR#302 fork (monster/Qwen-Image-Layered-MRP-MLX), tuned defaults 20 steps/640/baked-q8 (243s m3ultra, 366s m1ultra). Baked-model resolution via QWEN_LAYERED_MODEL env or per-box paths, HF+q8 fallback. nodes.json (gitignored, machine-local) updated on m3 to route to m1 too. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
8849438bce
commit
58a2a8177d
25
scripts/install_qwen_layered.sh
Executable file
25
scripts/install_qwen_layered.sh
Executable file
@ -0,0 +1,25 @@
|
||||
#!/bin/bash
|
||||
# Install the Qwen-Image-Layered MLX runtime (mflux PR#302 fork, vendored at
|
||||
# our Gitea) into vendor/mflux-qwen-layered + a uv venv. Idempotent.
|
||||
#
|
||||
# Model weights are NOT fetched here. run.py resolves, in order:
|
||||
# $QWEN_LAYERED_MODEL -> ~/qwen-layered/qwen-layered-q8 (m3)
|
||||
# -> ~/qwen-layered-staging/qwen-layered-q8 (m1)
|
||||
# -> HF Qwen/Qwen-Image-Layered with on-the-fly -q8 (54GB download).
|
||||
# Bake a local q8 once per box: .venv/bin/mflux-save \
|
||||
# --model Qwen/Qwen-Image-Layered --base-model qwen-image-layered \
|
||||
# --quantize 8 --path ~/qwen-layered/qwen-layered-q8
|
||||
set -euo pipefail
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
DEST="$ROOT/vendor/mflux-qwen-layered"
|
||||
REPO="ssh://git@100.71.119.27:222/monster/Qwen-Image-Layered-MRP-MLX.git"
|
||||
|
||||
if [ ! -d "$DEST/.git" ]; then
|
||||
git clone "$REPO" "$DEST"
|
||||
else
|
||||
echo "already cloned: $DEST"
|
||||
fi
|
||||
cd "$DEST"
|
||||
/opt/homebrew/bin/uv venv .venv --python 3.12
|
||||
/opt/homebrew/bin/uv pip install -q --python .venv/bin/python -e .
|
||||
.venv/bin/mflux-generate-qwen-layered --help >/dev/null && echo "qwen-layered OK: $DEST"
|
||||
21
server/operators/qwen_layered_local/manifest.json
Normal file
21
server/operators/qwen_layered_local/manifest.json
Normal file
@ -0,0 +1,21 @@
|
||||
{
|
||||
"id": "qwen_layered_local",
|
||||
"name": "Qwen-Image-Layered (local)",
|
||||
"category": "image-edit",
|
||||
"description": "Image → N editable RGBA layers (bg / subject / detail separation), fully local via the Qwen-Image-Layered 20B MLX port (mflux PR#302 fork). ~4 min on m3ultra, ~6 min on m1ultra at the tuned defaults (20 steps, 640, baked q8). The subject layer doubles as matting-grade background removal. Bake a local q8 model once per box (see scripts/install_qwen_layered.sh) or first run falls back to a 54GB HF download + on-the-fly quantization.",
|
||||
"accepts": ["image"],
|
||||
"produces": ["image"],
|
||||
"resources": "gpu",
|
||||
"entry": "run.py",
|
||||
"python": "vendor/mflux-qwen-layered/.venv/bin/python",
|
||||
"params_schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"layers": {"type": "integer", "default": 4, "description": "Number of RGBA layers to decompose into"},
|
||||
"steps": {"type": "integer", "default": 20, "description": "Denoising steps (20 = tuned default; 50 = upstream default, ~2.9x slower, no visible gain on tested inputs)"},
|
||||
"resolution": {"type": "integer", "enum": [640, 1024], "default": 640, "description": "Working resolution"},
|
||||
"seed": {"type": "integer", "default": 0, "description": "Random seed"},
|
||||
"prompt": {"type": "string", "default": "", "description": "Optional prompt to guide the decomposition"}
|
||||
}
|
||||
}
|
||||
}
|
||||
71
server/operators/qwen_layered_local/run.py
Normal file
71
server/operators/qwen_layered_local/run.py
Normal file
@ -0,0 +1,71 @@
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[3]
|
||||
VENDOR = ROOT / "vendor" / "mflux-qwen-layered"
|
||||
CLI = VENDOR / ".venv" / "bin" / "mflux-generate-qwen-layered"
|
||||
|
||||
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 image")
|
||||
sys.exit(1)
|
||||
if not CLI.exists():
|
||||
print(f"ERROR: qwen-layered not installed at {VENDOR}. "
|
||||
"Run scripts/install_qwen_layered.sh")
|
||||
sys.exit(1)
|
||||
|
||||
# Baked-model resolution: env override, then per-box conventional paths,
|
||||
# else fall back to the HF repo with on-the-fly q8 (slow first run: 54GB).
|
||||
candidates = [os.environ.get("QWEN_LAYERED_MODEL", "")]
|
||||
candidates += [str(Path.home() / "qwen-layered" / "qwen-layered-q8"),
|
||||
str(Path.home() / "qwen-layered-staging" / "qwen-layered-q8")]
|
||||
model_path = next((c for c in candidates if c and Path(c).is_dir()), None)
|
||||
|
||||
outdir = Path(a.outdir)
|
||||
cmd = [
|
||||
str(CLI), "--image", str(Path(a.input[0]).resolve()),
|
||||
"--layers", str(p.get("layers", 4)),
|
||||
"--steps", str(p.get("steps", 20)),
|
||||
"--resolution", str(p.get("resolution", 640)),
|
||||
"--seed", str(p.get("seed", 0)),
|
||||
"--output-dir", str(outdir.resolve()),
|
||||
]
|
||||
if model_path:
|
||||
cmd += ["--model-path", model_path]
|
||||
print(f"using baked model: {model_path}", flush=True)
|
||||
else:
|
||||
cmd += ["-q", "8"]
|
||||
print("no baked model found — HF fallback with on-the-fly q8 "
|
||||
"(first run downloads 54GB)", flush=True)
|
||||
if p.get("prompt"):
|
||||
cmd += ["--prompt", str(p["prompt"])]
|
||||
|
||||
env = os.environ.copy()
|
||||
env["HF_HUB_DISABLE_XET"] = "1"
|
||||
|
||||
print("+", " ".join(cmd), flush=True)
|
||||
res = subprocess.run(cmd, cwd=str(VENDOR), stdout=sys.stdout,
|
||||
stderr=subprocess.STDOUT, env=env)
|
||||
if res.returncode != 0:
|
||||
sys.exit(res.returncode)
|
||||
|
||||
layers = sorted(outdir.glob("layer_*.png"))
|
||||
if not layers:
|
||||
print("ERROR: qwen-layered produced no layer PNGs")
|
||||
sys.exit(1)
|
||||
stem = Path(a.input[0]).stem
|
||||
outputs = [{"path": str(f), "name": f"{stem}_{f.stem}.png",
|
||||
"meta": {"tool": "qwen_layered_local", "layer": i}}
|
||||
for i, f in enumerate(layers)]
|
||||
(outdir / "result.json").write_text(json.dumps({"outputs": outputs}))
|
||||
print(f"done: {len(layers)} layers", flush=True)
|
||||
Loading…
Reference in New Issue
Block a user