From 58a2a8177deb4941c0c707257fb4edac65df2c1c Mon Sep 17 00:00:00 2001 From: m3ultra Date: Sun, 19 Jul 2026 22:06:05 +1000 Subject: [PATCH] =?UTF-8?q?operator:=20qwen=5Flayered=5Flocal=20=E2=80=94?= =?UTF-8?q?=20image=20->=20N=20editable=20RGBA=20layers=20(Qwen-Image-Laye?= =?UTF-8?q?red=2020B,=20MLX)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- scripts/install_qwen_layered.sh | 25 +++++++ .../qwen_layered_local/manifest.json | 21 ++++++ server/operators/qwen_layered_local/run.py | 71 +++++++++++++++++++ 3 files changed, 117 insertions(+) create mode 100755 scripts/install_qwen_layered.sh create mode 100644 server/operators/qwen_layered_local/manifest.json create mode 100644 server/operators/qwen_layered_local/run.py diff --git a/scripts/install_qwen_layered.sh b/scripts/install_qwen_layered.sh new file mode 100755 index 0000000..58727b9 --- /dev/null +++ b/scripts/install_qwen_layered.sh @@ -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" diff --git a/server/operators/qwen_layered_local/manifest.json b/server/operators/qwen_layered_local/manifest.json new file mode 100644 index 0000000..a4fbffa --- /dev/null +++ b/server/operators/qwen_layered_local/manifest.json @@ -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"} + } + } +} diff --git a/server/operators/qwen_layered_local/run.py b/server/operators/qwen_layered_local/run.py new file mode 100644 index 0000000..55381a3 --- /dev/null +++ b/server/operators/qwen_layered_local/run.py @@ -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)