feat: sd_local (SD1.5 + anatomy LoRAs + pose/IP-Adapter) + stereo_depth (SBS → SGBM depth/cloud)

sd_local: the repose stack as an operator — Hyper_Realism + repose/models/lora
+ optional OpenPose T-pose template + IP-Adapter ref. Absolute venv path =
primary-only by design. Verified: 512x640 portrait in one shot.

stereo_depth: side-by-side stereo video/frame → per-frame SGBM disparity maps
+ colored midframe .ply for Blender displace/point-cloud work. venvs/stereo
(opencv-headless). Verified on a synthetic 24px-disparity plate: subject
bright, zero-disparity bg black, 1.4MB cloud.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
type-two 2026-07-19 01:22:12 +10:00
parent fcb5e80702
commit 95708ad220
4 changed files with 230 additions and 0 deletions

View File

@ -0,0 +1,26 @@
{
"id": "sd_local",
"name": "SD1.5 + anatomy LoRAs (local, primary-only)",
"category": "generate",
"description": "Prompt → image via the repose stack on the primary: SD1.5 Hyper_Realism + John's anatomy LoRAs (~/Documents/repose/models/lora) + optional OpenPose T-pose template (front/back/side) + optional IP-Adapter reference image. The no-cloud, no-content-filter lane FLUX can't cover — LoRA-styled anatomy, exact pose control, reference-guided identity. Primary-only: needs ~/Documents/repose (absolute venv path keeps it off remote dispatch by design).",
"accepts": ["image"],
"produces": ["image"],
"resources": "gpu",
"entry": "run.py",
"python": "/Users/m3ultra/Documents/repose/venv/bin/python",
"params_schema": {
"type": "object",
"properties": {
"prompt": {"type": "string", "default": "", "description": "What to generate"},
"negative": {"type": "string", "default": "deformed, extra limbs, extra fingers, mutated hands, missing limbs, cropped, blurry, lowres, watermark, text, multiple people, child", "description": "Negative prompt"},
"loras": {"type": "array", "items": {"type": "string"}, "default": [], "description": "'stem=weight' from repose/models/lora, e.g. 'GodPussy1 v4=0.6'"},
"pose": {"type": "string", "enum": ["none", "front", "back", "side"], "default": "none", "description": "OpenPose T-pose template via ControlNet"},
"ip": {"type": "number", "default": 0.7, "description": "IP-Adapter strength when a reference image is attached"},
"steps": {"type": "integer", "default": 28, "minimum": 1, "maximum": 60},
"cfg": {"type": "number", "default": 7.0},
"seed": {"type": "integer", "default": 7},
"width": {"type": "integer", "default": 512, "description": "SD1.5 native ≈512-768; upscale after via seedvr2"},
"height": {"type": "integer", "default": 768}
}
}
}

View File

