Instructions to use HandEdit/HandEdit-LoRA with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- PEFT
How to use HandEdit/HandEdit-LoRA with PEFT:
Task type is invalid.
- Notebooks
- Google Colab
- Kaggle
| #!/usr/bin/env python3 | |
| """Batch image editing with LongCat-Image-Edit and a PEFT LoRA adapter.""" | |
| from __future__ import annotations | |
| import argparse | |
| from pathlib import Path | |
| import torch | |
| from diffusers import LongCatImageEditPipeline | |
| from peft import PeftModel | |
| from PIL import Image | |
| IMAGE_EXTS = {".jpg", ".jpeg", ".png", ".webp", ".bmp"} | |
| DEFAULT_BASE = "meituan-longcat/LongCat-Image-Edit" | |
| DEFAULT_PROMPT = ( | |
| "Edit only the human hand region. Replace the human hand with a realistic Inspire robotic " | |
| "hand with correct robotic finger structure and joints. Preserve the original wrist pose, " | |
| "palm orientation, finger articulation, grasp geometry, and contact points with the object. " | |
| "The robot hand must be kinematically feasible and physically plausible, without penetrating " | |
| "the object. Keep the object pose, shape, texture, background, lighting, camera viewpoint, " | |
| "and all non-hand regions unchanged." | |
| ) | |
| def list_images(input_dir: Path, recursive: bool) -> list[Path]: | |
| iterator = input_dir.rglob("*") if recursive else input_dir.iterdir() | |
| return sorted( | |
| path | |
| for path in iterator | |
| if path.is_file() and path.suffix.lower() in IMAGE_EXTS | |
| ) | |
| def apply_lora_scale(model: torch.nn.Module, scale: float) -> int: | |
| """Multiply every active PEFT LoRA layer scale by ``scale``.""" | |
| if scale == 1.0: | |
| return 0 | |
| changed = 0 | |
| for module in model.modules(): | |
| scaling = getattr(module, "scaling", None) | |
| if isinstance(scaling, dict): | |
| for adapter_name in list(scaling): | |
| scaling[adapter_name] *= scale | |
| changed += 1 | |
| return changed | |
| def main() -> None: | |
| parser = argparse.ArgumentParser( | |
| description="Batch inference for LongCat-Image-Edit with the HandEdit LoRA." | |
| ) | |
| parser.add_argument( | |
| "--base", | |
| default=DEFAULT_BASE, | |
| help=f"Base model directory or model ID (default: {DEFAULT_BASE}).", | |
| ) | |
| parser.add_argument( | |
| "--lora", | |
| default="./checkpoints/longcat", | |
| help="PEFT LoRA directory (default: ./checkpoints/longcat).", | |
| ) | |
| parser.add_argument("--input_dir", required=True) | |
| parser.add_argument("--output_dir", required=True) | |
| parser.add_argument("--prompt", default=DEFAULT_PROMPT) | |
| parser.add_argument("--negative_prompt", default="") | |
| parser.add_argument("--guidance_scale", type=float, default=4.5) | |
| parser.add_argument("--steps", type=int, default=50) | |
| parser.add_argument("--seed", type=int, default=43) | |
| parser.add_argument( | |
| "--seed_mode", choices=["fixed", "increment"], default="fixed" | |
| ) | |
| parser.add_argument("--lora_scale", type=float, default=1.0) | |
| parser.add_argument( | |
| "--offload", choices=["model", "sequential", "none"], default="model" | |
| ) | |
| parser.add_argument("--local_files_only", action="store_true") | |
| parser.add_argument("--suffix", default="") | |
| parser.add_argument("--recursive", action="store_true") | |
| parser.add_argument("--skip_existing", action="store_true") | |
| args = parser.parse_args() | |
| if not torch.cuda.is_available(): | |
| raise RuntimeError("CUDA is required for practical LongCat inference.") | |
| if args.steps < 2: | |
| raise ValueError("--steps must be at least 2") | |
| if args.guidance_scale < 0: | |
| raise ValueError("--guidance_scale must be non-negative") | |
| input_root = Path(args.input_dir).expanduser().resolve() | |
| output_root = Path(args.output_dir).expanduser().resolve() | |
| lora_root = Path(args.lora).expanduser().resolve() | |
| if not input_root.is_dir(): | |
| raise FileNotFoundError(f"Input directory not found: {input_root}") | |
| if not lora_root.is_dir(): | |
| raise FileNotFoundError(f"LoRA directory not found: {lora_root}") | |
| paths = list_images(input_root, args.recursive) | |
| if not paths: | |
| raise RuntimeError(f"No images found under: {input_root}") | |
| output_root.mkdir(parents=True, exist_ok=True) | |
| print("[1/3] Loading LongCat-Image-Edit...") | |
| pipe = LongCatImageEditPipeline.from_pretrained( | |
| args.base, | |
| torch_dtype=torch.bfloat16, | |
| local_files_only=args.local_files_only, | |
| ) | |
| print("[2/3] Loading HandEdit LoRA...") | |
| pipe.transformer = PeftModel.from_pretrained( | |
| pipe.transformer, | |
| str(lora_root), | |
| is_trainable=False, | |
| ) | |
| changed = apply_lora_scale(pipe.transformer, args.lora_scale) | |
| if args.lora_scale != 1.0: | |
| print( | |
| f"[INFO] LoRA scale={args.lora_scale}; " | |
| f"adjusted {changed} active LoRA layers." | |
| ) | |
| if args.offload == "model": | |
| pipe.enable_model_cpu_offload() | |
| elif args.offload == "sequential": | |
| pipe.enable_sequential_cpu_offload() | |
| else: | |
| pipe.to("cuda") | |
| print(f"[3/3] Processing {len(paths)} images...") | |
| current_seed = args.seed | |
| for index, input_path in enumerate(paths, start=1): | |
| relative = input_path.relative_to(input_root) | |
| output_path = output_root / relative.with_name( | |
| relative.stem + args.suffix + ".png" | |
| ) | |
| output_path.parent.mkdir(parents=True, exist_ok=True) | |
| if args.skip_existing and output_path.exists(): | |
| print(f"[{index}/{len(paths)}] SKIP {output_path}") | |
| else: | |
| with Image.open(input_path) as source: | |
| image = source.convert("RGB") | |
| generator = torch.Generator("cpu").manual_seed(current_seed) | |
| result = pipe( | |
| image, | |
| args.prompt, | |
| negative_prompt=args.negative_prompt, | |
| guidance_scale=args.guidance_scale, | |
| num_inference_steps=args.steps, | |
| num_images_per_prompt=1, | |
| generator=generator, | |
| ).images[0] | |
| result.save(output_path) | |
| print(f"[{index}/{len(paths)}] OK {input_path.name} -> {output_path}") | |
| if args.seed_mode == "increment": | |
| current_seed += 1 | |
| print(f"[DONE] Results saved under: {output_root}") | |
| if __name__ == "__main__": | |
| main() | |