Spaces:
Running on Zero
Running on Zero
File size: 10,603 Bytes
b653e58 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 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 289 290 291 292 293 294 | # -*- coding: utf-8 -*-
import csv
import gc
import os
import random
import shutil
import subprocess
import sys
import uuid
from pathlib import Path
import gradio as gr
import spaces
import torch
from huggingface_hub import snapshot_download
ROOT = Path(__file__).resolve().parent
SOURCE_DIR = ROOT / "HunyuanVideo-Avatar"
WEIGHTS_DIR = ROOT / "weights"
OUTPUT_DIR = ROOT / "outputs"
SOURCE_REPO = "https://github.com/Tencent-Hunyuan/HunyuanVideo-Avatar.git"
MODEL_REPO = "tencent/HunyuanVideo-Avatar"
FPS = 25
FRAME_OPTIONS = {
"约 2 秒(49 帧,推荐首测)": 49,
"约 3 秒(73 帧)": 73,
"约 4 秒(97 帧)": 97,
"约 5 秒(129 帧,官方配置)": 129,
}
os.environ.setdefault("GRADIO_SSR_MODE", "0")
os.environ.setdefault("GRADIO_ANALYTICS_ENABLED", "False")
os.environ.setdefault("TOKENIZERS_PARALLELISM", "false")
os.environ.setdefault("PYTORCH_CUDA_ALLOC_CONF", "expandable_segments:True")
os.environ.setdefault("HF_HUB_ENABLE_HF_TRANSFER", "1")
def ensure_source():
if not (SOURCE_DIR / "hymm_sp" / "sample_gpu_poor.py").is_file():
print("[SETUP] Cloning official HunyuanVideo-Avatar source...", flush=True)
subprocess.run(
["git", "clone", "--depth", "1", SOURCE_REPO, str(SOURCE_DIR)],
check=True,
)
# The official `--infer-min` path hard-codes 129 frames. Make it honor the
# requested evaluation length so a Space can run short, lower-cost tests.
sample_file = SOURCE_DIR / "hymm_sp" / "sample_gpu_poor.py"
source = sample_file.read_text(encoding="utf-8")
patched = source.replace(
'batch["audio_len"][0] = 129',
'batch["audio_len"][0] = args.sample_n_frames',
)
if patched != source:
sample_file.write_text(patched, encoding="utf-8")
print("[SETUP] Patched infer-min to honor --sample-n-frames", flush=True)
ensure_source()
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
MODEL_PATTERNS = [
"ckpts/config.json",
"ckpts/det_align/detface.pt",
"ckpts/hunyuan-video-t2v-720p/transformers/mp_rank_00_model_states_fp8.pt",
"ckpts/hunyuan-video-t2v-720p/transformers/mp_rank_00_model_states_fp8_map.pt",
"ckpts/hunyuan-video-t2v-720p/vae/config.json",
"ckpts/hunyuan-video-t2v-720p/vae/pytorch_model.pt",
"ckpts/llava_llama_image/*.json",
"ckpts/llava_llama_image/*.safetensors",
"ckpts/llava_llama_image/*.model",
"ckpts/text_encoder_2/config.json",
"ckpts/text_encoder_2/model.safetensors",
"ckpts/text_encoder_2/*.json",
"ckpts/text_encoder_2/*.txt",
"ckpts/whisper-tiny/config.json",
"ckpts/whisper-tiny/model.safetensors",
"ckpts/whisper-tiny/preprocessor_config.json",
]
def ensure_weights():
checkpoint = (
WEIGHTS_DIR
/ "ckpts/hunyuan-video-t2v-720p/transformers/mp_rank_00_model_states_fp8.pt"
)
if checkpoint.is_file():
print(f"[MODEL CACHE] FP8 checkpoint ready: {checkpoint}", flush=True)
return checkpoint
WEIGHTS_DIR.mkdir(parents=True, exist_ok=True)
print(
"[MODEL DOWNLOAD START] repo=tencent/HunyuanVideo-Avatar, "
"profile=FP8-minimal, expected_size≈45GB",
flush=True,
)
snapshot_download(
repo_id=MODEL_REPO,
local_dir=WEIGHTS_DIR,
allow_patterns=MODEL_PATTERNS,
)
if not checkpoint.is_file():
raise RuntimeError("FP8 checkpoint download did not complete")
print(f"[MODEL DOWNLOAD DONE] checkpoint={checkpoint}", flush=True)
return checkpoint
def estimate_gpu_duration(_image, _audio, _prompt, frame_profile, *_args):
frames = FRAME_OPTIONS.get(frame_profile, 49)
# Includes first-run model download and CPU-offloaded inference.
return max(900, min(3600, 1200 + frames * 16))
def normalize_media(image_path, audio_path, work_dir):
image_target = work_dir / "character.png"
audio_target = work_dir / "speech.wav"
shutil.copy2(image_path, image_target)
subprocess.run(
[
"ffmpeg", "-y", "-i", str(audio_path), "-ac", "1", "-ar", "16000",
"-c:a", "pcm_s16le", str(audio_target),
],
check=True,
stdout=subprocess.DEVNULL,
stderr=subprocess.PIPE,
)
return image_target, audio_target
@spaces.GPU(size="xlarge", duration=estimate_gpu_duration)
def generate(image, audio, prompt, frame_profile, seed, progress=gr.Progress()):
if not image:
raise gr.Error("请上传一张角色图片")
if not audio:
raise gr.Error("请上传驱动语音")
frames = FRAME_OPTIONS.get(frame_profile, 49)
actual_seed = random.randint(0, 2**31 - 1) if int(seed) < 0 else int(seed)
job_id = uuid.uuid4().hex
job_dir = OUTPUT_DIR / job_id
result_dir = job_dir / "result"
job_dir.mkdir(parents=True, exist_ok=True)
result_dir.mkdir(parents=True, exist_ok=True)
try:
progress(0.02, desc="检查并下载官方 FP8 模型(首次约 45GB)...")
checkpoint = ensure_weights()
progress(0.12, desc="预处理图片和音频...")
image_path, audio_path = normalize_media(image, audio, job_dir)
meta_path = job_dir / "input.csv"
safe_prompt = (prompt or "A cartoon character speaks to the camera.").strip()
with meta_path.open("w", encoding="utf-8-sig", newline="") as handle:
writer = csv.DictWriter(
handle,
fieldnames=["videoid", "image", "audio", "prompt", "fps"],
)
writer.writeheader()
writer.writerow(
{
"videoid": job_id,
"image": str(image_path),
"audio": str(audio_path),
"prompt": safe_prompt,
"fps": FPS,
}
)
command = [
sys.executable,
str(SOURCE_DIR / "hymm_sp" / "sample_gpu_poor.py"),
"--input", str(meta_path),
"--ckpt", str(checkpoint),
"--sample-n-frames", str(frames),
"--seed", str(actual_seed),
"--image-size", "704",
"--cfg-scale", "7.5",
"--infer-steps", "50",
"--use-deepcache", "1",
"--flow-shift-eval-video", "5.0",
"--save-path", str(result_dir),
"--use-fp8",
"--cpu-offload",
"--infer-min",
]
env = os.environ.copy()
env.update(
{
"MODEL_BASE": str(WEIGHTS_DIR),
"CPU_OFFLOAD": "1",
"DISABLE_SP": "1",
"PYTHONPATH": str(SOURCE_DIR),
}
)
print("[INFERENCE START] " + " ".join(command), flush=True)
progress(0.18, desc="加载 FP8 模型并生成视频,可能需要较长时间...")
process = subprocess.run(
command,
cwd=SOURCE_DIR,
env=env,
text=True,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
timeout=3500,
)
print(process.stdout, flush=True)
if process.returncode != 0:
tail = "\n".join(process.stdout.splitlines()[-25:])
raise RuntimeError(f"官方推理进程退出码 {process.returncode}\n{tail}")
output = result_dir / f"{job_id}_audio.mp4"
if not output.is_file():
candidates = sorted(result_dir.glob("*_audio.mp4"))
if not candidates:
raise RuntimeError("推理完成,但没有找到带音频的 MP4 输出")
output = candidates[-1]
progress(1.0, desc="生成完成")
info = (
f"完成:{frames} 帧 / {FPS}fps(约 {frames / FPS:.1f} 秒),"
f"704px,50 steps,FP8 + CPU offload。"
)
return str(output), actual_seed, info
except subprocess.TimeoutExpired as exc:
raise gr.Error("生成超过 ZeroGPU 最长执行时间,请改用 49 帧重试") from exc
except gr.Error:
raise
except Exception as exc:
print(f"[ERROR] {exc}", flush=True)
raise gr.Error(f"生成失败:{exc}") from exc
finally:
gc.collect()
if torch.cuda.is_available():
torch.cuda.empty_cache()
with gr.Blocks(title="HunyuanVideo-Avatar 卡通数字人测试") as demo:
gr.Markdown(
"""
# HunyuanVideo-Avatar 卡通数字人测试
上传一张卡通/3D/拟人角色图片和一段语音,评估角色一致性、口型、表情和身体动作。
**首次运行需要下载约 45GB 官方权重。建议先选 49 帧短片。**
"""
)
with gr.Row():
with gr.Column():
image_input = gr.Image(label="角色图片", type="filepath")
audio_input = gr.Audio(label="驱动语音", type="filepath")
prompt_input = gr.Textbox(
label="英文画面描述",
value="A cute cartoon character speaks naturally to the camera with subtle gestures.",
lines=3,
info="描述角色、构图、背景和期望动作;不要写与原图冲突的外观。",
)
frame_input = gr.Dropdown(
choices=list(FRAME_OPTIONS),
value=list(FRAME_OPTIONS)[0],
label="测试时长",
)
seed_input = gr.Number(label="随机种子(-1 为随机)", value=-1, precision=0)
generate_button = gr.Button("生成测试视频", variant="primary")
with gr.Column():
video_output = gr.Video(label="生成结果")
seed_output = gr.Number(label="实际种子", precision=0)
info_output = gr.Textbox(label="生成信息")
gr.Markdown(
"""
### 素材建议
- 单个角色、脸部清晰;正面或轻微侧脸更稳定。
- 半身或全身卡通图都可测试,避免文字、水印和多人画面。
- 语音尽量清晰、无背景音乐;当前测试输出最多约 5 秒。
- 官方模型很大,ZeroGPU 冷启动与 CPU offload 都会显著增加等待时间。
"""
)
generate_button.click(
fn=generate,
inputs=[image_input, audio_input, prompt_input, frame_input, seed_input],
outputs=[video_output, seed_output, info_output],
api_name="generate",
)
if __name__ == "__main__":
demo.queue(default_concurrency_limit=1, max_size=8).launch(
server_name="0.0.0.0",
server_port=7860,
show_error=True,
)
|