henrybit commited on
Commit
b653e58
·
verified ·
1 Parent(s): 7e69039

Build HunyuanVideo-Avatar ZeroGPU evaluation app

Browse files
Files changed (5) hide show
  1. .gitignore +5 -0
  2. README.md +35 -6
  3. app.py +293 -0
  4. packages.txt +8 -0
  5. requirements.txt +27 -0
.gitignore ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ HunyuanVideo-Avatar/
2
+ weights/
3
+ outputs/
4
+ __pycache__/
5
+ *.pyc
README.md CHANGED
@@ -1,13 +1,42 @@
1
  ---
2
  title: HunyuanVideo Avatar Test
3
- emoji: 🏃
4
- colorFrom: blue
5
- colorTo: green
6
  sdk: gradio
7
- sdk_version: 6.22.0
8
- python_version: '3.13'
9
  app_file: app.py
 
 
10
  pinned: false
 
 
 
11
  ---
12
 
13
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
  title: HunyuanVideo Avatar Test
3
+ emoji: 🎭
4
+ colorFrom: red
5
+ colorTo: yellow
6
  sdk: gradio
7
+ sdk_version: 5.49.1
 
8
  app_file: app.py
9
+ pytorch_version: 2.8.0
10
+ python_version: "3.10"
11
  pinned: false
12
+ license: other
13
+ suggested_hardware: zero-a10g
14
+ short_description: Test cartoon avatars with HunyuanVideo-Avatar
15
  ---
16
 
