File size: 8,190 Bytes
e204ee2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
import os, sys, subprocess

# FramePack's sampling helpers aren't a pip package: fetch them at startup.
if not os.path.isdir("FramePack"):
    subprocess.run(["git", "clone", "--depth", "1",
                    "https://github.com/lllyasviel/FramePack", "FramePack"], check=True)
sys.path.insert(0, os.path.abspath("FramePack"))

import torch, numpy as np, gradio as gr
from diffusers import AutoencoderKLHunyuanVideo
from transformers import (LlamaModel, CLIPTextModel, LlamaTokenizerFast, CLIPTokenizer,
                          SiglipImageProcessor, SiglipVisionModel)
from diffusers_helper.hunyuan import encode_prompt_conds, vae_decode, vae_encode
from diffusers_helper.utils import (save_bcthw_as_mp4, crop_or_pad_yield_mask, soft_append_bcthw,
                                    resize_and_center_crop, generate_timestamp)
from diffusers_helper.models.hunyuan_video_packed import HunyuanVideoTransformer3DModelPacked
from diffusers_helper.pipelines.k_diffusion_hunyuan import sample_hunyuan
from diffusers_helper.clip_vision import hf_clip_vision_encode
from diffusers_helper.bucket_tools import find_nearest_bucket

try:
    import spaces
    GPU = spaces.GPU(duration=120)
except Exception:  # dedicated GPU hardware
    GPU = lambda f: f

MODEL_ID = "Suparious/FP-image-to-video-FLUX.1-HV-bf16"
BASE = "hunyuanvideo-community/HunyuanVideo"
dev = "cuda"

# Everything fits on a single 80GB GPU (ZeroGPU H200 / A100-80GB) without offloading.
text_encoder = LlamaModel.from_pretrained(BASE, subfolder="text_encoder", torch_dtype=torch.float16).to(dev).eval()
text_encoder_2 = CLIPTextModel.from_pretrained(BASE, subfolder="text_encoder_2", torch_dtype=torch.float16).to(dev).eval()
tokenizer = LlamaTokenizerFast.from_pretrained(BASE, subfolder="tokenizer")
tokenizer_2 = CLIPTokenizer.from_pretrained(BASE, subfolder="tokenizer_2")
vae = AutoencoderKLHunyuanVideo.from_pretrained(BASE, subfolder="vae", torch_dtype=torch.float16).to(dev).eval()
vae.enable_slicing(); vae.enable_tiling()
feature_extractor = SiglipImageProcessor.from_pretrained("lllyasviel/flux_redux_bfl", subfolder="feature_extractor")
image_encoder = SiglipVisionModel.from_pretrained("lllyasviel/flux_redux_bfl", subfolder="image_encoder",
                                                  torch_dtype=torch.float16).to(dev).eval()
transformer = HunyuanVideoTransformer3DModelPacked.from_pretrained(MODEL_ID, torch_dtype=torch.bfloat16).to(dev).eval()
transformer.high_quality_fp32_output_for_inference = True
for m in (text_encoder, text_encoder_2, vae, image_encoder, transformer):
    m.requires_grad_(False)

os.makedirs("outputs", exist_ok=True)


