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
Add files using upload-large-folder tool
Browse files- README.md +92 -0
- SANITIZATION.md +14 -0
- checkpoints.zip +3 -0
- checkpoints/flux2/handedit_flux2_klein4b_lora.safetensors +3 -0
- checkpoints/longcat/adapter_config.json +27 -0
- checkpoints/longcat/adapter_model.safetensors +3 -0
- checkpoints/omnigen/adapter_config.json +17 -0
- checkpoints/omnigen/adapter_model.safetensors +3 -0
- checkpoints/step1x/inspire_step1x_r32_a16_res512.safetensors +3 -0
- docs/INFERENCE.md +132 -0
- requirements.txt +14 -0
- scripts/download_weights.py +91 -0
- scripts/infer_flux2_lora.py +133 -0
- scripts/infer_longcat_lora.py +164 -0
- scripts/infer_omnigen_lora.py +121 -0
- scripts/infer_step1x_lora.py +156 -0
- scripts/verify_weights.py +56 -0
- weights_manifest.json +61 -0
README.md
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
library_name: peft
|
| 3 |
+
tags:
|
| 4 |
+
- image-editing
|
| 5 |
+
- lora
|
| 6 |
+
- robotics
|
| 7 |
+
- hand-editing
|
| 8 |
+
base_model:
|
| 9 |
+
- meituan-longcat/LongCat-Image-Edit
|
| 10 |
+
- Shitao/OmniGen-v1
|
| 11 |
+
- black-forest-labs/FLUX.2-klein-base-4B
|
| 12 |
+
- stepfun-ai/Step1X-Edit
|
| 13 |
+
---
|
| 14 |
+
|
| 15 |
+
# HandEdit LoRA
|
| 16 |
+
|
| 17 |
+
HandEdit provides parameter-efficient LoRA adapters that specialize four
|
| 18 |
+
open-source image-editing backbones for human-to-robot-hand replacement.
|
| 19 |
+
|
| 20 |
+
We build more than 20K aligned training pairs from the HandEdit data. Each pair
|
| 21 |
+
contains an input image with a real human hand interacting with an object and a
|
| 22 |
+
corresponding target image in which only the hand is replaced by an Inspire
|
| 23 |
+
robotic hand. The training instruction asks the model to preserve the original
|
| 24 |
+
wrist pose, finger configuration, grasp relation, object contact points,
|
| 25 |
+
interaction object, background, lighting, and camera viewpoint as closely as
|
| 26 |
+
possible.
|
| 27 |
+
|
| 28 |
+
Only LoRA parameters are released; the full base-model weights are not
|
| 29 |
+
redistributed.
|
| 30 |
+
|
| 31 |
+
## Models
|
| 32 |
+
|
| 33 |
+
| Adapter | Official base model | Local checkpoint |
|
| 34 |
+
| --- | --- | --- |
|
| 35 |
+
| LongCat-Image-Edit | [`meituan-longcat/LongCat-Image-Edit`](https://huggingface.co/meituan-longcat/LongCat-Image-Edit) | `checkpoints/longcat/` |
|
| 36 |
+
| OmniGen-v1 | [`Shitao/OmniGen-v1`](https://huggingface.co/Shitao/OmniGen-v1) | `checkpoints/omnigen/` |
|
| 37 |
+
| FLUX.2 Klein Base 4B | [`black-forest-labs/FLUX.2-klein-base-4B`](https://huggingface.co/black-forest-labs/FLUX.2-klein-base-4B) | `checkpoints/flux2/handedit_flux2_klein4b_lora.safetensors` |
|
| 38 |
+
| Step1X-Edit | [`stepfun-ai/Step1X-Edit`](https://huggingface.co/stepfun-ai/Step1X-Edit) | `checkpoints/step1x/inspire_step1x_r32_a16_res512.safetensors` |
|
| 39 |
+
|
| 40 |
+
## Download
|
| 41 |
+
|
| 42 |
+
**[Download all four sanitized LoRA checkpoints](https://huggingface.co/HandEdit/HandEdit-LoRA/resolve/main/checkpoints.zip?download=true)**
|
| 43 |
+
|
| 44 |
+
Or download and verify them from the command line:
|
| 45 |
+
|
| 46 |
+
```bash
|
| 47 |
+
pip install huggingface_hub
|
| 48 |
+
python scripts/download_weights.py
|
| 49 |
+
python scripts/verify_weights.py
|
| 50 |
+
```
|
| 51 |
+
|
| 52 |
+
The published ZIP and individual adapter files are hosted in
|
| 53 |
+
[`HandEdit/HandEdit-LoRA`](https://huggingface.co/HandEdit/HandEdit-LoRA).
|
| 54 |
+
|
| 55 |
+
## Inference
|
| 56 |
+
|
| 57 |
+
Detailed environment setup and commands are in
|
| 58 |
+
[`docs/INFERENCE.md`](docs/INFERENCE.md). The shortest examples are:
|
| 59 |
+
|
| 60 |
+
```bash
|
| 61 |
+
# LongCat-Image-Edit
|
| 62 |
+
python scripts/infer_longcat_lora.py \
|
| 63 |
+
--input_dir ./examples/input --output_dir ./outputs/longcat
|
| 64 |
+
|
| 65 |
+
# OmniGen-v1
|
| 66 |
+
python scripts/infer_omnigen_lora.py \
|
| 67 |
+
--input_dir ./examples/input --output_dir ./outputs/omnigen
|
| 68 |
+
|
| 69 |
+
# FLUX.2 Klein Base 4B
|
| 70 |
+
python scripts/infer_flux2_lora.py \
|
| 71 |
+
--input_dir ./examples/input --output_dir ./outputs/flux2
|
| 72 |
+
|
| 73 |
+
# Step1X-Edit
|
| 74 |
+
python scripts/infer_step1x_lora.py \
|
| 75 |
+
--repo_dir ./third_party/Step1X-Edit \
|
| 76 |
+
--model_dir ./weights/Step1X-Edit \
|
| 77 |
+
--input_dir ./examples/input --output_dir ./outputs/step1x \
|
| 78 |
+
--quantized --offload
|
| 79 |
+
```
|
| 80 |
+
|
| 81 |
+
## Release hygiene
|
| 82 |
+
|
| 83 |
+
The public weights are sanitized copies. Training-data identifiers, dataset
|
| 84 |
+
sizes, local paths, author fields, timestamps, session information, and other
|
| 85 |
+
training-process metadata were removed without changing tensor bytes. See
|
| 86 |
+
[`SANITIZATION.md`](SANITIZATION.md) and `weights_manifest.json`.
|
| 87 |
+
|
| 88 |
+
## Base-model licenses
|
| 89 |
+
|
| 90 |
+
Users must follow the license and access terms of each official base model.
|
| 91 |
+
This repository does not redistribute the four base models.
|
| 92 |
+
|
SANITIZATION.md
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Release sanitization
|
| 2 |
+
|
| 3 |
+
The public checkpoint files are sanitized copies, not the original training
|
| 4 |
+
artifacts.
|
| 5 |
+
|
| 6 |
+
- Optional SafeTensors metadata was removed from all four adapters.
|
| 7 |
+
- Published SafeTensors headers retain only `{"format": "pt"}`.
|
| 8 |
+
- PEFT JSON files contain only adapter architecture and loading parameters.
|
| 9 |
+
- Dataset identifiers, dataset sizes, local paths, timestamps, author fields,
|
| 10 |
+
host/session information, and training-process metadata are not published.
|
| 11 |
+
- Tensor names, shapes, data types, offsets, and tensor data bytes were preserved.
|
| 12 |
+
|
| 13 |
+
`weights_manifest.json` records checksums for the sanitized release files.
|
| 14 |
+
|
checkpoints.zip
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:9e89032b37d439205e940821f82735c790a35e5c32b4813ab985fb5ee0fdcad2
|
| 3 |
+
size 621076920
|
checkpoints/flux2/handedit_flux2_klein4b_lora.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:11a9d65f8b53a1f1de08d5bcf02c181ebc122ae79661367bdad3b5fddeac591a
|
| 3 |
+
size 369643968
|
checkpoints/longcat/adapter_config.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"bias": "none",
|
| 3 |
+
"fan_in_fan_out": false,
|
| 4 |
+
"inference_mode": true,
|
| 5 |
+
"init_lora_weights": "gaussian",
|
| 6 |
+
"lora_alpha": 8,
|
| 7 |
+
"lora_dropout": 0.0,
|
| 8 |
+
"peft_type": "LORA",
|
| 9 |
+
"r": 32,
|
| 10 |
+
"target_modules": [
|
| 11 |
+
"attn.to_add_out",
|
| 12 |
+
"attn.to_q",
|
| 13 |
+
"attn.add_v_proj",
|
| 14 |
+
"attn.to_v",
|
| 15 |
+
"attn.add_k_proj",
|
| 16 |
+
"attn.add_q_proj",
|
| 17 |
+
"ff_context.net.2",
|
| 18 |
+
"ff_context.net.0.proj",
|
| 19 |
+
"attn.to_k",
|
| 20 |
+
"ff.net.0.proj",
|
| 21 |
+
"attn.to_out.0",
|
| 22 |
+
"ff.net.2"
|
| 23 |
+
],
|
| 24 |
+
"task_type": null,
|
| 25 |
+
"use_dora": false,
|
| 26 |
+
"use_rslora": false
|
| 27 |
+
}
|
checkpoints/longcat/adapter_model.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:2391face660c6e5dd5439553d96f89059c8bfa5160af89778455cf9bf8568a5e
|
| 3 |
+
size 94421944
|
checkpoints/omnigen/adapter_config.json
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"bias": "none",
|
| 3 |
+
"fan_in_fan_out": false,
|
| 4 |
+
"inference_mode": true,
|
| 5 |
+
"init_lora_weights": "gaussian",
|
| 6 |
+
"lora_alpha": 16,
|
| 7 |
+
"lora_dropout": 0.0,
|
| 8 |
+
"peft_type": "LORA",
|
| 9 |
+
"r": 16,
|
| 10 |
+
"target_modules": [
|
| 11 |
+
"o_proj",
|
| 12 |
+
"qkv_proj"
|
| 13 |
+
],
|
| 14 |
+
"task_type": null,
|
| 15 |
+
"use_dora": false,
|
| 16 |
+
"use_rslora": false
|
| 17 |
+
}
|
checkpoints/omnigen/adapter_model.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:6863295d2829c25eeed85eb175d5d21ee5867b2536023124d9f1cd2dfce9b971
|
| 3 |
+
size 18891496
|
checkpoints/step1x/inspire_step1x_r32_a16_res512.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:e32ef656e023b0cc503ab0a24f645a3e99ba32ce8cae3dbd0eba221b988c1156
|
| 3 |
+
size 306426744
|
docs/INFERENCE.md
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Inference
|
| 2 |
+
|
| 3 |
+
Run commands from the repository root. The four upstream projects do not
|
| 4 |
+
necessarily share one dependency set, so separate virtual environments are
|
| 5 |
+
recommended.
|
| 6 |
+
|
| 7 |
+
## 1. Download the HandEdit adapters
|
| 8 |
+
|
| 9 |
+
Download and extract all four sanitized LoRA adapters:
|
| 10 |
+
|
| 11 |
+
```bash
|
| 12 |
+
python scripts/download_weights.py
|
| 13 |
+
```
|
| 14 |
+
|
| 15 |
+
You can verify an existing local copy with:
|
| 16 |
+
|
| 17 |
+
```bash
|
| 18 |
+
python scripts/verify_weights.py
|
| 19 |
+
```
|
| 20 |
+
|
| 21 |
+
The base models are not bundled. Download them from their official projects and
|
| 22 |
+
follow the corresponding licenses and access requirements.
|
| 23 |
+
|
| 24 |
+
## 2. LongCat-Image-Edit
|
| 25 |
+
|
| 26 |
+
Install the shared dependencies:
|
| 27 |
+
|
| 28 |
+
```bash
|
| 29 |
+
pip install -r requirements.txt
|
| 30 |
+
```
|
| 31 |
+
|
| 32 |
+
Run batch inference:
|
| 33 |
+
|
| 34 |
+
```bash
|
| 35 |
+
python scripts/infer_longcat_lora.py \
|
| 36 |
+
--input_dir ./examples/input \
|
| 37 |
+
--output_dir ./outputs/longcat \
|
| 38 |
+
--skip_existing
|
| 39 |
+
```
|
| 40 |
+
|
| 41 |
+
The default base model is `meituan-longcat/LongCat-Image-Edit`. Use `--base`
|
| 42 |
+
to point to a local model directory, and add `--local_files_only` for an
|
| 43 |
+
offline cache.
|
| 44 |
+
|
| 45 |
+
## 3. OmniGen-v1
|
| 46 |
+
|
| 47 |
+
Install the official OmniGen repository in its own environment:
|
| 48 |
+
|
| 49 |
+
```bash
|
| 50 |
+
git clone https://github.com/VectorSpaceLab/OmniGen.git third_party/OmniGen
|
| 51 |
+
pip install -e third_party/OmniGen
|
| 52 |
+
pip install peft pillow
|
| 53 |
+
```
|
| 54 |
+
|
| 55 |
+
Run:
|
| 56 |
+
|
| 57 |
+
```bash
|
| 58 |
+
python scripts/infer_omnigen_lora.py \
|
| 59 |
+
--input_dir ./examples/input \
|
| 60 |
+
--output_dir ./outputs/omnigen \
|
| 61 |
+
--offload_model \
|
| 62 |
+
--skip_existing
|
| 63 |
+
```
|
| 64 |
+
|
| 65 |
+
The default base model is `Shitao/OmniGen-v1`.
|
| 66 |
+
|
| 67 |
+
## 4. FLUX.2 Klein Base 4B
|
| 68 |
+
|
| 69 |
+
Install the shared dependencies:
|
| 70 |
+
|
| 71 |
+
```bash
|
| 72 |
+
pip install -r requirements.txt
|
| 73 |
+
```
|
| 74 |
+
|
| 75 |
+
Run:
|
| 76 |
+
|
| 77 |
+
```bash
|
| 78 |
+
python scripts/infer_flux2_lora.py \
|
| 79 |
+
--input_dir ./examples/input \
|
| 80 |
+
--output_dir ./outputs/flux2 \
|
| 81 |
+
--skip_existing
|
| 82 |
+
```
|
| 83 |
+
|
| 84 |
+
The default base model is
|
| 85 |
+
`black-forest-labs/FLUX.2-klein-base-4B`. The script uses model CPU offload by
|
| 86 |
+
default; select `--offload sequential` for lower VRAM or `--offload none` for
|
| 87 |
+
maximum speed on a sufficiently large GPU.
|
| 88 |
+
|
| 89 |
+
## 5. Step1X-Edit
|
| 90 |
+
|
| 91 |
+
Clone and install the official repository in its own environment:
|
| 92 |
+
|
| 93 |
+
```bash
|
| 94 |
+
git clone https://github.com/stepfun-ai/Step1X-Edit.git third_party/Step1X-Edit
|
| 95 |
+
pip install -r third_party/Step1X-Edit/requirements.txt
|
| 96 |
+
```
|
| 97 |
+
|
| 98 |
+
Prepare the official base-model directory expected by Step1X:
|
| 99 |
+
|
| 100 |
+
```text
|
| 101 |
+
weights/Step1X-Edit/
|
| 102 |
+
├── Qwen2.5-VL-7B-Instruct/
|
| 103 |
+
├── step1x-edit-i1258.safetensors
|
| 104 |
+
└── vae.safetensors
|
| 105 |
+
```
|
| 106 |
+
|
| 107 |
+
Run:
|
| 108 |
+
|
| 109 |
+
```bash
|
| 110 |
+
python scripts/infer_step1x_lora.py \
|
| 111 |
+
--repo_dir ./third_party/Step1X-Edit \
|
| 112 |
+
--model_dir ./weights/Step1X-Edit \
|
| 113 |
+
--input_dir ./examples/input \
|
| 114 |
+
--output_dir ./outputs/step1x \
|
| 115 |
+
--version v1.0 \
|
| 116 |
+
--quantized \
|
| 117 |
+
--offload \
|
| 118 |
+
--skip_existing
|
| 119 |
+
```
|
| 120 |
+
|
| 121 |
+
## Shared edit instruction
|
| 122 |
+
|
| 123 |
+
The scripts use the following default instruction:
|
| 124 |
+
|
| 125 |
+
> Edit only the human hand region. Replace the human hand with a realistic
|
| 126 |
+
> Inspire robotic hand with correct robotic finger structure and joints.
|
| 127 |
+
> Preserve the original wrist pose, palm orientation, finger articulation,
|
| 128 |
+
> grasp geometry, and contact points with the object. Keep the object,
|
| 129 |
+
> background, lighting, camera viewpoint, and all non-hand regions unchanged.
|
| 130 |
+
|
| 131 |
+
Override it with `--prompt` when needed.
|
| 132 |
+
|
requirements.txt
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Shared runtime for LongCat-Image-Edit and FLUX.2 Klein.
|
| 2 |
+
# OmniGen and Step1X additionally require their official repositories;
|
| 3 |
+
# see docs/INFERENCE.md.
|
| 4 |
+
accelerate>=1.2
|
| 5 |
+
huggingface_hub>=0.28
|
| 6 |
+
numpy>=1.24
|
| 7 |
+
peft>=0.18.0
|
| 8 |
+
pillow>=10.0
|
| 9 |
+
safetensors>=0.4
|
| 10 |
+
torch>=2.5
|
| 11 |
+
torchvision>=0.20
|
| 12 |
+
transformers>=4.57
|
| 13 |
+
git+https://github.com/huggingface/diffusers.git
|
| 14 |
+
|
scripts/download_weights.py
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Download and safely extract the published HandEdit LoRA archive."""
|
| 3 |
+
|
| 4 |
+
from __future__ import annotations
|
| 5 |
+
|
| 6 |
+
import argparse
|
| 7 |
+
import hashlib
|
| 8 |
+
import json
|
| 9 |
+
import shutil
|
| 10 |
+
import zipfile
|
| 11 |
+
from pathlib import Path
|
| 12 |
+
|
| 13 |
+
from huggingface_hub import hf_hub_download
|
| 14 |
+
|
| 15 |
+
DEFAULT_REPO_ID = "HandEdit/HandEdit-LoRA"
|
| 16 |
+
ARCHIVE_NAME = "checkpoints.zip"
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
def sha256(path: Path) -> str:
|
| 20 |
+
digest = hashlib.sha256()
|
| 21 |
+
with path.open("rb") as handle:
|
| 22 |
+
for chunk in iter(lambda: handle.read(8 * 1024 * 1024), b""):
|
| 23 |
+
digest.update(chunk)
|
| 24 |
+
return digest.hexdigest()
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
def safe_extract(archive: Path, destination: Path) -> None:
|
| 28 |
+
root = destination.resolve()
|
| 29 |
+
with zipfile.ZipFile(archive) as bundle:
|
| 30 |
+
for member in bundle.infolist():
|
| 31 |
+
target = (root / member.filename).resolve()
|
| 32 |
+
if target != root and root not in target.parents:
|
| 33 |
+
raise RuntimeError(f"Unsafe archive member: {member.filename}")
|
| 34 |
+
bundle.extractall(root)
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
def main() -> None:
|
| 38 |
+
parser = argparse.ArgumentParser(
|
| 39 |
+
description="Download the sanitized HandEdit LoRA checkpoints."
|
| 40 |
+
)
|
| 41 |
+
parser.add_argument("--repo-id", default=DEFAULT_REPO_ID)
|
| 42 |
+
parser.add_argument(
|
| 43 |
+
"--output-dir",
|
| 44 |
+
type=Path,
|
| 45 |
+
default=Path("."),
|
| 46 |
+
help="Repository root where checkpoints/ will be extracted.",
|
| 47 |
+
)
|
| 48 |
+
parser.add_argument(
|
| 49 |
+
"--keep-archive",
|
| 50 |
+
action="store_true",
|
| 51 |
+
help="Keep checkpoints.zip after successful extraction.",
|
| 52 |
+
)
|
| 53 |
+
args = parser.parse_args()
|
| 54 |
+
|
| 55 |
+
output_dir = args.output_dir.expanduser().resolve()
|
| 56 |
+
output_dir.mkdir(parents=True, exist_ok=True)
|
| 57 |
+
archive = Path(
|
| 58 |
+
hf_hub_download(
|
| 59 |
+
repo_id=args.repo_id,
|
| 60 |
+
filename=ARCHIVE_NAME,
|
| 61 |
+
repo_type="model",
|
| 62 |
+
local_dir=output_dir,
|
| 63 |
+
)
|
| 64 |
+
)
|
| 65 |
+
|
| 66 |
+
manifest_path = Path(
|
| 67 |
+
hf_hub_download(
|
| 68 |
+
repo_id=args.repo_id,
|
| 69 |
+
filename="weights_manifest.json",
|
| 70 |
+
repo_type="model",
|
| 71 |
+
local_dir=output_dir,
|
| 72 |
+
)
|
| 73 |
+
)
|
| 74 |
+
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
|
| 75 |
+
expected = manifest["archive"]["sha256"]
|
| 76 |
+
actual = sha256(archive)
|
| 77 |
+
if actual != expected:
|
| 78 |
+
raise RuntimeError(
|
| 79 |
+
f"Archive checksum mismatch: expected {expected}, received {actual}"
|
| 80 |
+
)
|
| 81 |
+
|
| 82 |
+
safe_extract(archive, output_dir)
|
| 83 |
+
print(f"[OK] Extracted sanitized weights to: {output_dir / 'checkpoints'}")
|
| 84 |
+
if not args.keep_archive:
|
| 85 |
+
archive.unlink()
|
| 86 |
+
print(f"[OK] Removed downloaded archive: {archive}")
|
| 87 |
+
|
| 88 |
+
|
| 89 |
+
if __name__ == "__main__":
|
| 90 |
+
main()
|
| 91 |
+
|
scripts/infer_flux2_lora.py
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Batch image editing with FLUX.2 Klein and a Diffusers-compatible LoRA file."""
|
| 3 |
+
|
| 4 |
+
import argparse
|
| 5 |
+
from pathlib import Path
|
| 6 |
+
|
| 7 |
+
import torch
|
| 8 |
+
from PIL import Image
|
| 9 |
+
from diffusers import Flux2KleinPipeline
|
| 10 |
+
|
| 11 |
+
IMAGE_EXTS = {".jpg", ".jpeg", ".png", ".webp", ".bmp"}
|
| 12 |
+
DEFAULT_BASE = "black-forest-labs/FLUX.2-klein-base-4B"
|
| 13 |
+
DEFAULT_PROMPT = (
|
| 14 |
+
"Edit only the human hand region. Replace the human hand with a realistic Inspire robotic "
|
| 15 |
+
"hand with correct robotic finger structure and joints. Preserve the original wrist pose, "
|
| 16 |
+
"palm orientation, finger articulation, grasp geometry, and contact points with the object. "
|
| 17 |
+
"The robot hand must be kinematically feasible and physically plausible, without penetrating "
|
| 18 |
+
"the object. Keep the object pose, shape, texture, background, lighting, camera viewpoint, "
|
| 19 |
+
"and all non-hand regions unchanged."
|
| 20 |
+
)
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
def list_images(input_dir: Path, recursive: bool):
|
| 24 |
+
iterator = input_dir.rglob("*") if recursive else input_dir.iterdir()
|
| 25 |
+
return sorted(
|
| 26 |
+
path for path in iterator
|
| 27 |
+
if path.is_file() and path.suffix.lower() in IMAGE_EXTS
|
| 28 |
+
)
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
def load_lora(pipe, lora_path: str, scale: float):
|
| 32 |
+
path = Path(lora_path).expanduser()
|
| 33 |
+
adapter_name = "handedit"
|
| 34 |
+
if path.is_file():
|
| 35 |
+
pipe.load_lora_weights(
|
| 36 |
+
str(path.parent),
|
| 37 |
+
weight_name=path.name,
|
| 38 |
+
adapter_name=adapter_name,
|
| 39 |
+
)
|
| 40 |
+
else:
|
| 41 |
+
pipe.load_lora_weights(str(path), adapter_name=adapter_name)
|
| 42 |
+
pipe.set_adapters(adapter_name, adapter_weights=scale)
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
def main():
|
| 46 |
+
parser = argparse.ArgumentParser(
|
| 47 |
+
description="Batch inference for FLUX.2 Klein with a LoRA checkpoint."
|
| 48 |
+
)
|
| 49 |
+
parser.add_argument(
|
| 50 |
+
"--base",
|
| 51 |
+
default=DEFAULT_BASE,
|
| 52 |
+
help=f"FLUX.2 Klein model directory or model ID (default: {DEFAULT_BASE}).",
|
| 53 |
+
)
|
| 54 |
+
parser.add_argument(
|
| 55 |
+
"--lora",
|
| 56 |
+
default="./checkpoints/flux2/handedit_flux2_klein4b_lora.safetensors",
|
| 57 |
+
help="LoRA .safetensors file or adapter directory.",
|
| 58 |
+
)
|
| 59 |
+
parser.add_argument("--input_dir", required=True)
|
| 60 |
+
parser.add_argument("--output_dir", required=True)
|
| 61 |
+
parser.add_argument("--prompt", default=DEFAULT_PROMPT)
|
| 62 |
+
parser.add_argument("--steps", type=int, default=50)
|
| 63 |
+
parser.add_argument("--guidance_scale", type=float, default=4.0)
|
| 64 |
+
parser.add_argument("--lora_scale", type=float, default=1.0)
|
| 65 |
+
parser.add_argument("--seed", type=int, default=43)
|
| 66 |
+
parser.add_argument("--seed_mode", choices=["fixed", "increment"], default="fixed")
|
| 67 |
+
parser.add_argument("--offload", choices=["model", "sequential", "none"], default="model")
|
| 68 |
+
parser.add_argument("--local_files_only", action="store_true")
|
| 69 |
+
parser.add_argument("--suffix", default="")
|
| 70 |
+
parser.add_argument("--recursive", action="store_true")
|
| 71 |
+
parser.add_argument("--skip_existing", action="store_true")
|
| 72 |
+
args = parser.parse_args()
|
| 73 |
+
|
| 74 |
+
if not torch.cuda.is_available():
|
| 75 |
+
raise RuntimeError("CUDA is required for practical FLUX.2 inference.")
|
| 76 |
+
|
| 77 |
+
input_root = Path(args.input_dir).expanduser().resolve()
|
| 78 |
+
output_root = Path(args.output_dir).expanduser().resolve()
|
| 79 |
+
if not input_root.is_dir():
|
| 80 |
+
raise FileNotFoundError(f"Input directory not found: {input_root}")
|
| 81 |
+
output_root.mkdir(parents=True, exist_ok=True)
|
| 82 |
+
|
| 83 |
+
paths = list_images(input_root, args.recursive)
|
| 84 |
+
if not paths:
|
| 85 |
+
raise RuntimeError(f"No images found under: {input_root}")
|
| 86 |
+
|
| 87 |
+
print("[1/3] Loading FLUX.2 Klein...")
|
| 88 |
+
pipe = Flux2KleinPipeline.from_pretrained(
|
| 89 |
+
args.base,
|
| 90 |
+
torch_dtype=torch.bfloat16,
|
| 91 |
+
local_files_only=args.local_files_only,
|
| 92 |
+
)
|
| 93 |
+
|
| 94 |
+
print("[2/3] Loading LoRA adapter...")
|
| 95 |
+
load_lora(pipe, args.lora, args.lora_scale)
|
| 96 |
+
|
| 97 |
+
if args.offload == "model":
|
| 98 |
+
pipe.enable_model_cpu_offload()
|
| 99 |
+
elif args.offload == "sequential":
|
| 100 |
+
pipe.enable_sequential_cpu_offload()
|
| 101 |
+
else:
|
| 102 |
+
pipe.to("cuda")
|
| 103 |
+
|
| 104 |
+
print(f"[3/3] Processing {len(paths)} images...")
|
| 105 |
+
current_seed = args.seed
|
| 106 |
+
for index, input_path in enumerate(paths, start=1):
|
| 107 |
+
relative = input_path.relative_to(input_root)
|
| 108 |
+
output_path = output_root / relative.with_name(relative.stem + args.suffix + ".png")
|
| 109 |
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
| 110 |
+
|
| 111 |
+
if args.skip_existing and output_path.exists():
|
| 112 |
+
print(f"[{index}/{len(paths)}] SKIP {output_path}")
|
| 113 |
+
else:
|
| 114 |
+
image = Image.open(input_path).convert("RGB")
|
| 115 |
+
generator = torch.Generator("cpu").manual_seed(current_seed)
|
| 116 |
+
result = pipe(
|
| 117 |
+
prompt=args.prompt,
|
| 118 |
+
image=image,
|
| 119 |
+
num_inference_steps=args.steps,
|
| 120 |
+
guidance_scale=args.guidance_scale,
|
| 121 |
+
generator=generator,
|
| 122 |
+
).images[0]
|
| 123 |
+
result.save(output_path)
|
| 124 |
+
print(f"[{index}/{len(paths)}] OK {input_path.name} -> {output_path}")
|
| 125 |
+
|
| 126 |
+
if args.seed_mode == "increment":
|
| 127 |
+
current_seed += 1
|
| 128 |
+
|
| 129 |
+
print(f"[DONE] Results saved under: {output_root}")
|
| 130 |
+
|
| 131 |
+
|
| 132 |
+
if __name__ == "__main__":
|
| 133 |
+
main()
|
scripts/infer_longcat_lora.py
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Batch image editing with LongCat-Image-Edit and a PEFT LoRA adapter."""
|
| 3 |
+
|
| 4 |
+
from __future__ import annotations
|
| 5 |
+
|
| 6 |
+
import argparse
|
| 7 |
+
from pathlib import Path
|
| 8 |
+
|
| 9 |
+
import torch
|
| 10 |
+
from diffusers import LongCatImageEditPipeline
|
| 11 |
+
from peft import PeftModel
|
| 12 |
+
from PIL import Image
|
| 13 |
+
|
| 14 |
+
IMAGE_EXTS = {".jpg", ".jpeg", ".png", ".webp", ".bmp"}
|
| 15 |
+
DEFAULT_BASE = "meituan-longcat/LongCat-Image-Edit"
|
| 16 |
+
DEFAULT_PROMPT = (
|
| 17 |
+
"Edit only the human hand region. Replace the human hand with a realistic Inspire robotic "
|
| 18 |
+
"hand with correct robotic finger structure and joints. Preserve the original wrist pose, "
|
| 19 |
+
"palm orientation, finger articulation, grasp geometry, and contact points with the object. "
|
| 20 |
+
"The robot hand must be kinematically feasible and physically plausible, without penetrating "
|
| 21 |
+
"the object. Keep the object pose, shape, texture, background, lighting, camera viewpoint, "
|
| 22 |
+
"and all non-hand regions unchanged."
|
| 23 |
+
)
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
def list_images(input_dir: Path, recursive: bool) -> list[Path]:
|
| 27 |
+
iterator = input_dir.rglob("*") if recursive else input_dir.iterdir()
|
| 28 |
+
return sorted(
|
| 29 |
+
path
|
| 30 |
+
for path in iterator
|
| 31 |
+
if path.is_file() and path.suffix.lower() in IMAGE_EXTS
|
| 32 |
+
)
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
def apply_lora_scale(model: torch.nn.Module, scale: float) -> int:
|
| 36 |
+
"""Multiply every active PEFT LoRA layer scale by ``scale``."""
|
| 37 |
+
if scale == 1.0:
|
| 38 |
+
return 0
|
| 39 |
+
changed = 0
|
| 40 |
+
for module in model.modules():
|
| 41 |
+
scaling = getattr(module, "scaling", None)
|
| 42 |
+
if isinstance(scaling, dict):
|
| 43 |
+
for adapter_name in list(scaling):
|
| 44 |
+
scaling[adapter_name] *= scale
|
| 45 |
+
changed += 1
|
| 46 |
+
return changed
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
def main() -> None:
|
| 50 |
+
parser = argparse.ArgumentParser(
|
| 51 |
+
description="Batch inference for LongCat-Image-Edit with the HandEdit LoRA."
|
| 52 |
+
)
|
| 53 |
+
parser.add_argument(
|
| 54 |
+
"--base",
|
| 55 |
+
default=DEFAULT_BASE,
|
| 56 |
+
help=f"Base model directory or model ID (default: {DEFAULT_BASE}).",
|
| 57 |
+
)
|
| 58 |
+
parser.add_argument(
|
| 59 |
+
"--lora",
|
| 60 |
+
default="./checkpoints/longcat",
|
| 61 |
+
help="PEFT LoRA directory (default: ./checkpoints/longcat).",
|
| 62 |
+
)
|
| 63 |
+
parser.add_argument("--input_dir", required=True)
|
| 64 |
+
parser.add_argument("--output_dir", required=True)
|
| 65 |
+
parser.add_argument("--prompt", default=DEFAULT_PROMPT)
|
| 66 |
+
parser.add_argument("--negative_prompt", default="")
|
| 67 |
+
parser.add_argument("--guidance_scale", type=float, default=4.5)
|
| 68 |
+
parser.add_argument("--steps", type=int, default=50)
|
| 69 |
+
parser.add_argument("--seed", type=int, default=43)
|
| 70 |
+
parser.add_argument(
|
| 71 |
+
"--seed_mode", choices=["fixed", "increment"], default="fixed"
|
| 72 |
+
)
|
| 73 |
+
parser.add_argument("--lora_scale", type=float, default=1.0)
|
| 74 |
+
parser.add_argument(
|
| 75 |
+
"--offload", choices=["model", "sequential", "none"], default="model"
|
| 76 |
+
)
|
| 77 |
+
parser.add_argument("--local_files_only", action="store_true")
|
| 78 |
+
parser.add_argument("--suffix", default="")
|
| 79 |
+
parser.add_argument("--recursive", action="store_true")
|
| 80 |
+
parser.add_argument("--skip_existing", action="store_true")
|
| 81 |
+
args = parser.parse_args()
|
| 82 |
+
|
| 83 |
+
if not torch.cuda.is_available():
|
| 84 |
+
raise RuntimeError("CUDA is required for practical LongCat inference.")
|
| 85 |
+
if args.steps < 2:
|
| 86 |
+
raise ValueError("--steps must be at least 2")
|
| 87 |
+
if args.guidance_scale < 0:
|
| 88 |
+
raise ValueError("--guidance_scale must be non-negative")
|
| 89 |
+
|
| 90 |
+
input_root = Path(args.input_dir).expanduser().resolve()
|
| 91 |
+
output_root = Path(args.output_dir).expanduser().resolve()
|
| 92 |
+
lora_root = Path(args.lora).expanduser().resolve()
|
| 93 |
+
if not input_root.is_dir():
|
| 94 |
+
raise FileNotFoundError(f"Input directory not found: {input_root}")
|
| 95 |
+
if not lora_root.is_dir():
|
| 96 |
+
raise FileNotFoundError(f"LoRA directory not found: {lora_root}")
|
| 97 |
+
|
| 98 |
+
paths = list_images(input_root, args.recursive)
|
| 99 |
+
if not paths:
|
| 100 |
+
raise RuntimeError(f"No images found under: {input_root}")
|
| 101 |
+
output_root.mkdir(parents=True, exist_ok=True)
|
| 102 |
+
|
| 103 |
+
print("[1/3] Loading LongCat-Image-Edit...")
|
| 104 |
+
pipe = LongCatImageEditPipeline.from_pretrained(
|
| 105 |
+
args.base,
|
| 106 |
+
torch_dtype=torch.bfloat16,
|
| 107 |
+
local_files_only=args.local_files_only,
|
| 108 |
+
)
|
| 109 |
+
|
| 110 |
+
print("[2/3] Loading HandEdit LoRA...")
|
| 111 |
+
pipe.transformer = PeftModel.from_pretrained(
|
| 112 |
+
pipe.transformer,
|
| 113 |
+
str(lora_root),
|
| 114 |
+
is_trainable=False,
|
| 115 |
+
)
|
| 116 |
+
changed = apply_lora_scale(pipe.transformer, args.lora_scale)
|
| 117 |
+
if args.lora_scale != 1.0:
|
| 118 |
+
print(
|
| 119 |
+
f"[INFO] LoRA scale={args.lora_scale}; "
|
| 120 |
+
f"adjusted {changed} active LoRA layers."
|
| 121 |
+
)
|
| 122 |
+
|
| 123 |
+
if args.offload == "model":
|
| 124 |
+
pipe.enable_model_cpu_offload()
|
| 125 |
+
elif args.offload == "sequential":
|
| 126 |
+
pipe.enable_sequential_cpu_offload()
|
| 127 |
+
else:
|
| 128 |
+
pipe.to("cuda")
|
| 129 |
+
|
| 130 |
+
print(f"[3/3] Processing {len(paths)} images...")
|
| 131 |
+
current_seed = args.seed
|
| 132 |
+
for index, input_path in enumerate(paths, start=1):
|
| 133 |
+
relative = input_path.relative_to(input_root)
|
| 134 |
+
output_path = output_root / relative.with_name(
|
| 135 |
+
relative.stem + args.suffix + ".png"
|
| 136 |
+
)
|
| 137 |
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
| 138 |
+
|
| 139 |
+
if args.skip_existing and output_path.exists():
|
| 140 |
+
print(f"[{index}/{len(paths)}] SKIP {output_path}")
|
| 141 |
+
else:
|
| 142 |
+
with Image.open(input_path) as source:
|
| 143 |
+
image = source.convert("RGB")
|
| 144 |
+
generator = torch.Generator("cpu").manual_seed(current_seed)
|
| 145 |
+
result = pipe(
|
| 146 |
+
image,
|
| 147 |
+
args.prompt,
|
| 148 |
+
negative_prompt=args.negative_prompt,
|
| 149 |
+
guidance_scale=args.guidance_scale,
|
| 150 |
+
num_inference_steps=args.steps,
|
| 151 |
+
num_images_per_prompt=1,
|
| 152 |
+
generator=generator,
|
| 153 |
+
).images[0]
|
| 154 |
+
result.save(output_path)
|
| 155 |
+
print(f"[{index}/{len(paths)}] OK {input_path.name} -> {output_path}")
|
| 156 |
+
|
| 157 |
+
if args.seed_mode == "increment":
|
| 158 |
+
current_seed += 1
|
| 159 |
+
|
| 160 |
+
print(f"[DONE] Results saved under: {output_root}")
|
| 161 |
+
|
| 162 |
+
|
| 163 |
+
if __name__ == "__main__":
|
| 164 |
+
main()
|
scripts/infer_omnigen_lora.py
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Batch image editing with OmniGen-v1 and a fine-tuned LoRA checkpoint."""
|
| 3 |
+
|
| 4 |
+
import argparse
|
| 5 |
+
from pathlib import Path
|
| 6 |
+
|
| 7 |
+
from PIL import Image
|
| 8 |
+
from OmniGen import OmniGenPipeline
|
| 9 |
+
|
| 10 |
+
IMAGE_EXTS = {".jpg", ".jpeg", ".png", ".webp", ".bmp"}
|
| 11 |
+
DEFAULT_BASE = "Shitao/OmniGen-v1"
|
| 12 |
+
DEFAULT_PROMPT = (
|
| 13 |
+
"<img><|image_1|></img> Edit only the human hand region. Replace the human hand "
|
| 14 |
+
"with a realistic Inspire robotic hand with correct robotic finger structure and joints. "
|
| 15 |
+
"Preserve the original wrist pose, palm orientation, finger articulation, grasp geometry, "
|
| 16 |
+
"and contact points with the object. The robot hand must be kinematically feasible and "
|
| 17 |
+
"physically plausible, without penetrating the object. Keep the object pose, shape, texture, "
|
| 18 |
+
"background, lighting, camera viewpoint, and all non-hand regions unchanged."
|
| 19 |
+
)
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
def list_images(input_dir: Path, recursive: bool):
|
| 23 |
+
iterator = input_dir.rglob("*") if recursive else input_dir.iterdir()
|
| 24 |
+
return sorted(
|
| 25 |
+
path for path in iterator
|
| 26 |
+
if path.is_file() and path.suffix.lower() in IMAGE_EXTS
|
| 27 |
+
)
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
def target_size(image_path: Path, max_size: int):
|
| 31 |
+
with Image.open(image_path) as image:
|
| 32 |
+
width, height = image.size
|
| 33 |
+
scale = min(1.0, max_size / max(width, height))
|
| 34 |
+
width = max(16, round(width * scale / 16) * 16)
|
| 35 |
+
height = max(16, round(height * scale / 16) * 16)
|
| 36 |
+
return width, height
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
def main():
|
| 40 |
+
parser = argparse.ArgumentParser(
|
| 41 |
+
description="Batch inference for OmniGen-v1 with a LoRA checkpoint."
|
| 42 |
+
)
|
| 43 |
+
parser.add_argument(
|
| 44 |
+
"--base",
|
| 45 |
+
default=DEFAULT_BASE,
|
| 46 |
+
help=f"OmniGen-v1 model directory or model ID (default: {DEFAULT_BASE}).",
|
| 47 |
+
)
|
| 48 |
+
parser.add_argument(
|
| 49 |
+
"--lora",
|
| 50 |
+
default="./checkpoints/omnigen",
|
| 51 |
+
help="LoRA checkpoint directory (default: ./checkpoints/omnigen).",
|
| 52 |
+
)
|
| 53 |
+
parser.add_argument("--input_dir", required=True)
|
| 54 |
+
parser.add_argument("--output_dir", required=True)
|
| 55 |
+
parser.add_argument("--prompt", default=DEFAULT_PROMPT)
|
| 56 |
+
parser.add_argument("--steps", type=int, default=50)
|
| 57 |
+
parser.add_argument("--guidance_scale", type=float, default=2.5)
|
| 58 |
+
parser.add_argument("--img_guidance_scale", type=float, default=1.6)
|
| 59 |
+
parser.add_argument("--max_size", type=int, default=512)
|
| 60 |
+
parser.add_argument("--seed", type=int, default=43)
|
| 61 |
+
parser.add_argument("--seed_mode", choices=["fixed", "increment"], default="fixed")
|
| 62 |
+
parser.add_argument("--suffix", default="")
|
| 63 |
+
parser.add_argument("--recursive", action="store_true")
|
| 64 |
+
parser.add_argument("--skip_existing", action="store_true")
|
| 65 |
+
parser.add_argument("--offload_model", action="store_true")
|
| 66 |
+
args = parser.parse_args()
|
| 67 |
+
|
| 68 |
+
input_root = Path(args.input_dir).expanduser().resolve()
|
| 69 |
+
output_root = Path(args.output_dir).expanduser().resolve()
|
| 70 |
+
if not input_root.is_dir():
|
| 71 |
+
raise FileNotFoundError(f"Input directory not found: {input_root}")
|
| 72 |
+
output_root.mkdir(parents=True, exist_ok=True)
|
| 73 |
+
|
| 74 |
+
paths = list_images(input_root, args.recursive)
|
| 75 |
+
if not paths:
|
| 76 |
+
raise RuntimeError(f"No images found under: {input_root}")
|
| 77 |
+
|
| 78 |
+
print("[1/3] Loading OmniGen-v1...")
|
| 79 |
+
pipe = OmniGenPipeline.from_pretrained(args.base)
|
| 80 |
+
|
| 81 |
+
print("[2/3] Merging LoRA checkpoint...")
|
| 82 |
+
pipe.merge_lora(args.lora)
|
| 83 |
+
|
| 84 |
+
print(f"[3/3] Processing {len(paths)} images...")
|
| 85 |
+
current_seed = args.seed
|
| 86 |
+
for index, input_path in enumerate(paths, start=1):
|
| 87 |
+
relative = input_path.relative_to(input_root)
|
| 88 |
+
output_path = output_root / relative.with_name(relative.stem + args.suffix + ".png")
|
| 89 |
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
| 90 |
+
|
| 91 |
+
if args.skip_existing and output_path.exists():
|
| 92 |
+
print(f"[{index}/{len(paths)}] SKIP {output_path}")
|
| 93 |
+
else:
|
| 94 |
+
width, height = target_size(input_path, args.max_size)
|
| 95 |
+
images = pipe(
|
| 96 |
+
prompt=args.prompt,
|
| 97 |
+
input_images=[str(input_path)],
|
| 98 |
+
height=height,
|
| 99 |
+
width=width,
|
| 100 |
+
num_inference_steps=args.steps,
|
| 101 |
+
guidance_scale=args.guidance_scale,
|
| 102 |
+
img_guidance_scale=args.img_guidance_scale,
|
| 103 |
+
max_input_image_size=args.max_size,
|
| 104 |
+
separate_cfg_infer=True,
|
| 105 |
+
use_kv_cache=True,
|
| 106 |
+
offload_kv_cache=True,
|
| 107 |
+
offload_model=args.offload_model,
|
| 108 |
+
use_input_image_size_as_output=False,
|
| 109 |
+
seed=current_seed,
|
| 110 |
+
)
|
| 111 |
+
images[0].save(output_path)
|
| 112 |
+
print(f"[{index}/{len(paths)}] OK {input_path.name} -> {output_path}")
|
| 113 |
+
|
| 114 |
+
if args.seed_mode == "increment":
|
| 115 |
+
current_seed += 1
|
| 116 |
+
|
| 117 |
+
print(f"[DONE] Results saved under: {output_root}")
|
| 118 |
+
|
| 119 |
+
|
| 120 |
+
if __name__ == "__main__":
|
| 121 |
+
main()
|
scripts/infer_step1x_lora.py
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Batch image editing with the official Step1X-Edit code and a LoRA file."""
|
| 3 |
+
|
| 4 |
+
import argparse
|
| 5 |
+
import importlib.util
|
| 6 |
+
import sys
|
| 7 |
+
from pathlib import Path
|
| 8 |
+
|
| 9 |
+
import torch
|
| 10 |
+
from PIL import Image
|
| 11 |
+
|
| 12 |
+
IMAGE_EXTS = {".jpg", ".jpeg", ".png", ".webp", ".bmp"}
|
| 13 |
+
DEFAULT_PROMPT = (
|
| 14 |
+
"Edit only the human hand region. Replace the human hand with a realistic Inspire robotic "
|
| 15 |
+
"hand with correct robotic finger structure and joints. Preserve the original wrist pose, "
|
| 16 |
+
"palm orientation, finger articulation, grasp geometry, and contact points with the object. "
|
| 17 |
+
"The robot hand must be kinematically feasible and physically plausible, without penetrating "
|
| 18 |
+
"the object. Keep the object pose, shape, texture, background, lighting, camera viewpoint, "
|
| 19 |
+
"and all non-hand regions unchanged."
|
| 20 |
+
)
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
def list_images(input_dir: Path, recursive: bool):
|
| 24 |
+
iterator = input_dir.rglob("*") if recursive else input_dir.iterdir()
|
| 25 |
+
return sorted(
|
| 26 |
+
path for path in iterator
|
| 27 |
+
if path.is_file() and path.suffix.lower() in IMAGE_EXTS
|
| 28 |
+
)
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
def import_official_inference(repo_dir: Path):
|
| 32 |
+
inference_file = repo_dir / "inference.py"
|
| 33 |
+
if not inference_file.is_file():
|
| 34 |
+
raise FileNotFoundError(f"Official inference.py not found: {inference_file}")
|
| 35 |
+
|
| 36 |
+
sys.path.insert(0, str(repo_dir))
|
| 37 |
+
spec = importlib.util.spec_from_file_location("step1x_official_inference", inference_file)
|
| 38 |
+
if spec is None or spec.loader is None:
|
| 39 |
+
raise RuntimeError(f"Unable to import: {inference_file}")
|
| 40 |
+
module = importlib.util.module_from_spec(spec)
|
| 41 |
+
spec.loader.exec_module(module)
|
| 42 |
+
return module
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
def main():
|
| 46 |
+
parser = argparse.ArgumentParser(
|
| 47 |
+
description="Batch inference for Step1X-Edit v1.0/v1.1 with a LoRA checkpoint."
|
| 48 |
+
)
|
| 49 |
+
parser.add_argument("--repo_dir", required=True, help="Official Step1X-Edit repository directory.")
|
| 50 |
+
parser.add_argument(
|
| 51 |
+
"--model_dir",
|
| 52 |
+
required=True,
|
| 53 |
+
help="Directory containing the DiT checkpoint, VAE, and Qwen2.5-VL directory.",
|
| 54 |
+
)
|
| 55 |
+
parser.add_argument(
|
| 56 |
+
"--lora",
|
| 57 |
+
default="./checkpoints/step1x/inspire_step1x_r32_a16_res512.safetensors",
|
| 58 |
+
help="Step1X LoRA .safetensors file.",
|
| 59 |
+
)
|
| 60 |
+
parser.add_argument("--input_dir", required=True)
|
| 61 |
+
parser.add_argument("--output_dir", required=True)
|
| 62 |
+
parser.add_argument("--prompt", default=DEFAULT_PROMPT)
|
| 63 |
+
parser.add_argument("--version", choices=["v1.0", "v1.1"], default="v1.0")
|
| 64 |
+
parser.add_argument("--steps", type=int, default=28)
|
| 65 |
+
parser.add_argument("--cfg_guidance", type=float, default=6.0)
|
| 66 |
+
parser.add_argument("--size_level", type=int, default=512)
|
| 67 |
+
parser.add_argument("--seed", type=int, default=42)
|
| 68 |
+
parser.add_argument("--seed_mode", choices=["fixed", "increment"], default="fixed")
|
| 69 |
+
parser.add_argument("--quantized", action="store_true")
|
| 70 |
+
parser.add_argument("--offload", action="store_true")
|
| 71 |
+
parser.add_argument("--suffix", default="")
|
| 72 |
+
parser.add_argument("--recursive", action="store_true")
|
| 73 |
+
parser.add_argument("--skip_existing", action="store_true")
|
| 74 |
+
args = parser.parse_args()
|
| 75 |
+
|
| 76 |
+
if not torch.cuda.is_available():
|
| 77 |
+
raise RuntimeError("CUDA is required for practical Step1X inference.")
|
| 78 |
+
|
| 79 |
+
repo_dir = Path(args.repo_dir).expanduser().resolve()
|
| 80 |
+
model_dir = Path(args.model_dir).expanduser().resolve()
|
| 81 |
+
input_root = Path(args.input_dir).expanduser().resolve()
|
| 82 |
+
output_root = Path(args.output_dir).expanduser().resolve()
|
| 83 |
+
lora_path = Path(args.lora).expanduser().resolve()
|
| 84 |
+
|
| 85 |
+
if not input_root.is_dir():
|
| 86 |
+
raise FileNotFoundError(f"Input directory not found: {input_root}")
|
| 87 |
+
if not lora_path.is_file():
|
| 88 |
+
raise FileNotFoundError(f"LoRA file not found: {lora_path}")
|
| 89 |
+
|
| 90 |
+
ckpt_name = (
|
| 91 |
+
"step1x-edit-i1258.safetensors"
|
| 92 |
+
if args.version == "v1.0"
|
| 93 |
+
else "step1x-edit-v1p1-official.safetensors"
|
| 94 |
+
)
|
| 95 |
+
required = [
|
| 96 |
+
model_dir / ckpt_name,
|
| 97 |
+
model_dir / "vae.safetensors",
|
| 98 |
+
model_dir / "Qwen2.5-VL-7B-Instruct",
|
| 99 |
+
]
|
| 100 |
+
for path in required:
|
| 101 |
+
if not path.exists():
|
| 102 |
+
raise FileNotFoundError(f"Required Step1X component not found: {path}")
|
| 103 |
+
|
| 104 |
+
output_root.mkdir(parents=True, exist_ok=True)
|
| 105 |
+
paths = list_images(input_root, args.recursive)
|
| 106 |
+
if not paths:
|
| 107 |
+
raise RuntimeError(f"No images found under: {input_root}")
|
| 108 |
+
|
| 109 |
+
official = import_official_inference(repo_dir)
|
| 110 |
+
|
| 111 |
+
print("[1/2] Loading Step1X-Edit and LoRA...")
|
| 112 |
+
generator = official.ImageGenerator(
|
| 113 |
+
ae_path=str(model_dir / "vae.safetensors"),
|
| 114 |
+
dit_path=str(model_dir / ckpt_name),
|
| 115 |
+
qwen2vl_model_path=str(model_dir / "Qwen2.5-VL-7B-Instruct"),
|
| 116 |
+
max_length=640,
|
| 117 |
+
quantized=args.quantized,
|
| 118 |
+
offload=args.offload,
|
| 119 |
+
lora=str(lora_path),
|
| 120 |
+
mode="flash",
|
| 121 |
+
version=args.version,
|
| 122 |
+
)
|
| 123 |
+
|
| 124 |
+
print(f"[2/2] Processing {len(paths)} images...")
|
| 125 |
+
current_seed = args.seed
|
| 126 |
+
for index, input_path in enumerate(paths, start=1):
|
| 127 |
+
relative = input_path.relative_to(input_root)
|
| 128 |
+
output_path = output_root / relative.with_name(relative.stem + args.suffix + ".png")
|
| 129 |
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
| 130 |
+
|
| 131 |
+
if args.skip_existing and output_path.exists():
|
| 132 |
+
print(f"[{index}/{len(paths)}] SKIP {output_path}")
|
| 133 |
+
else:
|
| 134 |
+
ref_image = Image.open(input_path).convert("RGB")
|
| 135 |
+
result = generator.generate_image(
|
| 136 |
+
prompt=args.prompt,
|
| 137 |
+
negative_prompt="",
|
| 138 |
+
ref_images=ref_image,
|
| 139 |
+
num_steps=args.steps,
|
| 140 |
+
cfg_guidance=args.cfg_guidance,
|
| 141 |
+
seed=current_seed,
|
| 142 |
+
num_samples=1,
|
| 143 |
+
show_progress=True,
|
| 144 |
+
size_level=args.size_level,
|
| 145 |
+
)[0]
|
| 146 |
+
result.save(output_path)
|
| 147 |
+
print(f"[{index}/{len(paths)}] OK {input_path.name} -> {output_path}")
|
| 148 |
+
|
| 149 |
+
if args.seed_mode == "increment":
|
| 150 |
+
current_seed += 1
|
| 151 |
+
|
| 152 |
+
print(f"[DONE] Results saved under: {output_root}")
|
| 153 |
+
|
| 154 |
+
|
| 155 |
+
if __name__ == "__main__":
|
| 156 |
+
main()
|
scripts/verify_weights.py
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Verify local sanitized LoRA files against weights_manifest.json."""
|
| 3 |
+
|
| 4 |
+
from __future__ import annotations
|
| 5 |
+
|
| 6 |
+
import argparse
|
| 7 |
+
import hashlib
|
| 8 |
+
import json
|
| 9 |
+
from pathlib import Path
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
def sha256(path: Path) -> str:
|
| 13 |
+
digest = hashlib.sha256()
|
| 14 |
+
with path.open("rb") as handle:
|
| 15 |
+
for chunk in iter(lambda: handle.read(8 * 1024 * 1024), b""):
|
| 16 |
+
digest.update(chunk)
|
| 17 |
+
return digest.hexdigest()
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
def main() -> None:
|
| 21 |
+
parser = argparse.ArgumentParser(description="Verify HandEdit LoRA checkpoints.")
|
| 22 |
+
parser.add_argument(
|
| 23 |
+
"--root",
|
| 24 |
+
type=Path,
|
| 25 |
+
default=Path("."),
|
| 26 |
+
help="Repository root containing weights_manifest.json and checkpoints/.",
|
| 27 |
+
)
|
| 28 |
+
args = parser.parse_args()
|
| 29 |
+
|
| 30 |
+
root = args.root.expanduser().resolve()
|
| 31 |
+
manifest = json.loads(
|
| 32 |
+
(root / "weights_manifest.json").read_text(encoding="utf-8")
|
| 33 |
+
)
|
| 34 |
+
failures = []
|
| 35 |
+
for adapter in manifest["adapters"].values():
|
| 36 |
+
for item in adapter["files"]:
|
| 37 |
+
path = root / item["path"]
|
| 38 |
+
if not path.is_file():
|
| 39 |
+
failures.append(f"missing: {item['path']}")
|
| 40 |
+
continue
|
| 41 |
+
if path.stat().st_size != item["size_bytes"]:
|
| 42 |
+
failures.append(f"size mismatch: {item['path']}")
|
| 43 |
+
continue
|
| 44 |
+
if sha256(path) != item["sha256"]:
|
| 45 |
+
failures.append(f"SHA-256 mismatch: {item['path']}")
|
| 46 |
+
continue
|
| 47 |
+
print(f"[OK] {item['path']}")
|
| 48 |
+
|
| 49 |
+
if failures:
|
| 50 |
+
raise SystemExit("\n".join(failures))
|
| 51 |
+
print("[DONE] All sanitized LoRA files passed verification.")
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
if __name__ == "__main__":
|
| 55 |
+
main()
|
| 56 |
+
|
weights_manifest.json
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"repo_id": "HandEdit/HandEdit-LoRA",
|
| 3 |
+
"release_type": "sanitized_lora_adapters",
|
| 4 |
+
"archive": {
|
| 5 |
+
"path": "checkpoints.zip",
|
| 6 |
+
"size_bytes": 621076920,
|
| 7 |
+
"sha256": "9e89032b37d439205e940821f82735c790a35e5c32b4813ab985fb5ee0fdcad2"
|
| 8 |
+
},
|
| 9 |
+
"adapters": {
|
| 10 |
+
"longcat": {
|
| 11 |
+
"base_model": "meituan-longcat/LongCat-Image-Edit",
|
| 12 |
+
"files": [
|
| 13 |
+
{
|
| 14 |
+
"path": "checkpoints/longcat/adapter_config.json",
|
| 15 |
+
"size_bytes": 530,
|
| 16 |
+
"sha256": "9ec4fbd1d7c3c3d9c6eef9cbee0cadf0c1fb1ce4874fd7e0252d666bd1eb8e9a"
|
| 17 |
+
},
|
| 18 |
+
{
|
| 19 |
+
"path": "checkpoints/longcat/adapter_model.safetensors",
|
| 20 |
+
"size_bytes": 94421944,
|
| 21 |
+
"sha256": "2391face660c6e5dd5439553d96f89059c8bfa5160af89778455cf9bf8568a5e"
|
| 22 |
+
}
|
| 23 |
+
]
|
| 24 |
+
},
|
| 25 |
+
"omnigen": {
|
| 26 |
+
"base_model": "Shitao/OmniGen-v1",
|
| 27 |
+
"files": [
|
| 28 |
+
{
|
| 29 |
+
"path": "checkpoints/omnigen/adapter_config.json",
|
| 30 |
+
"size_bytes": 307,
|
| 31 |
+
"sha256": "7a285bafa03c63d6414a098f76b737fa3fb143f7257a6edf0ad0442cc412cbcc"
|
| 32 |
+
},
|
| 33 |
+
{
|
| 34 |
+
"path": "checkpoints/omnigen/adapter_model.safetensors",
|
| 35 |
+
"size_bytes": 18891496,
|
| 36 |
+
"sha256": "6863295d2829c25eeed85eb175d5d21ee5867b2536023124d9f1cd2dfce9b971"
|
| 37 |
+
}
|
| 38 |
+
]
|
| 39 |
+
},
|
| 40 |
+
"flux2": {
|
| 41 |
+
"base_model": "black-forest-labs/FLUX.2-klein-base-4B",
|
| 42 |
+
"files": [
|
| 43 |
+
{
|
| 44 |
+
"path": "checkpoints/flux2/handedit_flux2_klein4b_lora.safetensors",
|
| 45 |
+
"size_bytes": 369643968,
|
| 46 |
+
"sha256": "11a9d65f8b53a1f1de08d5bcf02c181ebc122ae79661367bdad3b5fddeac591a"
|
| 47 |
+
}
|
| 48 |
+
]
|
| 49 |
+
},
|
| 50 |
+
"step1x": {
|
| 51 |
+
"base_model": "stepfun-ai/Step1X-Edit",
|
| 52 |
+
"files": [
|
| 53 |
+
{
|
| 54 |
+
"path": "checkpoints/step1x/inspire_step1x_r32_a16_res512.safetensors",
|
| 55 |
+
"size_bytes": 306426744,
|
| 56 |
+
"sha256": "e32ef656e023b0cc503ab0a24f645a3e99ba32ce8cae3dbd0eba221b988c1156"
|
| 57 |
+
}
|
| 58 |
+
]
|
| 59 |
+
}
|
| 60 |
+
}
|
| 61 |
+
}
|