model_test / app.py
doradream's picture
添加分辨率180/120和帧数17/21选项
3ad4bb2
Raw
History Blame Contribute Delete
20.9 kB
import gc
import os
import tempfile
import warnings
os.environ.setdefault("GRADIO_SSR_MODE", "false")
warnings.filterwarnings(
"ignore",
message=".*HTTP_422_UNPROCESSABLE_ENTITY.*",
category=DeprecationWarning,
)
#!!!!!
try:
import spaces
except ImportError:
class _SpacesFallback:
@staticmethod
def GPU(*args, **kwargs):
def decorator(fn):
return fn
return decorator
spaces = _SpacesFallback()
import gradio as gr
import torch
from diffusers import AutoencoderKLWan, WanPipeline
from diffusers import HunyuanVideo15Pipeline, HunyuanVideo15ImageToVideoPipeline
from diffusers.utils import export_to_video
from huggingface_hub import snapshot_download
from PIL import Image
from transformers import AutoTokenizer, AutoModelForCausalLM
# ---------------------------------------------------------------------------
# Model registry
# ---------------------------------------------------------------------------
MODEL_OPTIONS = {
"HunyuanVideo-1.5-T2V": "hunyuanvideo-community/HunyuanVideo-1.5-Diffusers-480p_t2v",
"HunyuanVideo-1.5-I2V": "hunyuanvideo-community/HunyuanVideo-1.5-Diffusers-480p_i2v",
"Wan2.2-TI2V-5B": "Wan-AI/Wan2.2-TI2V-5B-Diffusers",
"Wan2.2-T2V-A14B": "Wan-AI/Wan2.2-T2V-A14B",
}
LLM_MODEL_ID = "Qwen/Qwen3.6-35B-A3B"
DEFAULT_MODEL_LABEL = os.getenv("DEFAULT_MODEL_LABEL", "HunyuanVideo-1.5-T2V")
PRELOAD_MODELS = os.getenv("PRELOAD_MODELS", "HunyuanVideo-1.5-T2V")
OUTPUT_DIR = os.getenv("OUTPUT_DIR", tempfile.gettempdir())
CACHE_ROOT = os.getenv("MODEL_CACHE_DIR", "/data" if os.path.isdir("/data") else tempfile.gettempdir())
HY_T2V_CKPT_DIR = os.getenv("HY_T2V_CKPT_DIR", os.path.join(CACHE_ROOT, "HunyuanVideo-1.5-T2V"))
HY_I2V_CKPT_DIR = os.getenv("HY_I2V_CKPT_DIR", os.path.join(CACHE_ROOT, "HunyuanVideo-1.5-I2V"))
WAN_CKPT_DIR = os.getenv("WAN_CKPT_DIR", os.path.join(CACHE_ROOT, "Wan2.2-TI2V-5B"))
WAN_14B_CKPT_DIR = os.getenv("WAN_14B_CKPT_DIR", os.path.join(CACHE_ROOT, "Wan2.2-T2V-A14B"))
LLM_CKPT_DIR = os.getenv("LLM_CKPT_DIR", os.path.join(CACHE_ROOT, "Qwen3.6-35B-A3B"))
# ---------------------------------------------------------------------------
# Global state
# ---------------------------------------------------------------------------
pipe = None
loaded_model_id = None
# LLM state (kept separate from video pipelines)
llm_model = None
llm_tokenizer = None
# ===========================================================================
# LLM (Qwen3.6-35B-A3B) — prompt enhancement
# ===========================================================================
def _unload_llm():
"""Free the Qwen LLM from GPU memory."""
global llm_model, llm_tokenizer
if llm_model is not None:
del llm_model
llm_model = None
if llm_tokenizer is not None:
del llm_tokenizer
llm_tokenizer = None
gc.collect()
torch.cuda.empty_cache()
def _load_llm():
"""Load Qwen3.6-35B-A3B for prompt enhancement (lazy-loaded, cached)."""
global llm_model, llm_tokenizer
if llm_model is not None and llm_tokenizer is not None:
return llm_model, llm_tokenizer
model_id = LLM_CKPT_DIR
llm_tokenizer = AutoTokenizer.from_pretrained(model_id, trust_remote_code=True)
llm_model = AutoModelForCausalLM.from_pretrained(
model_id,
torch_dtype=torch.bfloat16,
device_map="auto",
trust_remote_code=True,
)
llm_model.eval()
return llm_model, llm_tokenizer
def enhance_prompt(prompt):
"""Use Qwen LLM to expand a short prompt into a detailed video description."""
if not prompt or not prompt.strip():
raise gr.Error("Please enter a prompt to enhance.")
# Unload video pipeline to free GPU memory for LLM
global pipe, loaded_model_id
if pipe is not None:
del pipe
pipe = None
loaded_model_id = None
gc.collect()
torch.cuda.empty_cache()
model, tokenizer = _load_llm()
messages = [
{
"role": "system",
"content": (
"You are a professional video prompt engineer. Given a short description, "
"expand it into a detailed, cinematic video generation prompt in English. "
"Include camera angles, lighting, motion, atmosphere, and visual details. "
"Output ONLY the enhanced prompt, nothing else. Keep it under 200 words."
),
},
{"role": "user", "content": prompt.strip()},
]
text = tokenizer.apply_chat_template(
messages, tokenize=False, add_generation_prompt=True
)
inputs = tokenizer(text, return_tensors="pt").to(model.device)
with torch.inference_mode():
outputs = model.generate(
**inputs,
max_new_tokens=300,
temperature=0.7,
do_sample=True,
)
# Decode only the newly generated tokens (exclude the input prompt)
new_tokens = outputs[0][inputs["input_ids"].shape[1]:]
result = tokenizer.decode(new_tokens, skip_special_tokens=True).strip()
# Clean up possible thinking tags from Qwen
if "<think>" in result:
# Remove everything between <think>...</think>
import re
result = re.sub(r"<think>.*?</think>", "", result, flags=re.DOTALL).strip()
return result if result else prompt.strip()
def enhance_prompt_ui(prompt):
"""UI wrapper for prompt enhancement with status feedback."""
try:
enhanced = enhance_prompt(prompt)
return enhanced, f"Prompt enhanced successfully using {LLM_MODEL_ID}."
except Exception as e:
return prompt, f"Enhancement failed: {e}. Using original prompt."
def download_llm():
"""Download Qwen LLM weights to local cache."""
if os.path.exists(os.path.join(LLM_CKPT_DIR, "config.json")):
return f"LLM ({LLM_MODEL_ID}) already downloaded."
snapshot_download(
repo_id=LLM_MODEL_ID,
local_dir=LLM_CKPT_DIR,
local_dir_use_symlinks=False,
)
return f"Downloaded LLM: {LLM_MODEL_ID}"
# ===========================================================================
# Video pipeline loading
# ===========================================================================
def _resolve_model_id(model_label):
"""Map a model label to its checkpoint directory."""
mapping = {
"HunyuanVideo-1.5-T2V": HY_T2V_CKPT_DIR,
"HunyuanVideo-1.5-I2V": HY_I2V_CKPT_DIR,
"Wan2.2-TI2V-5B": WAN_CKPT_DIR,
"Wan2.2-T2V-A14B": WAN_14B_CKPT_DIR,
}
if model_label not in mapping:
raise gr.Error(f"Unknown model: {model_label}")
return mapping[model_label]
def _load_pipeline(model_label):
global loaded_model_id
global pipe
model_id = _resolve_model_id(model_label)
if pipe is not None and loaded_model_id == model_id:
return pipe
# Unload LLM before loading video pipeline
_unload_llm()
if pipe is not None:
del pipe
pipe = None
gc.collect()
torch.cuda.empty_cache()
# --- HunyuanVideo-1.5 T2V ---
if model_label == "HunyuanVideo-1.5-T2V":
pipe = HunyuanVideo15Pipeline.from_pretrained(
model_id,
torch_dtype=torch.bfloat16,
)
pipe.to("cuda")
pipe.vae.enable_tiling()
loaded_model_id = model_id
return pipe
# --- HunyuanVideo-1.5 I2V ---
if model_label == "HunyuanVideo-1.5-I2V":
pipe = HunyuanVideo15ImageToVideoPipeline.from_pretrained(
model_id,
torch_dtype=torch.bfloat16,
)
pipe.to("cuda")
pipe.vae.enable_tiling()
loaded_model_id = model_id
return pipe
# --- Wan2.2-TI2V-5B ---
if model_label == "Wan2.2-TI2V-5B":
vae = AutoencoderKLWan.from_pretrained(
model_id,
subfolder="vae",
torch_dtype=torch.float32,
)
pipe = WanPipeline.from_pretrained(
model_id,
vae=vae,
torch_dtype=torch.bfloat16,
)
pipe.to("cuda")
loaded_model_id = model_id
return pipe
# --- Wan2.2-T2V-A14B (14B parameter text-to-video) ---
if model_label == "Wan2.2-T2V-A14B":
vae = AutoencoderKLWan.from_pretrained(
model_id,
subfolder="vae",
torch_dtype=torch.float32,
)
pipe = WanPipeline.from_pretrained(
model_id,
vae=vae,
torch_dtype=torch.bfloat16,
)
pipe.to("cuda")
loaded_model_id = model_id
return pipe
raise gr.Error(f"Unsupported model: {model_label}")
def _resize_image(image, width, height):
image = image.convert("RGB")
return image.resize((width, height), Image.Resampling.LANCZOS)
# ===========================================================================
# Weight downloading
# ===========================================================================
def _ensure_wan_weights():
if os.path.exists(os.path.join(WAN_CKPT_DIR, "model_index.json")):
return WAN_CKPT_DIR
return snapshot_download(
repo_id=MODEL_OPTIONS["Wan2.2-TI2V-5B"],
local_dir=WAN_CKPT_DIR,
local_dir_use_symlinks=False,
)
def _ensure_wan_14b_weights():
if os.path.exists(os.path.join(WAN_14B_CKPT_DIR, "model_index.json")):
return WAN_14B_CKPT_DIR
return snapshot_download(
repo_id=MODEL_OPTIONS["Wan2.2-T2V-A14B"],
local_dir=WAN_14B_CKPT_DIR,
local_dir_use_symlinks=False,
)
def _ensure_hunyuan_weights(model_label):
if model_label == "HunyuanVideo-1.5-T2V":
ckpt_dir = HY_T2V_CKPT_DIR
else:
ckpt_dir = HY_I2V_CKPT_DIR
if os.path.exists(os.path.join(ckpt_dir, "model_index.json")):
return ckpt_dir
return snapshot_download(
repo_id=MODEL_OPTIONS[model_label],
local_dir=ckpt_dir,
local_dir_use_symlinks=False,
)
def _model_cache_ready(model_label):
if model_label in ("HunyuanVideo-1.5-T2V", "HunyuanVideo-1.5-I2V"):
ckpt_dir = HY_T2V_CKPT_DIR if model_label == "HunyuanVideo-1.5-T2V" else HY_I2V_CKPT_DIR
return os.path.exists(os.path.join(ckpt_dir, "model_index.json"))
if model_label == "Wan2.2-TI2V-5B":
return os.path.exists(os.path.join(WAN_CKPT_DIR, "model_index.json"))
if model_label == "Wan2.2-T2V-A14B":
return os.path.exists(os.path.join(WAN_14B_CKPT_DIR, "model_index.json"))
return False
def download_model_assets(model_label):
if model_label not in MODEL_OPTIONS:
raise gr.Error("Please choose a supported model.")
if model_label in ("HunyuanVideo-1.5-T2V", "HunyuanVideo-1.5-I2V"):
_ensure_hunyuan_weights(model_label)
return f"Downloaded {model_label}: Diffusers model weights"
if model_label == "Wan2.2-TI2V-5B":
_ensure_wan_weights()
return f"Downloaded {model_label}: Diffusers model weights"
if model_label == "Wan2.2-T2V-A14B":
_ensure_wan_14b_weights()
return f"Downloaded {model_label}: Diffusers model weights"
raise gr.Error(f"Unsupported model: {model_label}")
def _preload_configured_models():
if PRELOAD_MODELS.strip().lower() in {"", "0", "false", "none", "off"}:
return "Startup preload disabled. Use the download button before generating."
if PRELOAD_MODELS.strip().lower() == "all":
model_labels = list(MODEL_OPTIONS.keys())
else:
requested = [item.strip() for item in PRELOAD_MODELS.split(",")]
model_labels = [item for item in requested if item in MODEL_OPTIONS]
if not model_labels:
return f"No valid PRELOAD_MODELS entries found: {PRELOAD_MODELS}"
messages = []
for model_label in model_labels:
try:
messages.append(download_model_assets(model_label))
except Exception as error:
messages.append(f"Failed to download {model_label}: {error}")
return "\n".join(messages)
# ===========================================================================
# Video generation
# ===========================================================================
def _duration(model_label, prompt, image, negative_prompt, width, height, frames, steps, guidance_scale, seed, enhance):
if "HunyuanVideo" in model_label:
base_seconds = 120
elif "A14B" in model_label:
base_seconds = 150 # 14B model needs more time
else:
base_seconds = 90
extra = 60 if enhance else 0 # LLM enhancement overhead
# ZeroGPU xlarge max is 300s
return min(300, max(60, int(base_seconds + steps * 4 + frames * 1.0 + extra)))
def _call_pipeline(pipeline, prompt, image, negative_prompt, width, height, frames, steps, guidance_scale, generator):
kwargs = {
"prompt": prompt,
"width": width,
"height": height,
"num_frames": frames,
"num_inference_steps": steps,
"guidance_scale": guidance_scale,
"generator": generator,
}
if image is not None:
kwargs["image"] = image
if negative_prompt:
kwargs["negative_prompt"] = negative_prompt
try:
return pipeline(**kwargs)
except TypeError as error:
raise gr.Error(f"The selected pipeline rejected these inputs: {error}") from error
@spaces.GPU(size="xlarge", duration=_duration)
def generate_video(
model_label,
prompt,
image,
negative_prompt,
width,
height,
frames,
steps,
guidance_scale,
seed,
enhance,
):
if model_label not in MODEL_OPTIONS:
raise gr.Error("Please choose a supported model.")
# HunyuanVideo I2V requires an input image
if image is None and model_label == "HunyuanVideo-1.5-I2V":
raise gr.Error("HunyuanVideo-1.5-I2V requires an input image for image-to-video generation.")
if not prompt or not prompt.strip():
raise gr.Error("Please enter a prompt.")
# --- Optional: enhance prompt with Qwen LLM ---
if enhance:
try:
prompt = enhance_prompt(prompt)
except Exception as e:
raise gr.Error(f"Prompt enhancement failed: {e}")
width = int(width)
height = int(height)
frames = int(frames)
steps = int(steps)
seed = int(seed)
# Force model-native resolutions before validation
if "HunyuanVideo" in model_label:
width, height = (848, 480) if width >= height else (480, 848)
elif model_label in ("Wan2.2-TI2V-5B", "Wan2.2-T2V-A14B"):
width, height = (1280, 704) if width >= height else (704, 1280)
# HunyuanVideo VAE compresses 16x, Wan VAE compresses 32x
divisor = 16 if "HunyuanVideo" in model_label else 32
if width % divisor != 0 or height % divisor != 0:
raise gr.Error(f"Width and height must be divisible by {divisor}.")
# Frame constraint: 4n+1 for both HunyuanVideo and Wan
if (frames - 1) % 4 != 0:
raise gr.Error("Frame count must be 4n + 1, for example 25, 29, 33, 37, 41, 45, 49, 53, 57, 61, 81, 97, or 121.")
if not _model_cache_ready(model_label):
raise gr.Error(f"{model_label} is not downloaded yet. Click Download selected model first.")
resized_image = _resize_image(image, width, height) if image is not None else None
generator = None
if seed >= 0:
generator = torch.Generator(device="cuda").manual_seed(seed)
pipeline = _load_pipeline(model_label)
with torch.inference_mode():
output = _call_pipeline(
pipeline=pipeline,
prompt=prompt.strip(),
image=resized_image,
negative_prompt=negative_prompt.strip(),
width=width,
height=height,
frames=frames,
steps=steps,
guidance_scale=float(guidance_scale),
generator=generator,
)
video_file = tempfile.NamedTemporaryFile(
prefix=f"{model_label.lower().replace('.', '_').replace('-', '_')}_",
suffix=".mp4",
dir=OUTPUT_DIR,
delete=False,
)
video_file.close()
video_path = video_file.name
export_to_video(output.frames[0], video_path, fps=24)
gc.collect()
torch.cuda.empty_cache()
return video_path
# ===========================================================================
# Gradio UI
# ===========================================================================
example_prompt = (
"A cinematic close-up of the subject turning toward the camera, soft natural "
"light, detailed motion, realistic texture, synchronized ambient audio."
)
with gr.Blocks(title="Video Model ZeroGPU Test") as demo:
gr.Markdown("# Video Model ZeroGPU Test")
with gr.Row():
with gr.Column(scale=1):
model_input = gr.Dropdown(
label="Model",
choices=list(MODEL_OPTIONS.keys()),
value=DEFAULT_MODEL_LABEL if DEFAULT_MODEL_LABEL in MODEL_OPTIONS else "HunyuanVideo-1.5-T2V",
)
download_button = gr.Button("Download selected model")
download_status = gr.Textbox(
label="Download status",
value="Startup preload has not started yet.",
lines=3,
interactive=False,
)
image_input = gr.Image(
label="Start image (required for I2V models)",
type="pil",
sources=["upload", "clipboard"],
height=320,
)
prompt_input = gr.Textbox(
label="Prompt",
value=example_prompt,
lines=5,
)
# --- LLM Prompt Enhancement ---
with gr.Row():
enhance_checkbox = gr.Checkbox(
label=f"Enhance prompt with LLM ({LLM_MODEL_ID})",
value=False,
)
enhance_button = gr.Button("Enhance now", size="sm")
enhance_status = gr.Textbox(
label="Enhancement status",
visible=False,
interactive=False,
)
negative_prompt_input = gr.Textbox(
label="Negative prompt",
value="low quality, blurry, distorted, flickering, artifacts",
lines=2,
)
with gr.Row():
width_input = gr.Dropdown(
label="Width",
choices=[120, 180, 256, 320, 384, 448, 480, 512, 640],
value=640,
)
height_input = gr.Dropdown(
label="Height",
choices=[120, 180, 256, 320, 384, 448, 480, 512, 640],
value=480,
)
frames_input = gr.Dropdown(
label="Frames",
choices=[17, 21, 25, 29, 33, 37, 41, 45, 49, 53, 57, 61, 65, 69, 73, 77, 81, 85, 89, 93, 97, 101, 105, 109, 113, 117, 121],
value=81,
)
with gr.Row():
steps_input = gr.Slider(
label="Inference steps",
minimum=4,
maximum=50,
step=1,
value=30,
)
guidance_input = gr.Slider(
label="CFG scale",
minimum=1.0,
maximum=8.0,
step=0.1,
value=6.0,
)
seed_input = gr.Number(
label="Seed (-1 for random)",
value=0,
precision=0,
)
generate_button = gr.Button("Generate", variant="primary")
with gr.Column(scale=1):
video_output = gr.Video(label="Generated video", format="mp4")
# --- Event bindings ---
demo.load(
fn=_preload_configured_models,
outputs=download_status,
)
download_button.click(
fn=download_model_assets,
inputs=model_input,
outputs=download_status,
)
# Prompt enhancement button: rewrites the prompt in-place
enhance_button.click(
fn=enhance_prompt_ui,
inputs=prompt_input,
outputs=[prompt_input, enhance_status],
)
generate_button.click(
fn=generate_video,
inputs=[
model_input,
prompt_input,
image_input,
negative_prompt_input,
width_input,
height_input,
frames_input,
steps_input,
guidance_input,
seed_input,
enhance_checkbox,
],
outputs=video_output,
)
if __name__ == "__main__":
try:
demo.queue(max_size=20).launch(ssr_mode=False)
except TypeError:
demo.queue(max_size=20).launch()
# End of file