@GPU
@torch.no_grad()
def generate(image, prompt, n_prompt, seconds, steps, seed, gs, use_teacache, crf, progress=gr.Progress()):
    if image is None:
        raise gr.Error("Please upload an image.")
    latent_window_size, cfg, rs = 9, 1.0, 0.0
    sections = max(int(round(seconds * 30 / (latent_window_size * 4))), 1)
    job = generate_timestamp()

    llama_vec, clip_pool = encode_prompt_conds(prompt, text_encoder, text_encoder_2, tokenizer, tokenizer_2)
    llama_vec_n, clip_pool_n = torch.zeros_like(llama_vec), torch.zeros_like(clip_pool)
    llama_vec, llama_mask = crop_or_pad_yield_mask(llama_vec, length=512)
    llama_vec_n, llama_mask_n = crop_or_pad_yield_mask(llama_vec_n, length=512)

    H, W, _ = image.shape
    height, width = find_nearest_bucket(H, W, resolution=640)
    img_np = resize_and_center_crop(image, target_width=width, target_height=height)
    img_pt = (torch.from_numpy(img_np).float() / 127.5 - 1).permute(2, 0, 1)[None, :, None]
    start_latent = vae_encode(img_pt, vae)
    img_hidden = hf_clip_vision_encode(img_np, feature_extractor, image_encoder).last_hidden_state

    dt = transformer.dtype
    llama_vec, llama_vec_n = llama_vec.to(dt), llama_vec_n.to(dt)
    clip_pool, clip_pool_n = clip_pool.to(dt), clip_pool_n.to(dt)
    img_hidden = img_hidden.to(dt)

    rnd = torch.Generator("cpu").manual_seed(int(seed))
    num_frames = latent_window_size * 4 - 3
    history_latents = torch.zeros((1, 16, 1 + 2 + 16, height // 8, width // 8), dtype=torch.float32).cpu()
    history_pixels, total = None, 0
    paddings = list(reversed(range(sections)))
    if sections > 4:
        paddings = [3] + [2] * (sections - 3) + [1, 0]

    out = None
    for i, pad in enumerate(paddings):
        last = pad == 0
        pad_size = pad * latent_window_size
        idx = torch.arange(0, 1 + pad_size + latent_window_size + 1 + 2 + 16).unsqueeze(0)
        (pre_i, _, lat_i, post_i, c2x_i, c4x_i) = idx.split([1, pad_size, latent_window_size, 1, 2, 16], dim=1)
        clean_i = torch.cat([pre_i, post_i], dim=1)
        post, c2x, c4x = history_latents[:, :, :19].split([1, 2, 16], dim=2)
        clean = torch.cat([start_latent.to(history_latents), post], dim=2)

        transformer.initialize_teacache(enable_teacache=use_teacache, num_steps=steps)

        def cb(d, i=i):
            progress((i * steps + d["i"] + 1) / (len(paddings) * steps),
                     desc=f"Section {i + 1}/{len(paddings)} · step {d['i'] + 1}/{steps}")

        gen = sample_hunyuan(
            transformer=transformer, sampler="unipc", width=width, height=height, frames=num_frames,
            real_guidance_scale=cfg, distilled_guidance_scale=gs, guidance_rescale=rs,
            num_inference_steps=steps, generator=rnd,
            prompt_embeds=llama_vec, prompt_embeds_mask=llama_mask, prompt_poolers=clip_pool,
            negative_prompt_embeds=llama_vec_n, negative_prompt_embeds_mask=llama_mask_n,
            negative_prompt_poolers=clip_pool_n,
            device=dev, dtype=torch.bfloat16, image_embeddings=img_hidden,
            latent_indices=lat_i, clean_latents=clean, clean_latent_indices=clean_i,
            clean_latents_2x=c2x, clean_latent_2x_indices=c2x_i,
            clean_latents_4x=c4x, clean_latent_4x_indices=c4x_i, callback=cb)

        if last:
            gen = torch.cat([start_latent.to(gen), gen], dim=2)
        total += int(gen.shape[2])
        history_latents = torch.cat([gen.to(history_latents), history_latents], dim=2)
        real = history_latents[:, :, :total]

        if history_pixels is None:
            history_pixels = vae_decode(real, vae).cpu()
        else:
            n = latent_window_size * 2 + (1 if last else 0)
            cur = vae_decode(real[:, :, :n], vae).cpu()
            history_pixels = soft_append_bcthw(cur, history_pixels, latent_window_size * 4 - 3)

        out = os.path.join("outputs", f"{job}_{total}.mp4")
        save_bcthw_as_mp4(history_pixels, out, fps=30, crf=int(crf))
        yield out
    yield out


with gr.Blocks(title="FramePack Image-to-Video") as demo:
    gr.Markdown("# 🎞️ FramePack Image-to-Video\nUpload an image, describe the motion, and generate a video with "
                f"[`{MODEL_ID}`](https://huggingface.co/{MODEL_ID}). Videos are built section by section "
                "(~1s each) and the preview updates as they finish.")
    with gr.Row():
        with gr.Column():
            image = gr.Image(type="numpy", label="Start image", height=360)
            prompt = gr.Textbox(label="Prompt", placeholder="The girl dances gracefully, with clear movements.")
            seconds = gr.Slider(1, 5, value=1, step=1, label="Video length (seconds)")
            with gr.Accordion("Advanced", open=False):
                n_prompt = gr.Textbox(label="Negative prompt (unused at CFG 1)", value="")
                steps = gr.Slider(10, 50, value=25, step=1, label="Steps")
                gs = gr.Slider(1, 32, value=10, step=0.5, label="Distilled CFG scale")
                seed = gr.Number(value=31337, precision=0, label="Seed")
                teacache = gr.Checkbox(value=True, label="TeaCache (faster, slightly lower quality)")
                crf = gr.Slider(0, 51, value=16, step=1, label="MP4 compression (lower = better)")
            go = gr.Button("Generate", variant="primary")
        with gr.Column():
            video = gr.Video(label="Result", autoplay=True, loop=True)
    go.click(generate, [image, prompt, n_prompt, seconds, steps, seed, gs, teacache, crf], video)

demo.queue().launch()