github-actions[bot] commited on
Commit
e97847e
·
0 Parent(s):

Deploy public runtime

Browse files
Files changed (3) hide show
  1. README.md +17 -0
  2. app.py +235 -0
  3. requirements.txt +13 -0
README.md ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: VC Experiment
3
+ emoji: 🎞️
4
+ colorFrom: gray
5
+ colorTo: blue
6
+ sdk: gradio
7
+ sdk_version: 6.26.0
8
+ app_file: app.py
9
+ python_version: "3.12"
10
+ startup_duration_timeout: 1h
11
+ ---
12
+
13
+ # VC Experiment
14
+
15
+ Experimental video generation interface.
16
+
17
+ Upload an input video, enter a text prompt, and select **Generate**.
app.py ADDED
@@ -0,0 +1,235 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Minimal conditioned video generation Space."""
2
+
3
+ import gc
4
+ import os
5
+ import random
6
+ import tempfile
7
+ from pathlib import Path
8
+
9
+ os.environ.setdefault("PYTORCH_ALLOC_CONF", "backend:cudaMallocAsync")
10
+ os.environ.setdefault("PYTORCH_CUDA_ALLOC_CONF", "backend:cudaMallocAsync")
11
+
12
+ import gradio as gr
13
+ import imageio.v2 as imageio
14
+ import numpy as np
15
+ import spaces
16
+ import torch
17
+ from PIL import Image, ImageOps
18
+ from diffusers import AutoencoderKLWan, WanVACEPipeline
19
+ from diffusers.schedulers.scheduling_unipc_multistep import UniPCMultistepScheduler
20
+ from diffusers.utils import export_to_video
21
+
22
+ MODEL_ID = "Wan-AI/Wan2.1-VACE-1.3B-diffusers"
23
+ TARGET_WIDTH = 832
24
+ TARGET_HEIGHT = 480
25
+ OUTPUT_FPS = 16
26
+ MAX_FRAMES = 81
27
+ MAX_SEED = 2**31 - 1
28
+
29
+ DEFAULT_NEGATIVE_PROMPT = (
30
+ "Bright tones, overexposed, static, blurred details, subtitles, style, works, paintings, "
31
+ "images, static, overall gray, worst quality, low quality, JPEG compression residue, ugly, "
32
+ "incomplete, extra fingers, poorly drawn hands, poorly drawn faces, deformed, disfigured, "
33
+ "misshapen limbs, fused fingers, still picture, messy background, three legs, many people "
34
+ "in the background, walking backwards"
35
+ )
36
+
37
+ print(f"[vc-experiment] Loading {MODEL_ID}...", flush=True)
38
+ vae = AutoencoderKLWan.from_pretrained(
39
+ MODEL_ID,
40
+ subfolder="vae",
41
+ torch_dtype=torch.float32,
42
+ )
43
+ pipe = WanVACEPipeline.from_pretrained(
44
+ MODEL_ID,
45
+ vae=vae,
46
+ torch_dtype=torch.bfloat16,
47
+ )
48
+ pipe.scheduler = UniPCMultistepScheduler.from_config(
49
+ pipe.scheduler.config,
50
+ flow_shift=3.0,
51
+ )
52
+ pipe.vae.enable_tiling()
53
+ pipe.to("cuda")
54
+ print("[vc-experiment] Model ready.", flush=True)
55
+
56
+
57
+ def _to_rgb_uint8(frame: np.ndarray) -> np.ndarray:
58
+ if frame.ndim == 2:
59
+ frame = np.repeat(frame[..., None], 3, axis=2)
60
+ elif frame.ndim == 3 and frame.shape[2] == 1:
61
+ frame = np.repeat(frame, 3, axis=2)
62
+ elif frame.ndim == 3 and frame.shape[2] >= 4:
63
+ frame = frame[..., :3]
64
+ if frame.dtype != np.uint8:
65
+ frame = np.clip(frame, 0, 255).astype(np.uint8)
66
+ return frame
67
+
68
+
69
+ def _resize_control_frame(frame: Image.Image) -> Image.Image:
70
+ return ImageOps.fit(
71
+ frame.convert("RGB"),
72
+ (TARGET_WIDTH, TARGET_HEIGHT),
73
+ method=Image.Resampling.BICUBIC,
74
+ centering=(0.5, 0.5),
75
+ )
76
+
77
+
78
+ def load_control_video(path: str) -> tuple[list[Image.Image], float]:
79
+ if not path:
80
+ raise gr.Error("Upload a control video first.")
81
+
82
+ source = Path(path)
83
+ if not source.exists():
84
+ raise gr.Error("The uploaded video is no longer available. Please upload it again.")
85
+
86
+ reader = None
87
+ try:
88
+ reader = imageio.get_reader(str(source))
89
+ metadata = reader.get_meta_data() or {}
90
+ fps = float(metadata.get("fps") or OUTPUT_FPS)
91
+ frames: list[Image.Image] = []
92
+
93
+ for index, frame in enumerate(reader):
94
+ if index >= MAX_FRAMES + 1:
95
+ break
96
+ frame = _to_rgb_uint8(np.asarray(frame))
97
+ frames.append(_resize_control_frame(Image.fromarray(frame)))
98
+ except Exception as exc:
99
+ raise gr.Error(f"Could not read the control video: {exc}") from exc
100
+ finally:
101
+ if reader is not None:
102
+ reader.close()
103
+
104
+ if not frames:
105
+ raise gr.Error("The control video contains no readable frames.")
106
+ if len(frames) > MAX_FRAMES:
107
+ raise gr.Error(
108
+ f"This Space accepts at most {MAX_FRAMES} frames. "
109
+ "Prepare a shorter input clip."
110
+ )
111
+ if len(frames) < 5:
112
+ raise gr.Error("The control video is too short. Use at least 5 frames.")
113
+ if (len(frames) - 1) % 4 != 0:
114
+ raise gr.Error(
115
+ f"The model requires 4n+1 frames; received {len(frames)}. "
116
+ "Use 5, 9, 13, ..., 81 frames."
117
+ )
118
+
119
+ return frames, fps
120
+
121
+
122
+ def _gpu_duration(control_video, prompt, steps, guidance, control_scale, seed, negative_prompt) -> int:
123
+ del control_video, prompt, guidance, control_scale, seed, negative_prompt
124
+ steps = int(steps)
125
+ return int(min(300, max(90, 30 + steps * 4)))
126
+
127
+
128
+ @spaces.GPU(duration=_gpu_duration)
129
+ def generate(
130
+ control_video: str,
131
+ prompt: str,
132
+ steps: int,
133
+ guidance: float,
134
+ control_scale: float,
135
+ seed: int,
136
+ negative_prompt: str,
137
+ ):
138
+ if not prompt or not prompt.strip():
139
+ raise gr.Error("Prompt cannot be empty.")
140
+
141
+ frames, input_fps = load_control_video(control_video)
142
+ num_frames = len(frames)
143
+ seed = int(seed)
144
+ if seed < 0:
145
+ seed = random.randint(0, MAX_SEED)
146
+
147
+ generator = torch.Generator(device="cuda").manual_seed(seed)
148
+
149
+ try:
150
+ result = pipe(
151
+ video=frames,
152
+ mask=None,
153
+ prompt=prompt.strip(),
154
+ negative_prompt=(negative_prompt or "").strip() or None,
155
+ height=TARGET_HEIGHT,
156
+ width=TARGET_WIDTH,
157
+ num_frames=num_frames,
158
+ num_inference_steps=int(steps),
159
+ guidance_scale=float(guidance),
160
+ conditioning_scale=float(control_scale),
161
+ generator=generator,
162
+ ).frames[0]
163
+
164
+ with tempfile.NamedTemporaryFile(prefix="vc_experiment_", suffix=".mp4", delete=False) as tmp:
165
+ output_path = tmp.name
166
+ export_to_video(result, output_path, fps=OUTPUT_FPS)
167
+
168
+ status = (
169
+ f"Generated {num_frames} frames at {TARGET_WIDTH}×{TARGET_HEIGHT}, {OUTPUT_FPS} fps. "
170
+ f"Input fps: {input_fps:.2f}. Seed: {seed}."
171
+ )
172
+ return output_path, status
173
+ except gr.Error:
174
+ raise
175
+ except torch.cuda.OutOfMemoryError as exc:
176
+ raise gr.Error(
177
+ "GPU memory was exhausted. Try fewer frames or fewer inference steps."
178
+ ) from exc
179
+ except Exception as exc:
180
+ raise gr.Error(f"Generation failed: {type(exc).__name__}: {exc}") from exc
181
+ finally:
182
+ gc.collect()
183
+ if torch.cuda.is_available():
184
+ torch.cuda.empty_cache()
185
+
186
+
187
+ with gr.Blocks(title="VC Experiment") as demo:
188
+ gr.Markdown(
189
+ "# VC Experiment\n"
190
+ "Upload a control video, enter a text prompt, and generate a video."
191
+ )
192
+
193
+ with gr.Row():
194
+ with gr.Column(scale=1):
195
+ control_video = gr.Video(
196
+ label="Control video",
197
+ sources=["upload"],
198
+ format="mp4",
199
+ )
200
+ prompt = gr.Textbox(
201
+ label="Prompt",
202
+ lines=4,
203
+ placeholder="Describe the desired output...",
204
+ )
205
+
206
+ with gr.Accordion("Inference settings", open=False):
207
+ steps = gr.Slider(10, 40, value=20, step=1, label="Inference steps")
208
+ guidance = gr.Slider(1.0, 10.0, value=5.0, step=0.5, label="Guidance scale")
209
+ control_scale = gr.Slider(0.0, 2.0, value=1.0, step=0.05, label="Control scale")
210
+ seed = gr.Number(value=42, precision=0, label="Seed (-1 = random)")
211
+ negative_prompt = gr.Textbox(
212
+ value=DEFAULT_NEGATIVE_PROMPT,
213
+ label="Negative prompt",
214
+ lines=4,
215
+ )
216
+
217
+ generate_button = gr.Button("Generate", variant="primary")
218
+
219
+ with gr.Column(scale=1):
220
+ result_video = gr.Video(label="Result", autoplay=True)
221
+ status = gr.Markdown()
222
+
223
+ generate_button.click(
224
+ fn=generate,
225
+ inputs=[control_video, prompt, steps, guidance, control_scale, seed, negative_prompt],
226
+ outputs=[result_video, status],
227
+ )
228
+
229
+ gr.Markdown(
230
+ "**Input:** 5–81 frames; frame count must be `4n+1`. "
231
+ "Frames are normalized to 832×480."
232
+ )
233
+
234
+ if __name__ == "__main__":
235
+ demo.queue(default_concurrency_limit=1).launch()
requirements.txt ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ZeroGPU provides a compatible CUDA-enabled PyTorch runtime.
2
+ gradio==6.26.0
3
+ spaces
4
+ diffusers==0.40.0
5
+ transformers>=4.49.0,<6
6
+ accelerate>=1.7.0
7
+ huggingface_hub>=0.34.0,<2
8
+ sentencepiece
9
+ ftfy
10
+ imageio
11
+ imageio-ffmpeg
12
+ Pillow
13
+ numpy