17
+ # HunyuanVideo-Avatar ZeroGPU Evaluation Space
18
+
19
+ Evaluation app for the official
20
+ [Tencent HunyuanVideo-Avatar](https://github.com/Tencent-Hunyuan/HunyuanVideo-Avatar)
21
+ single-GPU inference path.
22
+
23
+ ## Evaluation profile
24
+
25
+ - One avatar image + one speech audio file + an English scene prompt
26
+ - Official FP8 checkpoint
27
+ - 704px inference profile
28
+ - 49 / 73 / 97 / 129 frame test lengths at 25fps
29
+ - 50 inference steps with DeepCache
30
+ - CPU group offload
31
+ - `@spaces.GPU(size="xlarge")` only on the generation function
32
+
33
+ The upstream repository is approximately 80.8GB. This Space downloads only the
34
+ required FP8 and inference assets (approximately 45GB) and excludes the full
35
+ precision transformer plus duplicate TensorFlow, Flax, and PyTorch model files.
36
+
37
+ The first request can take a long time because the weights are downloaded lazily.
38
+ HunyuanVideo-Avatar is a very large model; the 49-frame mode is recommended for
39
+ initial compatibility and visual-quality testing.
40
+
41
+ Use only images and audio you have permission to process. Clearly disclose AI
42
+ generated or altered media when publishing the result.
app.py ADDED
@@ -0,0 +1,293 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ import csv
3
+ import gc
4
+ import os
5
+ import random
6
+ import shutil
7
+ import subprocess
8
+ import sys
9
+ import uuid
10
+ from pathlib import Path
11
+
12
+ import gradio as gr
13
+ import spaces
14
+ import torch
15
+ from huggingface_hub import snapshot_download
16
+
17
+
18
+ ROOT = Path(__file__).resolve().parent
19
+ SOURCE_DIR = ROOT / "HunyuanVideo-Avatar"
20
+ WEIGHTS_DIR = ROOT / "weights"
21
+ OUTPUT_DIR = ROOT / "outputs"
22
+ SOURCE_REPO = "https://github.com/Tencent-Hunyuan/HunyuanVideo-Avatar.git"
23
+ MODEL_REPO = "tencent/HunyuanVideo-Avatar"
24
+ FPS = 25
25
+ FRAME_OPTIONS = {
26
+ "约 2 秒(49 帧,推荐首测)": 49,
27
+ "约 3 秒(73 帧)": 73,
28
+ "约 4 秒(97 帧)": 97,
29
+ "约 5 秒(129 帧,官方配置)": 129,
30
+ }
31
+
32
+ os.environ.setdefault("GRADIO_SSR_MODE", "0")
33
+ os.environ.setdefault("GRADIO_ANALYTICS_ENABLED", "False")
34
+ os.environ.setdefault("TOKENIZERS_PARALLELISM", "false")
35
+ os.environ.setdefault("PYTORCH_CUDA_ALLOC_CONF", "expandable_segments:True")
36
+ os.environ.setdefault("HF_HUB_ENABLE_HF_TRANSFER", "1")
37
+
38
+
39
+ def ensure_source():
40
+ if not (SOURCE_DIR / "hymm_sp" / "sample_gpu_poor.py").is_file():
41
+ print("[SETUP] Cloning official HunyuanVideo-Avatar source...", flush=True)
42
+ subprocess.run(
43
+ ["git", "clone", "--depth", "1", SOURCE_REPO, str(SOURCE_DIR)],
44
+ check=True,
45
+ )
46
+
47
+ # The official `--infer-min` path hard-codes 129 frames. Make it honor the
48
+ # requested evaluation length so a Space can run short, lower-cost tests.
49
+ sample_file = SOURCE_DIR / "hymm_sp" / "sample_gpu_poor.py"
50
+ source = sample_file.read_text(encoding="utf-8")
51
+ patched = source.replace(
52
+ 'batch["audio_len"][0] = 129',
53
+ 'batch["audio_len"][0] = args.sample_n_frames',
54
+ )
55
+ if patched != source:
56
+ sample_file.write_text(patched, encoding="utf-8")
57
+ print("[SETUP] Patched infer-min to honor --sample-n-frames", flush=True)
58
+
59
+
60
+ ensure_source()
61
+ OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
62
+
63
+
64
+ MODEL_PATTERNS = [
65
+ "ckpts/config.json",
66
+ "ckpts/det_align/detface.pt",
67
+ "ckpts/hunyuan-video-t2v-720p/transformers/mp_rank_00_model_states_fp8.pt",
68
+ "ckpts/hunyuan-video-t2v-720p/transformers/mp_rank_00_model_states_fp8_map.pt",
69
+ "ckpts/hunyuan-video-t2v-720p/vae/config.json",
70
+ "ckpts/hunyuan-video-t2v-720p/vae/pytorch_model.pt",
71
+ "ckpts/llava_llama_image/*.json",
72
+ "ckpts/llava_llama_image/*.safetensors",
73
+ "ckpts/llava_llama_image/*.model",
74
+ "ckpts/text_encoder_2/config.json",
75
+ "ckpts/text_encoder_2/model.safetensors",
76
+ "ckpts/text_encoder_2/*.json",
77
+ "ckpts/text_encoder_2/*.txt",
78
+ "ckpts/whisper-tiny/config.json",
79
+ "ckpts/whisper-tiny/model.safetensors",
80
+ "ckpts/whisper-tiny/preprocessor_config.json",
81
+ ]
82
+
83
+
84
+ def ensure_weights():
85
+ checkpoint = (
86
+ WEIGHTS_DIR
87
+ / "ckpts/hunyuan-video-t2v-720p/transformers/mp_rank_00_model_states_fp8.pt"
88
+ )
89
+ if checkpoint.is_file():
90
+ print(f"[MODEL CACHE] FP8 checkpoint ready: {checkpoint}", flush=True)
91
+ return checkpoint
92
+
93
+ WEIGHTS_DIR.mkdir(parents=True, exist_ok=True)
94
+ print(
95
+ "[MODEL DOWNLOAD START] repo=tencent/HunyuanVideo-Avatar, "
96
+ "profile=FP8-minimal, expected_size≈45GB",
97
+ flush=True,
98
+ )
99
+ snapshot_download(
100
+ repo_id=MODEL_REPO,
101
+ local_dir=WEIGHTS_DIR,
102
+ allow_patterns=MODEL_PATTERNS,
103
+ )
104
+ if not checkpoint.is_file():
105
+ raise RuntimeError("FP8 checkpoint download did not complete")
106
+ print(f"[MODEL DOWNLOAD DONE] checkpoint={checkpoint}", flush=True)
107
+ return checkpoint
108
+
109
+
110
+ def estimate_gpu_duration(_image, _audio, _prompt, frame_profile, *_args):
111
+ frames = FRAME_OPTIONS.get(frame_profile, 49)
112
+ # Includes first-run model download and CPU-offloaded inference.
113
+ return max(900, min(3600, 1200 + frames * 16))
114
+
115
+
116
+ def normalize_media(image_path, audio_path, work_dir):
117
+ image_target = work_dir / "character.png"
118
+ audio_target = work_dir / "speech.wav"
119
+ shutil.copy2(image_path, image_target)
120
+ subprocess.run(
121
+ [
122
+ "ffmpeg", "-y", "-i", str(audio_path), "-ac", "1", "-ar", "16000",
123
+ "-c:a", "pcm_s16le", str(audio_target),
124
+ ],
125
+ check=True,
126
+ stdout=subprocess.DEVNULL,
127
+ stderr=subprocess.PIPE,
128
+ )
129
+ return image_target, audio_target
130
+
131
+
132
+ @spaces.GPU(size="xlarge", duration=estimate_gpu_duration)
133
+ def generate(image, audio, prompt, frame_profile, seed, progress=gr.Progress()):
134
+ if not image:
135
+ raise gr.Error("请上传一张角色图片")
136
+ if not audio:
137
+ raise gr.Error("请上传驱动语音")
138
+
139
+ frames = FRAME_OPTIONS.get(frame_profile, 49)
140
+ actual_seed = random.randint(0, 2**31 - 1) if int(seed) < 0 else int(seed)
141
+ job_id = uuid.uuid4().hex
142
+ job_dir = OUTPUT_DIR / job_id
143
+ result_dir = job_dir / "result"
144
+ job_dir.mkdir(parents=True, exist_ok=True)
145
+ result_dir.mkdir(parents=True, exist_ok=True)
146
+
147
+ try:
148
+ progress(0.02, desc="检查并下载官方 FP8 模型(首��约 45GB)...")
149
+ checkpoint = ensure_weights()
150
+ progress(0.12, desc="预处理图片和音频...")
151
+ image_path, audio_path = normalize_media(image, audio, job_dir)
152
+
153
+ meta_path = job_dir / "input.csv"
154
+ safe_prompt = (prompt or "A cartoon character speaks to the camera.").strip()
155
+ with meta_path.open("w", encoding="utf-8-sig", newline="") as handle:
156
+ writer = csv.DictWriter(
157
+ handle,
158
+ fieldnames=["videoid", "image", "audio", "prompt", "fps"],
159
+ )
160
+ writer.writeheader()
161
+ writer.writerow(
162
+ {
163
+ "videoid": job_id,
164
+ "image": str(image_path),
165
+ "audio": str(audio_path),
166
+ "prompt": safe_prompt,
167
+ "fps": FPS,
168
+ }
169
+ )
170
+
171
+ command = [
172
+ sys.executable,
173
+ str(SOURCE_DIR / "hymm_sp" / "sample_gpu_poor.py"),
174
+ "--input", str(meta_path),
175
+ "--ckpt", str(checkpoint),
176
+ "--sample-n-frames", str(frames),
177
+ "--seed", str(actual_seed),
178
+ "--image-size", "704",
179
+ "--cfg-scale", "7.5",
180
+ "--infer-steps", "50",
181
+ "--use-deepcache", "1",
182
+ "--flow-shift-eval-video", "5.0",
183
+ "--save-path", str(result_dir),
184
+ "--use-fp8",
185
+ "--cpu-offload",
186
+ "--infer-min",
187
+ ]
188
+ env = os.environ.copy()
189
+ env.update(
190
+ {
191
+ "MODEL_BASE": str(WEIGHTS_DIR),
192
+ "CPU_OFFLOAD": "1",
193
+ "DISABLE_SP": "1",
194
+ "PYTHONPATH": str(SOURCE_DIR),
195
+ }
196
+ )
197
+ print("[INFERENCE START] " + " ".join(command), flush=True)
198
+ progress(0.18, desc="加载 FP8 模型并生成视频,可能需要较长时间...")
199
+ process = subprocess.run(
200
+ command,
201
+ cwd=SOURCE_DIR,
202
+ env=env,
203
+ text=True,
204
+ stdout=subprocess.PIPE,
205
+ stderr=subprocess.STDOUT,
206
+ timeout=3500,
207
+ )
208
+ print(process.stdout, flush=True)
209
+ if process.returncode != 0:
210
+ tail = "\n".join(process.stdout.splitlines()[-25:])
211
+ raise RuntimeError(f"官方推理进程退出码 {process.returncode}\n{tail}")
212
+
213
+ output = result_dir / f"{job_id}_audio.mp4"
214
+ if not output.is_file():
215
+ candidates = sorted(result_dir.glob("*_audio.mp4"))
216
+ if not candidates:
217
+ raise RuntimeError("推理完成,但没有找到带音频的 MP4 输出")
218
+ output = candidates[-1]
219
+
220
+ progress(1.0, desc="生成完成")
221
+ info = (
222
+ f"完成:{frames} 帧 / {FPS}fps(约 {frames / FPS:.1f} 秒),"
223
+ f"704px,50 steps,FP8 + CPU offload。"
224
+ )
225
+ return str(output), actual_seed, info
226
+ except subprocess.TimeoutExpired as exc:
227
+ raise gr.Error("生成超过 ZeroGPU 最长执行时间,请改用 49 帧重试") from exc
228
+ except gr.Error:
229
+ raise
230
+ except Exception as exc:
231
+ print(f"[ERROR] {exc}", flush=True)
232
+ raise gr.Error(f"生成失败:{exc}") from exc
233
+ finally:
234
+ gc.collect()
235
+ if torch.cuda.is_available():
236
+ torch.cuda.empty_cache()
237
+
238
+
239
+ with gr.Blocks(title="HunyuanVideo-Avatar 卡通数字人测试") as demo:
240
+ gr.Markdown(
241
+ """
242
+ # HunyuanVideo-Avatar 卡通数字人测试
243
+ 上传一张卡通/3D/拟人角色图片和一段语音,评估角色一致性、口型、表情和身体动作。
244
+
245
+ **首次运行需要下载约 45GB 官方权重。建议先选 49 帧短片。**
246
+ """
247
+ )
248
+ with gr.Row():
249
+ with gr.Column():
250
+ image_input = gr.Image(label="角色图片", type="filepath")
251
+ audio_input = gr.Audio(label="驱动语音", type="filepath")
252
+ prompt_input = gr.Textbox(
253
+ label="英文画面描述",
254
+ value="A cute cartoon character speaks naturally to the camera with subtle gestures.",
255
+ lines=3,
256
+ info="描述角色、构图、背景和期望动作;不要写与原图冲突的外观。",
257
+ )
258
+ frame_input = gr.Dropdown(
259
+ choices=list(FRAME_OPTIONS),
260
+ value=list(FRAME_OPTIONS)[0],
261
+ label="测试时长",
262
+ )
263
+ seed_input = gr.Number(label="随机种子(-1 为随机)", value=-1, precision=0)
264
+ generate_button = gr.Button("生成测试视频", variant="primary")
265
+ with gr.Column():
266
+ video_output = gr.Video(label="生成结果")
267
+ seed_output = gr.Number(label="实际种子", precision=0)
268
+ info_output = gr.Textbox(label="生成信息")
269
+
270
+ gr.Markdown(
271
+ """
272
+ ### 素材建议
273
+ - 单个角色、脸部清晰;正面或轻微侧脸更稳定。
274
+ - 半身或全身卡通图都可���试,避免文字、水印和多人画面。
275
+ - 语音尽量清晰、无背景音乐;当前测试输出最多约 5 秒。
276
+ - 官方模型很大,ZeroGPU 冷启动与 CPU offload 都会显著增加等待时间。
277
+ """
278
+ )
279
+
280
+ generate_button.click(
281
+ fn=generate,
282
+ inputs=[image_input, audio_input, prompt_input, frame_input, seed_input],
283
+ outputs=[video_output, seed_output, info_output],
284
+ api_name="generate",
285
+ )
286
+
287
+
288
+ if __name__ == "__main__":
289
+ demo.queue(default_concurrency_limit=1, max_size=8).launch(
290
+ server_name="0.0.0.0",
291
+ server_port=7860,
292
+ show_error=True,
293
+ )
packages.txt ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ ffmpeg
2
+ git
3
+ git-lfs
4
+ libgl1
5
+ libglib2.0-0
6
+ libsm6
7
+ libxext6
8
+ libxrender1
requirements.txt ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ torch==2.8.0
2
+ torchvision==0.23.0
3
+ torchaudio==2.8.0
4
+ spaces>=0.42.0
5
+ gradio==5.49.1
6
+ huggingface_hub[hf_transfer]>=0.30.2
7
+ diffusers==0.33.0
8
+ transformers>=4.50.0,<5
9
+ accelerate==1.1.1
10
+ pandas==2.0.3
11
+ numpy==1.26.4
12
+ opencv-python-headless==4.10.0.84
13
+ einops==0.7.0
14
+ tqdm==4.66.2
15
+ loguru==0.7.2
16
+ imageio==2.34.0
17
+ imageio-ffmpeg==0.5.1
18
+ safetensors>=0.4.3
19
+ decord==0.6.0
20
+ librosa==0.11.0
21
+ scikit-video==1.1.11
22
+ scipy==1.14.1
23
+ scikit-image==0.24.0
24
+ Pillow>=10.2.0
25
+ sentencepiece
26
+ protobuf
27
+ ffmpeg-python