@ -0,0 +1,78 @@
"""sd_local — SD1.5 Hyper_Realism + anatomy LoRAs (+ optional pose ControlNet / IP-Adapter ref).
Runs UNDER the repose venv (manifest python is that venv, absolute primary-only).
Generalizes scripts/repose.py from MESHGOD: pose template optional, plain txt2img otherwise.
"""
import argparse
import json
import os
import sys
os.environ.setdefault("PYTORCH_ENABLE_MPS_FALLBACK", "1")
import torch # noqa: E402
from diffusers import DPMSolverMultistepScheduler # noqa: E402
from PIL import Image # noqa: E402
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)
HERE = os.path.expanduser("~/Documents/repose")
BASE = os.path.join(HERE, "models", "Hyper_Realism_1.2_fp16.safetensors")
LORA_DIR = os.path.join(HERE, "models", "lora")
if not os.path.exists(BASE):
print(f"ERROR: {BASE} missing — sd_local runs on the primary only")
sys.exit(1)
dev = "mps" if torch.backends.mps.is_available() else "cpu"
dtype = torch.float16 if dev == "mps" else torch.float32
pose = p.get("pose", "none")
kw = {}
if pose != "none":
from diffusers import ControlNetModel, StableDiffusionControlNetPipeline
tpl = os.path.join(HERE, f"tpose_{pose}.png")
cn = ControlNetModel.from_pretrained("lllyasviel/control_v11p_sd15_openpose", torch_dtype=dtype)
pipe = StableDiffusionControlNetPipeline.from_single_file(
BASE, controlnet=cn, torch_dtype=dtype, safety_checker=None, requires_safety_checker=False)
kw["image"] = Image.open(tpl).convert("RGB")
else:
from diffusers import StableDiffusionPipeline
pipe = StableDiffusionPipeline.from_single_file(
BASE, torch_dtype=dtype, safety_checker=None, requires_safety_checker=False)
pipe.scheduler = DPMSolverMultistepScheduler.from_config(pipe.scheduler.config, use_karras_sigmas=True)
pipe = pipe.to(dev)
# no attention slicing — it crashes IP-Adapter attn-processor injection (see repose.py)
if p.get("loras"):
names, weights = [], []
for spec in p["loras"]:
stem, _, w = str(spec).partition("=")
pipe.load_lora_weights(LORA_DIR, weight_name=f"{stem}.safetensors",
adapter_name=stem.replace(" ", "_"))
names.append(stem.replace(" ", "_"))
weights.append(float(w or 0.6))
pipe.set_adapters(names, weights)
print(f"[sd] loras: {dict(zip(names, weights))}", flush=True)
if a.input: # reference image → IP-Adapter
pipe.load_ip_adapter("h94/IP-Adapter", subfolder="models", weight_name="ip-adapter_sd15.bin")
pipe.set_ip_adapter_scale(float(p.get("ip", 0.7)))
kw["ip_adapter_image"] = Image.open(a.input[0]).convert("RGB")
print(f"[sd] ip-adapter ref @ {p.get('ip', 0.7)}", flush=True)
gen = torch.Generator("cpu").manual_seed(int(p.get("seed", 7)))
img = pipe(prompt=p.get("prompt", ""), negative_prompt=p.get("negative", ""),
num_inference_steps=int(p.get("steps", 28)), guidance_scale=float(p.get("cfg", 7.0)),
generator=gen, width=int(p.get("width", 512)), height=int(p.get("height", 768)),
**kw).images[0]
out = os.path.join(a.outdir, "sd_local.png")
img.save(out)
json.dump({"outputs": [{"path": "sd_local.png", "meta": {"tool": "sd_local"}}]},
open(os.path.join(a.outdir, "result.json"), "w"))
print("done: sd_local.png", flush=True)

View File

@ -0,0 +1,20 @@
{
"id": "stereo_depth",
"name": "Stereo SBS → depth + point cloud (local)",
"category": "video-prep",
"description": "Side-by-side stereoscopic video (or one SBS frame) → per-frame disparity/depth maps via OpenCV StereoSGBM + a colored .ply point cloud of the middle frame for Blender. TRUE geometric depth from the L/R eye shift — no AI hallucination. Uncalibrated SBS rips aren't rectified, so expect relief-map depth (great for Displace-modifier planes), not survey-grade geometry. Output: depth preview mp4 + mid-frame .ply + zipped depth PNGs.",
"accepts": ["video", "image"],
"produces": ["archive"],
"resources": "cpu",
"entry": "run.py",
"python": "venvs/stereo/bin/python",
"params_schema": {
"type": "object",
"properties": {
"max_frames": {"type": "integer", "default": 120, "description": "Cap processed frames"},
"num_disp": {"type": "integer", "default": 128, "description": "Disparity search range (multiple of 16; raise for close subjects)"},
"block": {"type": "integer", "default": 5, "description": "SGBM block size (odd, 3-11)"},
"layout": {"type": "string", "enum": ["sbs", "tab"], "default": "sbs", "description": "side-by-side or top-and-bottom stereo"}
}
}
}

View File

