Spaces:
Running on Zero
Running on Zero
Qwen-Video-Edit ZeroGPU demo
Browse files- .gitattributes +4 -0
- README.md +45 -21
- app.py +338 -286
- diffsynth/core/loader/model.py +5 -7
- diffsynth/core/vram/disk_map.py +3 -3
- diffsynth/models/qwen_image_dit.py +0 -14
- examples/city_traffic_night.mp4 +3 -0
- examples/man_dancing.mp4 +3 -0
- examples/ocean_waves.mp4 +3 -0
- examples/pottery_wheel.mp4 +3 -0
- requirements.txt +6 -5
.gitattributes
CHANGED
|
@@ -35,3 +35,7 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
example1.mp4 filter=lfs diff=lfs merge=lfs -text
|
| 37 |
example2.mp4 filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
example1.mp4 filter=lfs diff=lfs merge=lfs -text
|
| 37 |
example2.mp4 filter=lfs diff=lfs merge=lfs -text
|
| 38 |
+
examples/city_traffic_night.mp4 filter=lfs diff=lfs merge=lfs -text
|
| 39 |
+
examples/man_dancing.mp4 filter=lfs diff=lfs merge=lfs -text
|
| 40 |
+
examples/ocean_waves.mp4 filter=lfs diff=lfs merge=lfs -text
|
| 41 |
+
examples/pottery_wheel.mp4 filter=lfs diff=lfs merge=lfs -text
|
README.md
CHANGED
|
@@ -4,37 +4,61 @@ emoji: 🎬
|
|
| 4 |
colorFrom: gray
|
| 5 |
colorTo: green
|
| 6 |
sdk: gradio
|
| 7 |
-
sdk_version: 6.
|
| 8 |
app_file: app.py
|
| 9 |
-
short_description: Instruction-based video editing
|
| 10 |
python_version: "3.12"
|
| 11 |
startup_duration_timeout: 1h
|
|
|
|
| 12 |
---
|
| 13 |
|
| 14 |
# Qwen-Video-Edit
|
| 15 |
|
| 16 |
-
|
|
|
|
| 17 |
|
| 18 |
-
|
|
|
|
|
|
|
|
|
|
| 19 |
|
| 20 |
-
|
| 21 |
|
| 22 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 23 |
|
| 24 |
-
|
| 25 |
-
source video → (frozen Wan2.1 VAE) → video latents
|
| 26 |
-
│
|
| 27 |
-
trainable in-projection
|
| 28 |
-
▼
|
| 29 |
-
prompt + frame-grid preview → Qwen-Image-Edit DiT (LoRA) → grid RoPE over latent frames
|
| 30 |
-
│
|
| 31 |
-
trainable out-projection
|
| 32 |
-
▼
|
| 33 |
-
edited latents → (frozen Wan2.1 VAE) → edited video
|
| 34 |
-
```
|
| 35 |
|
| 36 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 37 |
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
colorFrom: gray
|
| 5 |
colorTo: green
|
| 6 |
sdk: gradio
|
| 7 |
+
sdk_version: 6.24.0
|
| 8 |
app_file: app.py
|
| 9 |
+
short_description: Instruction-based video editing with Qwen-Image-Edit
|
| 10 |
python_version: "3.12"
|
| 11 |
startup_duration_timeout: 1h
|
| 12 |
+
license: mit
|
| 13 |
---
|
| 14 |
|
| 15 |
# Qwen-Video-Edit
|
| 16 |
|
| 17 |
+
Demo of **[Qwen-Video-Edit: Instruction-Based Video Editing by Repurposing an Image
|
| 18 |
+
Editing Model](https://huggingface.co/papers/2608.14790)** (Bai, Gandelsman, Gharbi, Huang).
|
| 19 |
|
| 20 |
+
* Paper: https://huggingface.co/papers/2608.14790
|
| 21 |
+
* Project page: https://yunpeng1998.github.io/Qwen-Video-Edit-Page
|
| 22 |
+
* Code: https://github.com/yunpeng1998/Qwen-Video-Edit
|
| 23 |
+
* Weights: https://huggingface.co/yunpeng1998/Qwen-Video-Edit (`360P/step-30000.safetensors`)
|
| 24 |
|
| 25 |
+
## What it does
|
| 26 |
|
| 27 |
+
An image-editing DiT (Qwen-Image-Edit, 20.4B) is repurposed to edit **video latents**
|
| 28 |
+
directly. The source clip is encoded by the frozen Wan 2.1 video VAE; two small trainable
|
| 29 |
+
projections bridge the 16-channel video latents into and out of the DiT's token space, and
|
| 30 |
+
the 12 latent frames are laid out as a 3×4 tile grid of one virtual image so the image
|
| 31 |
+
model's 2-D RoPE covers the whole clip. Decoding the edited tokens back through the Wan VAE
|
| 32 |
+
gives the edited video.
|
| 33 |
|
| 34 |
+
## What this Space runs
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
|
| 36 |
+
This is **stage 1** of the authors' `infer.py`, matching the released 360P checkpoint's
|
| 37 |
+
training configuration exactly (`--num_frames 45 --video_max_pixels 245760
|
| 38 |
+
--latent_mode wan_compressed --pe_mode grid`), including the flow-matching schedule with
|
| 39 |
+
`dynamic_shift_len`, the Qwen2.5-VL prompt embedding over a 3×3 preview grid of the clip,
|
| 40 |
+
and true-CFG with the norm-preserving rescale.
|
| 41 |
|
| 42 |
+
Deliberate deviations, for ZeroGPU:
|
| 43 |
+
|
| 44 |
+
* **One 45-frame (≈2.8 s) chunk per run**, at 360p — the window the checkpoint was
|
| 45 |
+
trained on. The reference script can loop over multiple chunks.
|
| 46 |
+
* **The optional Wan 2.2 denoising-enhancement pass is not run** (it needs ~80 GB of extra
|
| 47 |
+
weights on top of the editing stack). This is equivalent to the reference's
|
| 48 |
+
`--skip_enhance`.
|
| 49 |
+
* **Quantization**: the DiT transformer blocks run in fp8 (dynamic activation) and the
|
| 50 |
+
Qwen2.5-VL text encoder in int8 weight-only, via `torchao`. Entry/exit layers and both
|
| 51 |
+
trainable projections stay in bf16.
|
| 52 |
+
* Uploads are temporally resampled to 16 fps (the training frame rate) before the
|
| 53 |
+
45-frame window is taken, so higher-fps clips are not played back in slow motion.
|
| 54 |
+
* The default is 20 denoising steps (the reference default is 40) to keep a run inside a
|
| 55 |
+
reasonable ZeroGPU slice; raise it in **Advanced settings** for higher fidelity.
|
| 56 |
+
|
| 57 |
+
## Example clips
|
| 58 |
+
|
| 59 |
+
The bundled example videos come from
|
| 60 |
+
[`linoyts/repo-to-space-example-videos`](https://huggingface.co/datasets/linoyts/repo-to-space-example-videos)
|
| 61 |
+
(CC0-1.0), trimmed to the model's 45-frame window. The editing instructions are taken
|
| 62 |
+
verbatim from the authors' own `examples/prompts.txt`.
|
| 63 |
+
|
| 64 |
+
Model weights are MIT-licensed; the vendored DiffSynth-Studio code is Apache-2.0.
|
app.py
CHANGED
|
@@ -1,364 +1,416 @@
|
|
| 1 |
-
"""Qwen-Video-Edit
|
| 2 |
|
| 3 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
"""
|
| 5 |
|
| 6 |
import os
|
| 7 |
|
| 8 |
-
|
| 9 |
os.environ.setdefault("DIFFSYNTH_DOWNLOAD_SOURCE", "huggingface")
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
|
| 11 |
-
import spaces #
|
| 12 |
-
import functools
|
| 13 |
-
import io
|
| 14 |
-
import time
|
| 15 |
-
import tempfile
|
| 16 |
-
import math
|
| 17 |
-
import gc
|
| 18 |
|
| 19 |
-
import
|
| 20 |
-
import
|
| 21 |
-
|
| 22 |
-
import gradio as gr
|
| 23 |
|
| 24 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
|
| 26 |
-
#
|
| 27 |
-
import rope_patch
|
| 28 |
-
rope_patch.apply()
|
| 29 |
|
| 30 |
-
|
| 31 |
-
from diffsynth.pipelines.qwen_image import QwenImagePipeline, QwenImageUnit_PromptEmbedder
|
| 32 |
-
from safetensors.torch import load_file
|
| 33 |
|
| 34 |
-
from
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
WanToQwenProjection, QwenToWanProjection,
|
| 41 |
)
|
| 42 |
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 48 |
LATENT_MODE = "wan_compressed"
|
| 49 |
PE_MODE = "grid"
|
| 50 |
-
ZERO_COND_T =
|
| 51 |
-
NUM_INFERENCE_STEPS = 30
|
| 52 |
-
CFG_SCALE = 4.0
|
| 53 |
-
DEFAULT_SEED = 42
|
| 54 |
FPS = 16
|
| 55 |
-
|
| 56 |
-
DEVICE = "cuda"
|
| 57 |
-
CPU = "cpu"
|
| 58 |
DTYPE = torch.bfloat16
|
|
|
|
| 59 |
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
dit_sd = {k[len("pipe.dit."):]: v for k, v in state_dict.items() if k.startswith("pipe.dit.")}
|
| 67 |
-
in_proj.load_state_dict({k[len("in_proj."):]: v for k, v in state_dict.items() if k.startswith("in_proj.")})
|
| 68 |
-
out_proj.load_state_dict({k[len("out_proj."):]: v for k, v in state_dict.items() if k.startswith("out_proj.")})
|
| 69 |
-
if any("lora" in k for k in dit_sd):
|
| 70 |
-
pipe.load_lora(pipe.dit, state_dict=dit_sd, hotload=True)
|
| 71 |
-
print(f"[app] Loaded LoRA DiT weights ({len(dit_sd)} tensors) + projections.")
|
| 72 |
-
else:
|
| 73 |
-
pipe.dit.load_state_dict(dit_sd, strict=False)
|
| 74 |
-
print(f"[app] Loaded full DiT weights ({len(dit_sd)} tensors) + projections.")
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
def tensor_to_uint8_frames(video):
|
| 78 |
-
"""(C,T,H,W) [-1,1] -> list of HWC uint8 arrays."""
|
| 79 |
-
v = ((video.float().clamp(-1, 1) + 1) * 127.5).to(torch.uint8)
|
| 80 |
-
return [v[:, t].permute(1, 2, 0).cpu().numpy() for t in range(v.shape[1])]
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
def save_video(frames_uint8, path_base, fps=16):
|
| 84 |
-
"""Save frames as mp4."""
|
| 85 |
-
import imageio.v3 as iio
|
| 86 |
-
path = path_base + ".mp4"
|
| 87 |
-
iio.imwrite(path, frames_uint8, fps=fps, codec="libx264")
|
| 88 |
-
return path
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
def free_cuda():
|
| 92 |
-
"""Empty cache and collect garbage."""
|
| 93 |
-
torch.cuda.empty_cache()
|
| 94 |
-
gc.collect()
|
| 95 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 96 |
|
| 97 |
-
|
| 98 |
-
print("[app] Loading Qwen-Video-Edit pipeline "
|
| 99 |
-
"(downloading on first run, can take several minutes) ...")
|
| 100 |
-
t0 = time.time()
|
| 101 |
|
| 102 |
-
# Load DiT (Qwen-Image-Edit transformer) — goes to CUDA for ZeroGPU packing
|
| 103 |
-
model_configs = [
|
| 104 |
-
ModelConfig(
|
| 105 |
-
model_id="Qwen/Qwen-Image-Edit",
|
| 106 |
-
origin_file_pattern="transformer/diffusion_pytorch_model*.safetensors",
|
| 107 |
-
),
|
| 108 |
-
]
|
| 109 |
-
|
| 110 |
-
# Load text encoder separately — keep on CPU to save VRAM
|
| 111 |
-
text_encoder_config = ModelConfig(
|
| 112 |
-
model_id="Qwen/Qwen-Image",
|
| 113 |
-
origin_file_pattern="text_encoder/model*.safetensors",
|
| 114 |
-
)
|
| 115 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 116 |
pipe = QwenImagePipeline.from_pretrained(
|
| 117 |
-
torch_dtype=DTYPE,
|
|
|
|
|
|
|
|
|
|
|
|
|
| 118 |
tokenizer_config=ModelConfig(model_id="Qwen/Qwen-Image", origin_file_pattern="tokenizer/"),
|
| 119 |
processor_config=ModelConfig(model_id="Qwen/Qwen-Image-Edit", origin_file_pattern="processor/"),
|
| 120 |
)
|
| 121 |
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
text_encoder_configs, None
|
| 126 |
)
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 135 |
)
|
| 136 |
-
wan_vae = vae_pool.fetch_model("wan_video_vae")
|
| 137 |
-
wan_vae.to(device=CPU) # Keep on CPU
|
| 138 |
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
|
|
|
| 142 |
|
| 143 |
-
# Download and load checkpoint
|
| 144 |
-
from huggingface_hub import hf_hub_download
|
| 145 |
-
checkpoint_path = hf_hub_download(CHECKPOINT_REPO, CHECKPOINT_PATH)
|
| 146 |
-
load_checkpoint_into(pipe, in_proj, out_proj, checkpoint_path)
|
| 147 |
|
| 148 |
-
|
| 149 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 150 |
|
| 151 |
-
print(f"[app] Pipeline ready in {time.time() - t0:.0f}s.")
|
| 152 |
|
|
|
|
|
|
|
| 153 |
|
| 154 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 155 |
def edit_video(
|
| 156 |
-
|
| 157 |
-
|
|
|
|
|
|
|
|
|
|
| 158 |
seed: int = DEFAULT_SEED,
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
negative_prompt: str = " ",
|
| 162 |
progress=gr.Progress(track_tqdm=True),
|
| 163 |
):
|
| 164 |
-
"""Edit
|
| 165 |
|
| 166 |
Args:
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
num_inference_steps:
|
| 171 |
-
cfg_scale:
|
| 172 |
-
negative_prompt: Negative prompt
|
|
|
|
|
|
|
|
|
|
|
|
|
| 173 |
|
| 174 |
Returns:
|
| 175 |
-
|
| 176 |
"""
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
# Use only the first NUM_FRAMES frames
|
| 190 |
-
chunk = first_n_frames_padded(all_frames[:NUM_FRAMES], NUM_FRAMES)
|
| 191 |
-
prompt = edit_instruction
|
| 192 |
|
| 193 |
-
|
|
|
|
| 194 |
h0, w0 = chunk[0].shape[:2]
|
| 195 |
-
width, height = adaptive_dims(w0, h0,
|
| 196 |
-
|
| 197 |
-
# Prepare source video tensor
|
| 198 |
source = frames_to_tensor(chunk, width, height)
|
| 199 |
preview = build_preview_grid(chunk)
|
|
|
|
| 200 |
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
[source.to(dtype=DTYPE)], device=DEVICE, tiled=tiled
|
| 206 |
-
).to(device=DEVICE, dtype=DTYPE)
|
| 207 |
-
wan_vae.to(device=CPU)
|
| 208 |
-
free_cuda()
|
| 209 |
-
|
| 210 |
-
# --- Phase 2: Prompt embedding (load text encoder to CUDA, then free) ---
|
| 211 |
-
pipe.text_encoder.to(device=DEVICE, dtype=DTYPE)
|
| 212 |
-
emb = QwenImageUnit_PromptEmbedder().process(pipe, prompt=prompt, edit_image=preview)
|
| 213 |
-
use_cfg = cfg_scale > 1.0
|
| 214 |
-
if use_cfg:
|
| 215 |
-
neg_emb = QwenImageUnit_PromptEmbedder().process(
|
| 216 |
-
pipe, prompt=negative_prompt, edit_image=preview
|
| 217 |
)
|
| 218 |
-
pipe.text_encoder.to(device=CPU)
|
| 219 |
-
free_cuda()
|
| 220 |
-
|
| 221 |
-
# --- Phase 3: Denoising loop (only DiT + projections in VRAM) ---
|
| 222 |
-
if torch.cuda.is_available():
|
| 223 |
-
vram_alloc = torch.cuda.memory_allocated(0) / 1e9
|
| 224 |
-
vram_total = torch.cuda.get_device_properties(0).total_memory / 1e9
|
| 225 |
-
print(f"[app] Before denoising: VRAM {vram_alloc:.1f} / {vram_total:.1f} GB")
|
| 226 |
-
group = getattr(in_proj, "group", 1)
|
| 227 |
-
noise_seq_len = (
|
| 228 |
-
(ref_latents.shape[2] // group)
|
| 229 |
-
* (ref_latents.shape[3] // 2)
|
| 230 |
-
* (ref_latents.shape[4] // 2)
|
| 231 |
-
)
|
| 232 |
-
pipe.scheduler.set_timesteps(
|
| 233 |
-
num_inference_steps, dynamic_shift_len=noise_seq_len
|
| 234 |
-
)
|
| 235 |
-
gen = torch.Generator(device="cpu").manual_seed(seed)
|
| 236 |
-
latents = torch.randn(ref_latents.shape, generator=gen).to(
|
| 237 |
-
device=DEVICE, dtype=DTYPE
|
| 238 |
-
)
|
| 239 |
|
| 240 |
-
|
| 241 |
-
|
| 242 |
-
|
| 243 |
-
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 248 |
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
try:
|
| 253 |
noise_pred = forward(emb, timestep)
|
| 254 |
if use_cfg:
|
|
|
|
|
|
|
| 255 |
neg_pred = forward(neg_emb, timestep)
|
| 256 |
comb = neg_pred + cfg_scale * (noise_pred - neg_pred)
|
| 257 |
-
comb = comb * (
|
| 258 |
-
torch.norm(noise_pred, dim=1, keepdim=True)
|
| 259 |
-
/ torch.norm(comb, dim=1, keepdim=True)
|
| 260 |
-
)
|
| 261 |
noise_pred = comb
|
| 262 |
-
latents = pipe.scheduler.step(
|
| 263 |
-
|
| 264 |
-
|
| 265 |
-
|
| 266 |
-
|
| 267 |
-
|
| 268 |
-
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
|
| 272 |
-
|
| 273 |
-
|
| 274 |
-
|
| 275 |
-
|
| 276 |
-
|
| 277 |
-
|
| 278 |
-
|
| 279 |
-
|
| 280 |
-
|
| 281 |
-
|
| 282 |
-
|
| 283 |
-
|
| 284 |
-
|
| 285 |
-
|
| 286 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 287 |
|
| 288 |
-
# ---- Gradio UI ----
|
| 289 |
CSS = """
|
| 290 |
#col-container { max-width: 1100px; margin: 0 auto; }
|
| 291 |
-
.dark .gradio-container { color: var(--body-text-color); }
|
| 292 |
"""
|
| 293 |
|
| 294 |
with gr.Blocks(theme=gr.themes.Citrus(), css=CSS) as demo:
|
| 295 |
with gr.Column(elem_id="col-container"):
|
| 296 |
-
gr.Markdown("# 🎬 Qwen-Video-Edit")
|
| 297 |
gr.Markdown(
|
| 298 |
-
"
|
| 299 |
-
|
| 300 |
-
|
| 301 |
-
|
| 302 |
-
|
| 303 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 304 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 305 |
with gr.Row():
|
| 306 |
-
with gr.Column(
|
| 307 |
-
|
| 308 |
-
|
| 309 |
-
|
| 310 |
-
|
| 311 |
-
edit_instruction = gr.Textbox(
|
| 312 |
-
label="Edit Instruction",
|
| 313 |
-
placeholder="e.g., Replace the woman with robot",
|
| 314 |
lines=2,
|
| 315 |
)
|
| 316 |
-
run_btn = gr.Button("Edit
|
| 317 |
-
with gr.Column(
|
| 318 |
-
|
| 319 |
-
|
| 320 |
-
|
| 321 |
-
with gr.Accordion("Advanced
|
| 322 |
-
|
| 323 |
-
|
| 324 |
-
|
| 325 |
-
|
| 326 |
-
|
| 327 |
-
|
| 328 |
-
|
| 329 |
-
|
| 330 |
-
|
| 331 |
-
|
| 332 |
-
|
| 333 |
-
|
| 334 |
-
|
| 335 |
-
|
| 336 |
-
|
| 337 |
-
|
| 338 |
-
label="
|
| 339 |
-
|
| 340 |
-
|
| 341 |
)
|
| 342 |
|
| 343 |
gr.Examples(
|
| 344 |
-
examples=
|
| 345 |
-
|
| 346 |
-
|
| 347 |
-
|
| 348 |
-
|
| 349 |
-
|
| 350 |
-
|
| 351 |
-
outputs=[output_video, status_text],
|
| 352 |
-
fn=edit_video,
|
| 353 |
-
cache_examples=False,
|
| 354 |
-
run_on_click=True,
|
| 355 |
)
|
| 356 |
|
| 357 |
-
|
|
|
|
| 358 |
fn=edit_video,
|
| 359 |
-
inputs=[
|
| 360 |
-
outputs=[
|
| 361 |
-
api_name="edit_video",
|
| 362 |
)
|
| 363 |
|
| 364 |
-
demo.launch(mcp_server=True)
|
|
|
|
| 1 |
+
"""Qwen-Video-Edit — instruction-based video editing on ZeroGPU.
|
| 2 |
|
| 3 |
+
Stage 1 of the official pipeline (`infer.py`), run on a single 45-frame chunk:
|
| 4 |
+
|
| 5 |
+
source video -> (frozen Wan 2.1 video VAE) -> wan latents
|
| 6 |
+
-> trainable in-projection
|
| 7 |
+
-> Qwen-Image-Edit DiT (full fine-tune, grid RoPE)
|
| 8 |
+
-> trainable out-projection
|
| 9 |
+
-> (frozen Wan 2.1 video VAE) -> edited video
|
| 10 |
+
|
| 11 |
+
The optional Wan2.2 denoising-enhancement stage (~80GB of extra weights) is
|
| 12 |
+
not run here — it does not fit alongside the editing stack on ZeroGPU.
|
| 13 |
"""
|
| 14 |
|
| 15 |
import os
|
| 16 |
|
| 17 |
+
# DiffSynth's loader defaults to ModelScope; keep everything on the HF Hub.
|
| 18 |
os.environ.setdefault("DIFFSYNTH_DOWNLOAD_SOURCE", "huggingface")
|
| 19 |
+
os.environ.setdefault(
|
| 20 |
+
"DIFFSYNTH_MODEL_BASE_PATH",
|
| 21 |
+
os.path.join(os.path.dirname(os.path.abspath(__file__)), "models"),
|
| 22 |
+
)
|
| 23 |
|
| 24 |
+
import spaces # noqa: E402 -- must precede any torch / CUDA-touching import
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
|
| 26 |
+
import random # noqa: E402
|
| 27 |
+
import tempfile # noqa: E402
|
| 28 |
+
import time # noqa: E402
|
|
|
|
| 29 |
|
| 30 |
+
import gradio as gr # noqa: E402
|
| 31 |
+
import imageio.v3 as iio # noqa: E402
|
| 32 |
+
import numpy as np # noqa: E402
|
| 33 |
+
import torch # noqa: E402
|
| 34 |
+
from huggingface_hub import hf_hub_download # noqa: E402
|
| 35 |
+
from safetensors.torch import load_file # noqa: E402
|
| 36 |
+
from tqdm import tqdm # noqa: E402
|
| 37 |
|
| 38 |
+
import rope_patch # noqa: E402
|
|
|
|
|
|
|
| 39 |
|
| 40 |
+
rope_patch.apply()
|
|
|
|
|
|
|
| 41 |
|
| 42 |
+
from diffsynth.core import ModelConfig # noqa: E402
|
| 43 |
+
from diffsynth.core.vram.initialization import skip_model_initialization # noqa: E402
|
| 44 |
+
from diffsynth.models.qwen_image_dit import QwenImageDiT # noqa: E402
|
| 45 |
+
from diffsynth.pipelines.qwen_image import ( # noqa: E402
|
| 46 |
+
QwenImagePipeline,
|
| 47 |
+
QwenImageUnit_PromptEmbedder,
|
|
|
|
| 48 |
)
|
| 49 |
|
| 50 |
+
from dataset import adaptive_dims, build_preview_grid, frames_to_tensor # noqa: E402
|
| 51 |
+
from model import factorize_latent_grid, model_fn_video_tokens, num_token_groups # noqa: E402
|
| 52 |
+
from projections import QwenToWanProjection, WanToQwenProjection # noqa: E402
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
# --------------------------------------------------------------------------
|
| 56 |
+
# Checkpoint-pinned configuration (must match how the 360P model was trained;
|
| 57 |
+
# see the repo README: --num_frames 45 --video_max_pixels 245760
|
| 58 |
+
# --latent_mode wan_compressed --pe_mode grid, no --zero_cond_t).
|
| 59 |
+
# --------------------------------------------------------------------------
|
| 60 |
+
CKPT_REPO = "yunpeng1998/Qwen-Video-Edit"
|
| 61 |
+
CKPT_FILE = "360P/step-30000.safetensors"
|
| 62 |
+
NUM_FRAMES = 45
|
| 63 |
LATENT_MODE = "wan_compressed"
|
| 64 |
PE_MODE = "grid"
|
| 65 |
+
ZERO_COND_T = False
|
|
|
|
|
|
|
|
|
|
| 66 |
FPS = 16
|
|
|
|
|
|
|
|
|
|
| 67 |
DTYPE = torch.bfloat16
|
| 68 |
+
MAX_SEED = np.iinfo(np.int32).max
|
| 69 |
|
| 70 |
+
DEFAULT_STEPS = 20
|
| 71 |
+
DEFAULT_CFG = 4.0
|
| 72 |
+
DEFAULT_NEG = " "
|
| 73 |
+
DEFAULT_SEED = 42
|
| 74 |
+
NATIVE_PIXELS = 245760 # 360p — the resolution the checkpoint was trained at
|
| 75 |
+
DEFAULT_PIXELS = NATIVE_PIXELS
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 76 |
|
| 77 |
+
# Calibrated against the live Space (see README): seconds of GPU time per DiT
|
| 78 |
+
# forward at the native 360p token count, plus fixed VAE / text-encoder cost.
|
| 79 |
+
SEC_PER_FORWARD = 3.0
|
| 80 |
+
FIXED_OVERHEAD = 45.0
|
| 81 |
|
| 82 |
+
LATENT_GRID = factorize_latent_grid(num_token_groups(NUM_FRAMES, LATENT_MODE))
|
|
|
|
|
|
|
|
|
|
| 83 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 84 |
|
| 85 |
+
# --------------------------------------------------------------------------
|
| 86 |
+
# Model loading (module scope, eagerly onto CUDA — ZeroGPU streams it in)
|
| 87 |
+
# --------------------------------------------------------------------------
|
| 88 |
+
print("[boot] Loading Qwen text encoder / tokenizer / processor ...", flush=True)
|
| 89 |
+
_t0 = time.time()
|
| 90 |
pipe = QwenImagePipeline.from_pretrained(
|
| 91 |
+
torch_dtype=DTYPE,
|
| 92 |
+
device="cpu",
|
| 93 |
+
model_configs=[
|
| 94 |
+
ModelConfig(model_id="Qwen/Qwen-Image", origin_file_pattern="text_encoder/model*.safetensors"),
|
| 95 |
+
],
|
| 96 |
tokenizer_config=ModelConfig(model_id="Qwen/Qwen-Image", origin_file_pattern="tokenizer/"),
|
| 97 |
processor_config=ModelConfig(model_id="Qwen/Qwen-Image-Edit", origin_file_pattern="processor/"),
|
| 98 |
)
|
| 99 |
|
| 100 |
+
print("[boot] Loading the Wan 2.1 video VAE ...", flush=True)
|
| 101 |
+
_pool = pipe.download_and_load_models(
|
| 102 |
+
[ModelConfig(model_id="Wan-AI/Wan2.1-T2V-1.3B", origin_file_pattern="Wan2.1_VAE.pth")], None
|
|
|
|
| 103 |
)
|
| 104 |
+
wan_vae = _pool.fetch_model("wan_video_vae")
|
| 105 |
+
|
| 106 |
+
print(f"[boot] Downloading the Qwen-Video-Edit DiT checkpoint (~41GB) ...", flush=True)
|
| 107 |
+
_ckpt_path = hf_hub_download(CKPT_REPO, CKPT_FILE)
|
| 108 |
+
print("[boot] Loading the fine-tuned DiT + projections ...", flush=True)
|
| 109 |
+
_state_dict = load_file(_ckpt_path, device="cpu")
|
| 110 |
+
|
| 111 |
+
in_proj = WanToQwenProjection(16, 3072)
|
| 112 |
+
out_proj = QwenToWanProjection(16, 3072)
|
| 113 |
+
in_proj.load_state_dict({k[len("in_proj.") :]: v for k, v in _state_dict.items() if k.startswith("in_proj.")})
|
| 114 |
+
out_proj.load_state_dict({k[len("out_proj.") :]: v for k, v in _state_dict.items() if k.startswith("out_proj.")})
|
| 115 |
+
in_proj = in_proj.to(dtype=DTYPE).eval()
|
| 116 |
+
out_proj = out_proj.to(dtype=DTYPE).eval()
|
| 117 |
+
|
| 118 |
+
_dit_sd = {k[len("pipe.dit.") :]: v for k, v in _state_dict.items() if k.startswith("pipe.dit.")}
|
| 119 |
+
with skip_model_initialization():
|
| 120 |
+
dit = QwenImageDiT()
|
| 121 |
+
_missing, _unexpected = dit.load_state_dict(_dit_sd, strict=False, assign=True)
|
| 122 |
+
_still_meta = [n for n, p in dit.named_parameters() if p.is_meta]
|
| 123 |
+
if _still_meta:
|
| 124 |
+
raise RuntimeError(f"DiT parameters missing from the checkpoint: {_still_meta[:8]} ({len(_still_meta)} total)")
|
| 125 |
+
print(
|
| 126 |
+
f"[boot] DiT loaded: {len(_dit_sd)} tensors, {sum(p.numel() for p in dit.parameters()) / 1e9:.2f}B params "
|
| 127 |
+
f"(missing={len(_missing)}, unexpected={len(_unexpected)})",
|
| 128 |
+
flush=True,
|
| 129 |
+
)
|
| 130 |
+
dit = dit.eval()
|
| 131 |
+
del _state_dict, _dit_sd
|
| 132 |
+
|
| 133 |
+
pipe.dit = dit
|
| 134 |
+
pipe.to("cuda")
|
| 135 |
+
wan_vae = wan_vae.to("cuda").eval()
|
| 136 |
+
in_proj = in_proj.to("cuda")
|
| 137 |
+
out_proj = out_proj.to("cuda")
|
| 138 |
+
|
| 139 |
+
# The bf16 stack is ~57GB (20.4B DiT + 8.3B Qwen2.5-VL text encoder), which does
|
| 140 |
+
# not fit a ZeroGPU slice. fp8 dynamic-activation quantization of the DiT blocks
|
| 141 |
+
# and int8 weight-only on the text encoder bring it to ~29GB and roughly halve
|
| 142 |
+
# the DiT matmul cost on Blackwell.
|
| 143 |
+
from torchao.quantization import ( # noqa: E402
|
| 144 |
+
Float8DynamicActivationFloat8WeightConfig,
|
| 145 |
+
Int8WeightOnlyConfig,
|
| 146 |
+
quantize_,
|
| 147 |
)
|
|
|
|
|
|
|
| 148 |
|
| 149 |
+
print("[boot] Quantizing (fp8 DiT blocks / int8 text encoder) ...", flush=True)
|
| 150 |
+
quantize_(dit.transformer_blocks, Float8DynamicActivationFloat8WeightConfig())
|
| 151 |
+
quantize_(pipe.text_encoder, Int8WeightOnlyConfig())
|
| 152 |
+
print(f"[boot] Ready in {time.time() - _t0:.0f}s.", flush=True)
|
| 153 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 154 |
|
| 155 |
+
# --------------------------------------------------------------------------
|
| 156 |
+
# Helpers (ported 1:1 from the repo's infer.py / dataset.py)
|
| 157 |
+
# --------------------------------------------------------------------------
|
| 158 |
+
def tensor_to_uint8_frames(video):
|
| 159 |
+
"""(C, T, H, W) in [-1, 1] -> list of HWC uint8 frames."""
|
| 160 |
+
v = ((video.float().clamp(-1, 1) + 1) * 127.5).to(torch.uint8)
|
| 161 |
+
return [v[:, t].permute(1, 2, 0).cpu().numpy() for t in range(v.shape[1])]
|
| 162 |
|
|
|
|
| 163 |
|
| 164 |
+
def read_chunk(video_path):
|
| 165 |
+
"""Decode the source video and take the first NUM_FRAMES at ~16 fps.
|
| 166 |
|
| 167 |
+
The checkpoint was trained on 16 fps clips, so a higher-fps upload is
|
| 168 |
+
temporally subsampled (rather than blindly taking 45 consecutive frames,
|
| 169 |
+
which would play back in slow motion at the fixed 16 fps output rate).
|
| 170 |
+
"""
|
| 171 |
+
try:
|
| 172 |
+
meta = iio.immeta(video_path, plugin="pyav")
|
| 173 |
+
src_fps = float(meta.get("fps") or FPS)
|
| 174 |
+
except Exception:
|
| 175 |
+
src_fps = float(FPS)
|
| 176 |
+
if not np.isfinite(src_fps) or src_fps <= 0:
|
| 177 |
+
src_fps = float(FPS)
|
| 178 |
+
|
| 179 |
+
frames = [np.asarray(f) for f in iio.imiter(video_path, plugin="pyav")]
|
| 180 |
+
if not frames:
|
| 181 |
+
raise gr.Error("Could not decode any frames from that video.")
|
| 182 |
+
frames = [f[..., :3] if f.ndim == 3 else np.stack([f] * 3, -1) for f in frames]
|
| 183 |
+
|
| 184 |
+
idx = np.round(np.arange(NUM_FRAMES) * src_fps / FPS).astype(int)
|
| 185 |
+
idx = np.minimum(idx, len(frames) - 1)
|
| 186 |
+
return [frames[i] for i in idx]
|
| 187 |
+
|
| 188 |
+
|
| 189 |
+
def _estimate_duration(*args, **kwargs):
|
| 190 |
+
def pick(name, pos, default):
|
| 191 |
+
if name in kwargs:
|
| 192 |
+
return kwargs[name]
|
| 193 |
+
return args[pos] if len(args) > pos else default
|
| 194 |
+
|
| 195 |
+
steps = int(pick("num_inference_steps", 2, DEFAULT_STEPS))
|
| 196 |
+
cfg = float(pick("cfg_scale", 3, DEFAULT_CFG))
|
| 197 |
+
pixels = float(pick("max_pixels", 7, DEFAULT_PIXELS))
|
| 198 |
+
forwards = steps * (2 if cfg > 1.0 else 1)
|
| 199 |
+
return int(FIXED_OVERHEAD + forwards * SEC_PER_FORWARD * (pixels / NATIVE_PIXELS))
|
| 200 |
+
|
| 201 |
+
|
| 202 |
+
# --------------------------------------------------------------------------
|
| 203 |
+
# Inference
|
| 204 |
+
# --------------------------------------------------------------------------
|
| 205 |
+
@spaces.GPU(duration=_estimate_duration)
|
| 206 |
def edit_video(
|
| 207 |
+
video_path: str,
|
| 208 |
+
instruction: str,
|
| 209 |
+
num_inference_steps: int = DEFAULT_STEPS,
|
| 210 |
+
cfg_scale: float = DEFAULT_CFG,
|
| 211 |
+
negative_prompt: str = DEFAULT_NEG,
|
| 212 |
seed: int = DEFAULT_SEED,
|
| 213 |
+
randomize_seed: bool = False,
|
| 214 |
+
max_pixels: int = DEFAULT_PIXELS,
|
|
|
|
| 215 |
progress=gr.Progress(track_tqdm=True),
|
| 216 |
):
|
| 217 |
+
"""Edit the first ~2.8 seconds of a video according to a text instruction.
|
| 218 |
|
| 219 |
Args:
|
| 220 |
+
video_path: Path to the source video. Only the first 45 frames
|
| 221 |
+
(resampled to 16 fps) are edited.
|
| 222 |
+
instruction: Editing instruction, e.g. "Make it Ghibli style".
|
| 223 |
+
num_inference_steps: Flow-matching denoising steps.
|
| 224 |
+
cfg_scale: True-CFG scale. Values above 1 double the compute per step.
|
| 225 |
+
negative_prompt: Negative prompt used when cfg_scale > 1.
|
| 226 |
+
seed: Random seed for the initial noise.
|
| 227 |
+
randomize_seed: Draw a fresh random seed instead of using `seed`.
|
| 228 |
+
max_pixels: Pixel budget per frame (245760 = the native 360p training
|
| 229 |
+
resolution; lower is faster).
|
| 230 |
|
| 231 |
Returns:
|
| 232 |
+
The edited video (mp4, 16 fps) and the seed that produced it.
|
| 233 |
"""
|
| 234 |
+
if not video_path:
|
| 235 |
+
raise gr.Error("Please upload a source video.")
|
| 236 |
+
if not instruction or not instruction.strip():
|
| 237 |
+
raise gr.Error("Please write an editing instruction.")
|
| 238 |
+
|
| 239 |
+
if randomize_seed:
|
| 240 |
+
seed = random.randint(0, MAX_SEED)
|
| 241 |
+
seed = int(seed)
|
| 242 |
+
num_inference_steps = int(num_inference_steps)
|
| 243 |
+
cfg_scale = float(cfg_scale)
|
| 244 |
+
max_pixels = int(max_pixels)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 245 |
|
| 246 |
+
t_start = time.time()
|
| 247 |
+
chunk = read_chunk(video_path)
|
| 248 |
h0, w0 = chunk[0].shape[:2]
|
| 249 |
+
width, height = adaptive_dims(w0, h0, max_pixels)
|
|
|
|
|
|
|
| 250 |
source = frames_to_tensor(chunk, width, height)
|
| 251 |
preview = build_preview_grid(chunk)
|
| 252 |
+
print(f"[run] {width}x{height}x{NUM_FRAMES} | steps={num_inference_steps} cfg={cfg_scale} seed={seed}", flush=True)
|
| 253 |
|
| 254 |
+
with torch.no_grad():
|
| 255 |
+
tiled = height * width >= 700_000
|
| 256 |
+
ref_latents = wan_vae.encode([source.to(dtype=DTYPE)], device="cuda", tiled=tiled).to(
|
| 257 |
+
device="cuda", dtype=DTYPE
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 258 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 259 |
|
| 260 |
+
embedder = QwenImageUnit_PromptEmbedder()
|
| 261 |
+
emb = embedder.process(pipe, prompt=instruction, edit_image=preview)
|
| 262 |
+
use_cfg = cfg_scale > 1.0
|
| 263 |
+
neg_emb = embedder.process(pipe, prompt=negative_prompt, edit_image=preview) if use_cfg else None
|
| 264 |
+
|
| 265 |
+
group = getattr(in_proj, "group", 1)
|
| 266 |
+
noise_seq_len = (ref_latents.shape[2] // group) * (ref_latents.shape[3] // 2) * (ref_latents.shape[4] // 2)
|
| 267 |
+
pipe.scheduler.set_timesteps(num_inference_steps, dynamic_shift_len=noise_seq_len)
|
| 268 |
+
gen = torch.Generator(device="cpu").manual_seed(seed)
|
| 269 |
+
latents = torch.randn(ref_latents.shape, generator=gen).to(device="cuda", dtype=DTYPE)
|
| 270 |
+
|
| 271 |
+
def forward(e, timestep):
|
| 272 |
+
return model_fn_video_tokens(
|
| 273 |
+
dit,
|
| 274 |
+
in_proj,
|
| 275 |
+
out_proj,
|
| 276 |
+
latents=latents,
|
| 277 |
+
ref_latents=ref_latents,
|
| 278 |
+
prompt_emb=e["prompt_emb"],
|
| 279 |
+
prompt_emb_mask=e["prompt_emb_mask"],
|
| 280 |
+
timestep=timestep,
|
| 281 |
+
latent_grid=LATENT_GRID,
|
| 282 |
+
zero_cond_t=ZERO_COND_T,
|
| 283 |
+
pe_mode=PE_MODE,
|
| 284 |
+
)
|
| 285 |
|
| 286 |
+
t_denoise = time.time()
|
| 287 |
+
for pid, timestep in enumerate(tqdm(pipe.scheduler.timesteps, desc="denoise")):
|
| 288 |
+
timestep = timestep.unsqueeze(0).to(dtype=DTYPE, device="cuda")
|
|
|
|
| 289 |
noise_pred = forward(emb, timestep)
|
| 290 |
if use_cfg:
|
| 291 |
+
# True CFG with a norm-preserving rescale over the latent
|
| 292 |
+
# channel dim (exactly as in the repo's infer.py).
|
| 293 |
neg_pred = forward(neg_emb, timestep)
|
| 294 |
comb = neg_pred + cfg_scale * (noise_pred - neg_pred)
|
| 295 |
+
comb = comb * (torch.norm(noise_pred, dim=1, keepdim=True) / torch.norm(comb, dim=1, keepdim=True))
|
|
|
|
|
|
|
|
|
|
| 296 |
noise_pred = comb
|
| 297 |
+
latents = pipe.scheduler.step(noise_pred, pipe.scheduler.timesteps[pid], latents)
|
| 298 |
+
denoise_s = time.time() - t_denoise
|
| 299 |
+
|
| 300 |
+
tiled = (latents.shape[3] * 8) * (latents.shape[4] * 8) >= 700_000
|
| 301 |
+
video = wan_vae.decode(latents, device="cuda", tiled=tiled)[0].cpu()
|
| 302 |
+
|
| 303 |
+
out_path = tempfile.NamedTemporaryFile(suffix=".mp4", delete=False).name
|
| 304 |
+
iio.imwrite(out_path, np.stack(tensor_to_uint8_frames(video)), fps=FPS, codec="libx264")
|
| 305 |
+
total = time.time() - t_start
|
| 306 |
+
forwards = num_inference_steps * (2 if cfg_scale > 1.0 else 1)
|
| 307 |
+
print(
|
| 308 |
+
f"[run] done in {total:.1f}s (denoise {denoise_s:.1f}s over {forwards} forwards "
|
| 309 |
+
f"= {denoise_s / max(forwards, 1):.2f}s/forward)",
|
| 310 |
+
flush=True,
|
| 311 |
+
)
|
| 312 |
+
return out_path, seed
|
| 313 |
+
|
| 314 |
+
|
| 315 |
+
def run_example(video_path: str, instruction: str):
|
| 316 |
+
"""Run an example row: edit a bundled clip with its instruction, all other
|
| 317 |
+
settings left at their defaults."""
|
| 318 |
+
return edit_video(video_path, instruction)
|
| 319 |
+
|
| 320 |
+
|
| 321 |
+
# --------------------------------------------------------------------------
|
| 322 |
+
# UI
|
| 323 |
+
# --------------------------------------------------------------------------
|
| 324 |
+
EXAMPLES = [
|
| 325 |
+
["examples/man_dancing.mp4", "Turn the dancer into a glowing neon hologram"],
|
| 326 |
+
[
|
| 327 |
+
"examples/ocean_waves.mp4",
|
| 328 |
+
"Convert the video into a soft-focus, impressionist painting with brushstrokes "
|
| 329 |
+
"mimicking the movement of the waves and the warmth of the sunset.",
|
| 330 |
+
],
|
| 331 |
+
["examples/pottery_wheel.mp4", "Imitate the look of the Ghibli style."],
|
| 332 |
+
[
|
| 333 |
+
"examples/city_traffic_night.mp4",
|
| 334 |
+
"Convert the urban landscape into a retro-futuristic cityscape rendered in "
|
| 335 |
+
"neon-drenched 1980s synthwave style, with glowing rooftops, floating vehicles, "
|
| 336 |
+
"and chromatic halos around buildings.",
|
| 337 |
+
],
|
| 338 |
+
]
|
| 339 |
|
|
|
|
| 340 |
CSS = """
|
| 341 |
#col-container { max-width: 1100px; margin: 0 auto; }
|
|
|
|
| 342 |
"""
|
| 343 |
|
| 344 |
with gr.Blocks(theme=gr.themes.Citrus(), css=CSS) as demo:
|
| 345 |
with gr.Column(elem_id="col-container"):
|
|
|
|
| 346 |
gr.Markdown(
|
| 347 |
+
"""
|
| 348 |
+
# 🎬 Qwen-Video-Edit
|
| 349 |
+
|
| 350 |
+
**Instruction-based video editing by repurposing an image editing model.**
|
| 351 |
+
Qwen-Image-Edit's DiT edits Wan 2.1 video-VAE latents directly, bridged by two tiny
|
| 352 |
+
projections warm-started from the DiT's own input/output layers — no video-pretrained
|
| 353 |
+
transformer required.
|
| 354 |
+
|
| 355 |
+
[Paper](https://huggingface.co/papers/2608.14790) ·
|
| 356 |
+
[Project page](https://yunpeng1998.github.io/Qwen-Video-Edit-Page) ·
|
| 357 |
+
[Code](https://github.com/yunpeng1998/Qwen-Video-Edit) ·
|
| 358 |
+
[Weights](https://huggingface.co/yunpeng1998/Qwen-Video-Edit)
|
| 359 |
|
| 360 |
+
The model edits one **45-frame (≈2.8 s) chunk at 360p** per run — that is the window the
|
| 361 |
+
released checkpoint was trained on.
|
| 362 |
+
"""
|
| 363 |
+
)
|
| 364 |
with gr.Row():
|
| 365 |
+
with gr.Column():
|
| 366 |
+
video_in = gr.Video(label="Source video", height=340)
|
| 367 |
+
instruction = gr.Textbox(
|
| 368 |
+
label="Editing instruction",
|
| 369 |
+
placeholder="e.g. Make it Ghibli style.",
|
|
|
|
|
|
|
|
|
|
| 370 |
lines=2,
|
| 371 |
)
|
| 372 |
+
run_btn = gr.Button("Edit video", variant="primary")
|
| 373 |
+
with gr.Column():
|
| 374 |
+
video_out = gr.Video(label="Edited video", height=340, autoplay=True, loop=True)
|
| 375 |
+
seed_out = gr.Number(label="Seed used", interactive=False)
|
| 376 |
+
|
| 377 |
+
with gr.Accordion("Advanced settings", open=False):
|
| 378 |
+
with gr.Row():
|
| 379 |
+
steps = gr.Slider(
|
| 380 |
+
label="Denoising steps", minimum=8, maximum=40, step=1, value=DEFAULT_STEPS
|
| 381 |
+
)
|
| 382 |
+
cfg = gr.Slider(
|
| 383 |
+
label="True-CFG scale (>1 doubles the compute)",
|
| 384 |
+
minimum=1.0,
|
| 385 |
+
maximum=7.0,
|
| 386 |
+
step=0.1,
|
| 387 |
+
value=DEFAULT_CFG,
|
| 388 |
+
)
|
| 389 |
+
negative = gr.Textbox(label="Negative prompt", value=DEFAULT_NEG)
|
| 390 |
+
with gr.Row():
|
| 391 |
+
seed_in = gr.Slider(label="Seed", minimum=0, maximum=MAX_SEED, step=1, value=DEFAULT_SEED)
|
| 392 |
+
randomize = gr.Checkbox(label="Randomize seed", value=False)
|
| 393 |
+
pixels = gr.Radio(
|
| 394 |
+
label="Resolution",
|
| 395 |
+
choices=[("360p — native training resolution", NATIVE_PIXELS), ("256p — faster", 122880)],
|
| 396 |
+
value=DEFAULT_PIXELS,
|
| 397 |
)
|
| 398 |
|
| 399 |
gr.Examples(
|
| 400 |
+
examples=EXAMPLES,
|
| 401 |
+
inputs=[video_in, instruction],
|
| 402 |
+
outputs=[video_out, seed_out],
|
| 403 |
+
fn=run_example,
|
| 404 |
+
cache_examples=True,
|
| 405 |
+
cache_mode="lazy",
|
| 406 |
+
label="Examples (instructions from the authors' prompt list)",
|
|
|
|
|
|
|
|
|
|
|
|
|
| 407 |
)
|
| 408 |
|
| 409 |
+
gr.on(
|
| 410 |
+
triggers=[run_btn.click, instruction.submit],
|
| 411 |
fn=edit_video,
|
| 412 |
+
inputs=[video_in, instruction, steps, cfg, negative, seed_in, randomize, pixels],
|
| 413 |
+
outputs=[video_out, seed_out],
|
|
|
|
| 414 |
)
|
| 415 |
|
| 416 |
+
demo.launch(mcp_server=True)
|
diffsynth/core/loader/model.py
CHANGED
|
@@ -20,7 +20,7 @@ def load_model(model_class, path, config=None, torch_dtype=torch.bfloat16, devic
|
|
| 20 |
dtypes = [vram_config["offload_dtype"], vram_config["onload_dtype"], vram_config["preparing_dtype"], vram_config["computation_dtype"]]
|
| 21 |
dtype = [d for d in dtypes if d != "disk"][0]
|
| 22 |
if vram_config["offload_device"] != "disk":
|
| 23 |
-
if state_dict is None: state_dict = DiskMap(path,
|
| 24 |
if state_dict_converter is not None:
|
| 25 |
state_dict = state_dict_converter(state_dict)
|
| 26 |
else:
|
|
@@ -28,7 +28,7 @@ def load_model(model_class, path, config=None, torch_dtype=torch.bfloat16, devic
|
|
| 28 |
model.load_state_dict(state_dict, assign=True)
|
| 29 |
model = enable_vram_management(model, module_map, vram_config=vram_config, disk_map=None, vram_limit=vram_limit)
|
| 30 |
else:
|
| 31 |
-
disk_map = DiskMap(path,
|
| 32 |
model = enable_vram_management(model, module_map, vram_config=vram_config, disk_map=disk_map, vram_limit=vram_limit)
|
| 33 |
else:
|
| 34 |
# Why do we use `DiskMap`?
|
|
@@ -38,11 +38,9 @@ def load_model(model_class, path, config=None, torch_dtype=torch.bfloat16, devic
|
|
| 38 |
if state_dict is not None:
|
| 39 |
pass
|
| 40 |
elif use_disk_map:
|
| 41 |
-
|
| 42 |
-
# is intercepted by the spaces hijack to pack weights to disk.
|
| 43 |
-
state_dict = DiskMap(path, "cpu", torch_dtype=torch_dtype)
|
| 44 |
else:
|
| 45 |
-
state_dict = load_state_dict(path, torch_dtype,
|
| 46 |
# Why do we use `state_dict_converter`?
|
| 47 |
# Some models are saved in complex formats,
|
| 48 |
# and we need to convert the state dict into the appropriate format.
|
|
@@ -79,7 +77,7 @@ def load_model_with_disk_offload(model_class, path, config=None, torch_dtype=tor
|
|
| 79 |
model = model_class(**config)
|
| 80 |
if hasattr(model, "eval"):
|
| 81 |
model = model.eval()
|
| 82 |
-
disk_map = DiskMap(path,
|
| 83 |
vram_config = {
|
| 84 |
"offload_dtype": "disk",
|
| 85 |
"offload_device": "disk",
|
|
|
|
| 20 |
dtypes = [vram_config["offload_dtype"], vram_config["onload_dtype"], vram_config["preparing_dtype"], vram_config["computation_dtype"]]
|
| 21 |
dtype = [d for d in dtypes if d != "disk"][0]
|
| 22 |
if vram_config["offload_device"] != "disk":
|
| 23 |
+
if state_dict is None: state_dict = DiskMap(path, device, torch_dtype=dtype)
|
| 24 |
if state_dict_converter is not None:
|
| 25 |
state_dict = state_dict_converter(state_dict)
|
| 26 |
else:
|
|
|
|
| 28 |
model.load_state_dict(state_dict, assign=True)
|
| 29 |
model = enable_vram_management(model, module_map, vram_config=vram_config, disk_map=None, vram_limit=vram_limit)
|
| 30 |
else:
|
| 31 |
+
disk_map = DiskMap(path, device, state_dict_converter=state_dict_converter)
|
| 32 |
model = enable_vram_management(model, module_map, vram_config=vram_config, disk_map=disk_map, vram_limit=vram_limit)
|
| 33 |
else:
|
| 34 |
# Why do we use `DiskMap`?
|
|
|
|
| 38 |
if state_dict is not None:
|
| 39 |
pass
|
| 40 |
elif use_disk_map:
|
| 41 |
+
state_dict = DiskMap(path, device, torch_dtype=torch_dtype)
|
|
|
|
|
|
|
| 42 |
else:
|
| 43 |
+
state_dict = load_state_dict(path, torch_dtype, device)
|
| 44 |
# Why do we use `state_dict_converter`?
|
| 45 |
# Some models are saved in complex formats,
|
| 46 |
# and we need to convert the state dict into the appropriate format.
|
|
|
|
| 77 |
model = model_class(**config)
|
| 78 |
if hasattr(model, "eval"):
|
| 79 |
model = model.eval()
|
| 80 |
+
disk_map = DiskMap(path, device, state_dict_converter=state_dict_converter)
|
| 81 |
vram_config = {
|
| 82 |
"offload_dtype": "disk",
|
| 83 |
"offload_device": "disk",
|
diffsynth/core/vram/disk_map.py
CHANGED
|
@@ -47,13 +47,13 @@ class DiskMap:
|
|
| 47 |
if len(self.files) == 0:
|
| 48 |
for path in self.path:
|
| 49 |
if path.endswith(".safetensors"):
|
| 50 |
-
self.files.append(safe_open(path, framework="pt", device=
|
| 51 |
else:
|
| 52 |
-
self.files.append(SafetensorsCompatibleBinaryLoader(path, device=
|
| 53 |
else:
|
| 54 |
for i, path in enumerate(self.path):
|
| 55 |
if path.endswith(".safetensors"):
|
| 56 |
-
self.files[i] = safe_open(path, framework="pt", device=
|
| 57 |
self.num_params = 0
|
| 58 |
|
| 59 |
def __getitem__(self, name):
|
|
|
|
| 47 |
if len(self.files) == 0:
|
| 48 |
for path in self.path:
|
| 49 |
if path.endswith(".safetensors"):
|
| 50 |
+
self.files.append(safe_open(path, framework="pt", device=str(self.device)))
|
| 51 |
else:
|
| 52 |
+
self.files.append(SafetensorsCompatibleBinaryLoader(path, device=self.device))
|
| 53 |
else:
|
| 54 |
for i, path in enumerate(self.path):
|
| 55 |
if path.endswith(".safetensors"):
|
| 56 |
+
self.files[i] = safe_open(path, framework="pt", device=str(self.device))
|
| 57 |
self.num_params = 0
|
| 58 |
|
| 59 |
def __getitem__(self, name):
|
diffsynth/models/qwen_image_dit.py
CHANGED
|
@@ -22,12 +22,6 @@ try:
|
|
| 22 |
except ModuleNotFoundError:
|
| 23 |
FLASH_ATTN_3_AVAILABLE = False
|
| 24 |
|
| 25 |
-
try:
|
| 26 |
-
from flash_attn import flash_attn_func as flash_attn_2_func
|
| 27 |
-
FLASH_ATTN_2_AVAILABLE = True
|
| 28 |
-
except ModuleNotFoundError:
|
| 29 |
-
FLASH_ATTN_2_AVAILABLE = False
|
| 30 |
-
|
| 31 |
|
| 32 |
def qwen_image_flash_attention(q: torch.Tensor, k: torch.Tensor, v: torch.Tensor, num_heads: int, attention_mask = None, enable_fp8_attention: bool = False):
|
| 33 |
if FLASH_ATTN_3_AVAILABLE and attention_mask is None:
|
|
@@ -51,14 +45,6 @@ def qwen_image_flash_attention(q: torch.Tensor, k: torch.Tensor, v: torch.Tensor
|
|
| 51 |
x = x[0]
|
| 52 |
x = x.to(origin_dtype) * v_std
|
| 53 |
x = rearrange(x, "b s n d -> b s (n d)", n=num_heads)
|
| 54 |
-
elif FLASH_ATTN_2_AVAILABLE and attention_mask is None:
|
| 55 |
-
q = rearrange(q, "b n s d -> b s n d", n=num_heads)
|
| 56 |
-
k = rearrange(k, "b n s d -> b s n d", n=num_heads)
|
| 57 |
-
v = rearrange(v, "b n s d -> b s n d", n=num_heads)
|
| 58 |
-
x = flash_attn_2_func(q, k, v)
|
| 59 |
-
if isinstance(x, tuple):
|
| 60 |
-
x = x[0]
|
| 61 |
-
x = rearrange(x, "b s n d -> b s (n d)", n=num_heads)
|
| 62 |
else:
|
| 63 |
x = torch.nn.functional.scaled_dot_product_attention(q, k, v, attn_mask=attention_mask)
|
| 64 |
x = rearrange(x, "b n s d -> b s (n d)", n=num_heads)
|
|
|
|
| 22 |
except ModuleNotFoundError:
|
| 23 |
FLASH_ATTN_3_AVAILABLE = False
|
| 24 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
|
| 26 |
def qwen_image_flash_attention(q: torch.Tensor, k: torch.Tensor, v: torch.Tensor, num_heads: int, attention_mask = None, enable_fp8_attention: bool = False):
|
| 27 |
if FLASH_ATTN_3_AVAILABLE and attention_mask is None:
|
|
|
|
| 45 |
x = x[0]
|
| 46 |
x = x.to(origin_dtype) * v_std
|
| 47 |
x = rearrange(x, "b s n d -> b s (n d)", n=num_heads)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 48 |
else:
|
| 49 |
x = torch.nn.functional.scaled_dot_product_attention(q, k, v, attn_mask=attention_mask)
|
| 50 |
x = rearrange(x, "b n s d -> b s (n d)", n=num_heads)
|
examples/city_traffic_night.mp4
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:ed5ce2a6c4c7b951ff34d5d9ae282080c2f19d3864b5efa4ccc79b1ce9092b2c
|
| 3 |
+
size 335551
|
examples/man_dancing.mp4
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:6061764e56dd8cb6220894c254a639f0547869ea2609a9c971ee7dc9940fcc10
|
| 3 |
+
size 333527
|
examples/ocean_waves.mp4
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:477dfe9c68d1ba5525320c07e597963d657ed14471ece6343fc667f865c209a5
|
| 3 |
+
size 1366054
|
examples/pottery_wheel.mp4
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:4fbb21011083c2a5e7f417dcce6aa92a54d124b2d4e2d926476875df6d4850fa
|
| 3 |
+
size 380123
|
requirements.txt
CHANGED
|
@@ -1,7 +1,8 @@
|
|
| 1 |
-
torch
|
| 2 |
torchvision
|
|
|
|
| 3 |
transformers
|
| 4 |
accelerate
|
|
|
|
| 5 |
safetensors
|
| 6 |
einops
|
| 7 |
imageio
|
|
@@ -9,9 +10,9 @@ imageio[ffmpeg]
|
|
| 9 |
av
|
| 10 |
numpy
|
| 11 |
Pillow
|
| 12 |
-
|
| 13 |
tqdm
|
|
|
|
|
|
|
|
|
|
| 14 |
sentencepiece
|
| 15 |
-
diffusers
|
| 16 |
-
peft
|
| 17 |
-
https://huggingface.co/datasets/multimodalart/zerogpu-blackwell-wheels/resolve/main/wheels/pt211-cu130-cp312/flash_attn-2.8.3-cp312-cp312-linux_x86_64.whl
|
|
|
|
|
|
|
| 1 |
torchvision
|
| 2 |
+
torchao
|
| 3 |
transformers
|
| 4 |
accelerate
|
| 5 |
+
peft
|
| 6 |
safetensors
|
| 7 |
einops
|
| 8 |
imageio
|
|
|
|
| 10 |
av
|
| 11 |
numpy
|
| 12 |
Pillow
|
| 13 |
+
pandas
|
| 14 |
tqdm
|
| 15 |
+
modelscope
|
| 16 |
+
ftfy
|
| 17 |
+
regex
|
| 18 |
sentencepiece
|
|
|
|
|
|
|
|
|