@ -0,0 +1,106 @@
"""stereo_depth — SBS stereoscopic video/frame → SGBM disparity maps + mid-frame point cloud.
True geometric depth from the built-in L/R parallax (cv2.StereoSGBM). ffmpeg (system)
decodes/splits; depth PNGs + preview mp4 + a colored .ply land as the outputs.
ponytail: uncalibrated disparity (SBS rips have no camera intrinsics) z is relative,
perfect for Blender displace planes; not metric reconstruction.
"""
import argparse
import glob
import json
import os
import shutil
import subprocess
import sys
import tempfile
import zipfile
import cv2
import numpy as np
FFMPEG = shutil.which("ffmpeg") or "/opt/homebrew/bin/ffmpeg" # headless ssh has no brew 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")
sys.exit(1)
src = a.input[0]
outdir = a.outdir
max_frames = int(p.get("max_frames", 120))
num_disp = max(16, (int(p.get("num_disp", 128)) // 16) * 16)
block = int(p.get("block", 5)) | 1
tab = p.get("layout", "sbs") == "tab"
work = tempfile.mkdtemp(prefix="stereo_")
frames_dir = os.path.join(work, "frames")
depth_dir = os.path.join(work, "depth")
os.makedirs(frames_dir)
os.makedirs(depth_dir)
# decode → frames (a single image just becomes frame 1)
subprocess.run([FFMPEG, "-y", "-loglevel", "error", "-i", src,
"-frames:v", str(max_frames), os.path.join(frames_dir, "f%05d.png")], check=True)
frames = sorted(glob.glob(os.path.join(frames_dir, "*.png")))
print(f"[stereo] {len(frames)} frames, num_disp={num_disp} block={block}", flush=True)
sgbm = cv2.StereoSGBM_create(
minDisparity=0, numDisparities=num_disp, blockSize=block,
P1=8 * 3 * block * block, P2=32 * 3 * block * block,
disp12MaxDiff=1, uniquenessRatio=10, speckleWindowSize=100, speckleRange=2,
mode=cv2.STEREO_SGBM_MODE_SGBM_3WAY)
mid = frames[len(frames) // 2]
for i, f in enumerate(frames):
img = cv2.imread(f)
h, w = img.shape[:2]
if tab:
L, R = img[: h // 2], img[h // 2:]
else:
L, R = img[:, : w // 2], img[:, w // 2:]
gl, gr = cv2.cvtColor(L, cv2.COLOR_BGR2GRAY), cv2.cvtColor(R, cv2.COLOR_BGR2GRAY)
disp = sgbm.compute(gl, gr).astype(np.float32) / 16.0
disp[disp < 0] = 0
norm = (disp / max(1e-6, disp.max()) * 255).astype(np.uint8)
cv2.imwrite(os.path.join(depth_dir, os.path.basename(f)), norm)
if f == mid: # colored point cloud of the middle frame
ys, xs = np.where(disp > 1)
step = max(1, len(ys) // 400_000) # cap cloud size
ys, xs = ys[::step], xs[::step]
z = disp[ys, xs]
rgb = L[ys, xs][:, ::-1] # BGR→RGB
ply = os.path.join(outdir, "midframe_cloud.ply")
with open(ply, "w") as fh:
fh.write("ply\nformat ascii 1.0\n"
f"element vertex {len(ys)}\n"
"property float x\nproperty float y\nproperty float z\n"
"property uchar red\nproperty uchar green\nproperty uchar blue\nend_header\n")
for (yy, xx, zz, (r, g, b)) in zip(ys, xs, z, rgb):
fh.write(f"{xx} {-yy} {zz:.1f} {r} {g} {b}\n")
print(f"[stereo] wrote midframe_cloud.ply ({len(ys)} pts)", flush=True)
if i % 24 == 0:
print(f"[stereo] frame {i + 1}/{len(frames)}", flush=True)
outputs = [{"path": "midframe_cloud.ply", "meta": {"tool": "stereo_sgbm"}}]
if len(frames) > 1: # depth preview video
prev = os.path.join(outdir, "depth_preview.mp4")
subprocess.run([FFMPEG, "-y", "-loglevel", "error", "-framerate", "24",
"-i", os.path.join(depth_dir, "f%05d.png"),
"-pix_fmt", "yuv420p", prev], check=True)
outputs.append({"path": "depth_preview.mp4", "meta": {"tool": "stereo_sgbm"}})
zpath = os.path.join(outdir, "depth_frames.zip")
with zipfile.ZipFile(zpath, "w", zipfile.ZIP_DEFLATED) as zf:
for f in sorted(glob.glob(os.path.join(depth_dir, "*.png"))):
zf.write(f, os.path.basename(f))
outputs.append({"path": "depth_frames.zip", "meta": {"tool": "stereo_sgbm"}})
json.dump({"outputs": outputs}, open(os.path.join(outdir, "result.json"), "w"))
shutil.rmtree(work, ignore_errors=True)
print("done:", ", ".join(o["path"] for o in outputs), flush=True)