dosesnrolls1's picture
Update app.py
ef69955 verified
Raw
History Blame Contribute Delete
17.7 kB
import gc
import random
import tempfile
from functools import lru_cache
from typing import List, Optional, Tuple
import gradio as gr
import numpy as np
import torch
from PIL import Image
from briarmbg import BriaRMBG
from diffusers import AutoencoderKLWan, UniPCMultistepScheduler, WanVACEPipeline
from diffusers.utils import export_to_video
MODEL_ID = "Wan-AI/Wan2.1-VACE-14B-diffusers"
RMBG_ID = "briaai/RMBG-1.4"
LORA_REPO = "vrgamedevgirl84/Wan14BT2VFusioniX"
LORA_FILE = "FusionX_LoRa/Phantom_Wan_14B_FusionX_LoRA.safetensors"
LORA_ADAPTER = "phantom"
DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
PIPE_DTYPE = torch.bfloat16 if DEVICE == "cuda" else torch.float32
VAE_DTYPE = torch.float32
RMBG_DTYPE = torch.float32
MOD_VALUE = 32
DEFAULT_H_SLIDER_VALUE = 512
DEFAULT_W_SLIDER_VALUE = 896
NEW_FORMULA_MAX_AREA = 480.0 * 832.0
SLIDER_MIN_H, SLIDER_MAX_H = 128, 896
SLIDER_MIN_W, SLIDER_MAX_W = 128, 896
MAX_SEED = np.iinfo(np.int32).max
FIXED_FPS = 16
MIN_FRAMES_MODEL = 8
MAX_FRAMES_MODEL = 81
MODE_PROMPTS = {
"Reference": "the playful penguin picks up the green cat eye sunglasses and puts them on",
"First - Last Frame": "CG animation style, a small blue bird takes off from the ground, flapping its wings. The bird's feathers are delicate, with a unique pattern on its chest. The background shows a blue sky with white clouds under bright sunshine. The camera follows the bird upward, capturing its flight and the vastness of the sky from a close-up, low-angle perspective.",
"Random Transitions": "Various different characters appear and disappear in a fast transition video showcasing their unique features and personalities. The video is about showcasing different dance styles, with each character performing a distinct dance move. The background is a vibrant, colorful stage with dynamic lighting that changes with each dance style. The camera captures close-ups of the dancers' expressions and movements. Highly dynamic, fast-paced music video, with quick cuts and transitions between characters, cinematic, vibrant colors",
}
DEFAULT_NEGATIVE_PROMPT = (
"Bright tones, overexposed, static, blurred details, subtitles, style, works, paintings, images, "
"static, overall gray, worst quality, low quality, JPEG compression residue, ugly, incomplete, extra fingers, "
"poorly drawn hands, poorly drawn faces, deformed, disfigured, misshapen limbs, fused fingers, still picture, "
"messy background, three legs, many people in the background, walking backwards, watermark, text, signature"
)
def clear_cuda():
gc.collect()
if torch.cuda.is_available():
torch.cuda.empty_cache()
if hasattr(torch.cuda, "ipc_collect"):
torch.cuda.ipc_collect()
@lru_cache(maxsize=1)
def get_pipe() -> WanVACEPipeline:
vae = AutoencoderKLWan.from_pretrained(MODEL_ID, subfolder="vae", torch_dtype=VAE_DTYPE)
pipe = WanVACEPipeline.from_pretrained(MODEL_ID, vae=vae, torch_dtype=PIPE_DTYPE)
pipe.scheduler = UniPCMultistepScheduler.from_config(pipe.scheduler.config, flow_shift=2.0)
pipe.load_lora_weights(LORA_REPO, weight_name=LORA_FILE, adapter_name=LORA_ADAPTER)
try:
pipe.set_adapters([LORA_ADAPTER], adapter_weights=[1.0])
except Exception:
pass
pipe = pipe.to(DEVICE)
return pipe
@lru_cache(maxsize=1)
def get_rmbg() -> BriaRMBG:
model = BriaRMBG.from_pretrained(RMBG_ID)
model = model.to(DEVICE, dtype=RMBG_DTYPE)
model.eval()
return model
def remove_alpha_channel(image: Image.Image) -> Image.Image:
if image.mode in ("RGBA", "LA"):
background = Image.new("RGB", image.size, (255, 255, 255))
paste_source = image if image.mode == "RGBA" else image.convert("RGB")
background.paste(paste_source, mask=image.split()[-1])
return background
if image.mode == "P":
if "transparency" in image.info:
image = image.convert("RGBA")
background = Image.new("RGB", image.size, (255, 255, 255))
background.paste(image, mask=image.split()[-1])
return background
return image.convert("RGB")
if image.mode != "RGB":
return image.convert("RGB")
return image
def numpy2pytorch(imgs: List[np.ndarray]) -> torch.Tensor:
h = torch.from_numpy(np.stack(imgs, axis=0)).float() / 127.0 - 1.0
return h.movedim(-1, 1)
def resize_without_crop(image: np.ndarray, target_width: int, target_height: int) -> np.ndarray:
return np.array(Image.fromarray(image).resize((target_width, target_height), Image.LANCZOS))
def run_rmbg(img: np.ndarray, sigma: float = 0.0) -> Tuple[np.ndarray, np.ndarray]:
model = get_rmbg()
h, w, c = img.shape
if c != 3:
raise ValueError("RMBG expects RGB input")
k = (256.0 / float(h * w)) ** 0.5
feed = resize_without_crop(img, int(64 * round(w * k)), int(64 * round(h * k)))
feed = numpy2pytorch([feed]).to(device=DEVICE, dtype=RMBG_DTYPE)
with torch.inference_mode():
alpha = model(feed)[0][0]
alpha = torch.nn.functional.interpolate(alpha, size=(h, w), mode="bilinear")
alpha = alpha.movedim(1, -1)[0].detach().float().cpu().numpy().clip(0, 1)
result = 127 + (img.astype(np.float32) - 127 + sigma) * alpha
return result.clip(0, 255).astype(np.uint8), alpha
def remove_background_from_image(image: Image.Image) -> Image.Image:
image = remove_alpha_channel(image)
result_array, alpha_mask = run_rmbg(np.array(image))
result_image = Image.fromarray(result_array).convert("RGBA")
alpha_mask_2d = np.squeeze(alpha_mask)
if alpha_mask_2d.ndim > 2:
alpha_mask_2d = alpha_mask_2d[:, :, 0]
alpha_array = (alpha_mask_2d * 255).astype(np.uint8)
result_image.putalpha(Image.fromarray(alpha_array, "L"))
return result_image
def calculate_new_dimensions_wan(
pil_image: Image.Image,
mod_val: int,
calculation_max_area: float,
min_slider_h: int,
max_slider_h: int,
min_slider_w: int,
max_slider_w: int,
default_h: int,
default_w: int,
) -> Tuple[int, int]:
orig_w, orig_h = pil_image.size
if orig_w <= 0 or orig_h <= 0:
return default_h, default_w
aspect_ratio = orig_h / orig_w
calc_h = round(np.sqrt(calculation_max_area * aspect_ratio))
calc_w = round(np.sqrt(calculation_max_area / aspect_ratio))
calc_h = max(mod_val, (calc_h // mod_val) * mod_val)
calc_w = max(mod_val, (calc_w // mod_val) * mod_val)
new_h = int(np.clip(calc_h, min_slider_h, (max_slider_h // mod_val) * mod_val))
new_w = int(np.clip(calc_w, min_slider_w, (max_slider_w // mod_val) * mod_val))
return new_h, new_w
def handle_gallery_upload_for_dims_wan(gallery_images, current_h_val, current_w_val):
if not gallery_images:
return gr.update(value=DEFAULT_H_SLIDER_VALUE), gr.update(value=DEFAULT_W_SLIDER_VALUE)
try:
first_image = gallery_images[0][0] if isinstance(gallery_images[0], (list, tuple)) else gallery_images[0]
first_image = remove_alpha_channel(first_image)
new_h, new_w = calculate_new_dimensions_wan(
first_image,
MOD_VALUE,
NEW_FORMULA_MAX_AREA,
SLIDER_MIN_H,
SLIDER_MAX_H,
SLIDER_MIN_W,
SLIDER_MAX_W,
DEFAULT_H_SLIDER_VALUE,
DEFAULT_W_SLIDER_VALUE,
)
return gr.update(value=new_h), gr.update(value=new_w)
except Exception:
gr.Warning("Error attempting to calculate new dimensions")
return gr.update(value=DEFAULT_H_SLIDER_VALUE), gr.update(value=DEFAULT_W_SLIDER_VALUE)
def update_prompt_from_mode(mode: str) -> str:
return MODE_PROMPTS.get(mode, "")
def prepare_video_and_mask_ref2v(height: int, width: int, num_frames: int):
frames = [Image.new("RGB", (width, height), (128, 128, 128)) for _ in range(num_frames)]
mask_white = Image.new("L", (width, height), 255)
mask = [mask_white for _ in range(num_frames)]
return frames, mask
def prepare_video_and_mask_flf2v(first_img: Image.Image, last_img: Image.Image, height: int, width: int, num_frames: int):
first_img = remove_alpha_channel(first_img).resize((width, height))
last_img = remove_alpha_channel(last_img).resize((width, height))
frames = [first_img]
frames.extend([Image.new("RGB", (width, height), (128, 128, 128)) for _ in range(num_frames - 2)])
frames.append(last_img)
mask_black = Image.new("L", (width, height), 0)
mask_white = Image.new("L", (width, height), 255)
mask = [mask_black] + [mask_white for _ in range(num_frames - 2)] + [mask_black]
return frames, mask
def calculate_random2v_frame_indices(num_images: int, num_frames: int) -> List[int]:
if num_images <= 0:
return []
if num_images == 1:
return [num_frames // 2]
if num_images >= num_frames:
return list(range(num_frames))
indices = []
step = (num_frames - 1) / (num_images - 1)
for i in range(num_images):
frame_idx = min(int(round(i * step)), num_frames - 1)
indices.append(frame_idx)
seen = set()
unique_indices = []
for idx in indices:
if idx not in seen:
seen.add(idx)
unique_indices.append(idx)
return unique_indices
def prepare_video_and_mask_random2v(images: List[Image.Image], frame_indices: List[int], height: int, width: int, num_frames: int):
clean_images = [remove_alpha_channel(img).resize((width, height)) for img in images]
frames = [Image.new("RGB", (width, height), (128, 128, 128)) for _ in range(num_frames)]
mask_black = Image.new("L", (width, height), 0)
mask_white = Image.new("L", (width, height), 255)
mask = [mask_white for _ in range(num_frames)]
for img, idx in zip(clean_images, frame_indices):
if idx >= num_frames:
raise ValueError(f"Frame index {idx} exceeds num_frames {num_frames}")
frames[idx] = img
mask[idx] = mask_black
return frames, mask
def normalize_gallery_images(gallery_images) -> List[Image.Image]:
images: List[Image.Image] = []
for item in gallery_images or []:
if isinstance(item, (list, tuple)) and item:
image = item[0]
else:
image = item
if not isinstance(image, Image.Image):
raise gr.Error("Gallery contains an invalid image item.")
images.append(image)
return images
def generate_video(
gallery_images,
mode,
prompt,
height,
width,
negative_prompt=DEFAULT_NEGATIVE_PROMPT,
duration_seconds=2.0,
guidance_scale=1.0,
steps=6,
seed=42,
randomize_seed=False,
remove_bg=False,
progress=gr.Progress(track_tqdm=True),
):
images = normalize_gallery_images(gallery_images)
if not images:
raise gr.Error("Please upload at least one image to the gallery.")
processed_images: List[Image.Image] = []
for image in images:
if mode == "Reference" and remove_bg:
image = remove_background_from_image(image)
image = remove_alpha_channel(image)
processed_images.append(image)
if mode == "First - Last Frame":
if len(processed_images) < 2:
raise gr.Error(f"First - Last Frame mode requires at least 2 images, but only {len(processed_images)} were supplied.")
processed_images = processed_images[:2]
target_h = max(MOD_VALUE, (int(height) // MOD_VALUE) * MOD_VALUE)
target_w = max(MOD_VALUE, (int(width) // MOD_VALUE) * MOD_VALUE)
num_frames = int(np.clip(int(round(duration_seconds * FIXED_FPS)), MIN_FRAMES_MODEL, MAX_FRAMES_MODEL))
current_seed = random.randint(0, MAX_SEED) if randomize_seed else int(seed)
if mode == "First - Last Frame":
frames, mask = prepare_video_and_mask_flf2v(processed_images[0], processed_images[1], target_h, target_w, num_frames)
reference_images = None
elif mode == "Reference":
frames, mask = prepare_video_and_mask_ref2v(target_h, target_w, num_frames)
reference_images = processed_images
else:
frame_indices = calculate_random2v_frame_indices(len(processed_images), num_frames)
frames, mask = prepare_video_and_mask_random2v(processed_images, frame_indices, target_h, target_w, num_frames)
reference_images = None
pipe = get_pipe()
clear_cuda()
generator_device = DEVICE if DEVICE == "cuda" else "cpu"
generator = torch.Generator(device=generator_device).manual_seed(current_seed)
with torch.inference_mode():
output_frames_list = pipe(
video=frames,
mask=mask,
reference_images=reference_images,
prompt=prompt,
negative_prompt=negative_prompt,
height=target_h,
width=target_w,
num_frames=num_frames,
guidance_scale=float(guidance_scale),
num_inference_steps=int(steps),
generator=generator,
).frames[0]
with tempfile.NamedTemporaryFile(suffix=".mp4", delete=False) as tmpfile:
video_path = tmpfile.name
export_to_video(output_frames_list, video_path, fps=FIXED_FPS)
clear_cuda()
return video_path, current_seed
control_modes = """
**3 Control Modes Available:**
- **Reference** Generate a video incorporating elements from input reference images
- **First - Last Frame** Generate a video using first and last frame conditioning defined by input images
- **Random Transitions** Generate a video with intermediate transitions between multiple input images
"""
with gr.Blocks() as demo:
gr.Markdown("# Fast 6 step Wan 2.1 VACE (14B)")
gr.Markdown(
"Using [**Wan2.1-VACE-14B**](https://huggingface.co/Wan-AI/Wan2.1-VACE-14B-diffusers) + "
"[**FusionX Phantom LoRA**](https://huggingface.co/vrgamedevgirl84/Wan14BT2VFusioniX) for fast video generation with multiple conditions."
)
gr.Markdown(control_modes)
with gr.Row():
with gr.Column():
with gr.Group():
mode_radio = gr.Radio(
choices=["Reference", "First - Last Frame", "Random Transitions"],
value="Reference",
label="Control Mode",
)
gallery_component = gr.Gallery(
label="Upload 1 or more images",
show_label=True,
elem_id="gallery",
columns=3,
rows=2,
object_fit="contain",
height="auto",
type="pil",
allow_preview=True,
)
remove_bg_checkbox = gr.Checkbox(
label="Remove Background",
value=False,
visible=True,
info="Removes background from input images. Best used in Reference mode.",
)
prompt_input = gr.Textbox(label="Prompt", value=MODE_PROMPTS["Reference"])
duration_seconds_input = gr.Slider(
minimum=round(MIN_FRAMES_MODEL / FIXED_FPS, 1),
maximum=round(MAX_FRAMES_MODEL / FIXED_FPS, 1),
step=0.1,
value=2.3,
label="Duration (seconds)",
info=f"Clamped to model's {MIN_FRAMES_MODEL}-{MAX_FRAMES_MODEL} frames at {FIXED_FPS} fps.",
)
with gr.Accordion("Advanced Settings", open=False):
negative_prompt_input = gr.Textbox(label="Negative Prompt", value=DEFAULT_NEGATIVE_PROMPT, lines=3)
seed_input = gr.Slider(label="Seed", minimum=0, maximum=MAX_SEED, step=1, value=42, interactive=True)
randomize_seed_checkbox = gr.Checkbox(label="Randomize seed", value=True, interactive=True)
with gr.Row():
height_input = gr.Slider(minimum=SLIDER_MIN_H, maximum=SLIDER_MAX_H, step=MOD_VALUE, value=DEFAULT_H_SLIDER_VALUE, label=f"Output Height (multiple of {MOD_VALUE})")
width_input = gr.Slider(minimum=SLIDER_MIN_W, maximum=SLIDER_MAX_W, step=MOD_VALUE, value=DEFAULT_W_SLIDER_VALUE, label=f"Output Width (multiple of {MOD_VALUE})")
steps_slider = gr.Slider(minimum=1, maximum=10, step=1, value=6, label="Inference Steps")
guidance_scale_input = gr.Slider(minimum=0.0, maximum=5.0, step=0.5, value=1.0, label="Guidance Scale", visible=False)
generate_button = gr.Button("Generate Video", variant="primary")
with gr.Column():
video_output = gr.Video(label="Generated Video", autoplay=True, interactive=False)
def update_bg_removal_visibility(mode: str):
return gr.update(visible=(mode == "Reference"))
mode_radio.change(fn=update_prompt_from_mode, inputs=[mode_radio], outputs=[prompt_input])
mode_radio.change(fn=update_bg_removal_visibility, inputs=[mode_radio], outputs=[remove_bg_checkbox])
gallery_component.change(
fn=handle_gallery_upload_for_dims_wan,
inputs=[gallery_component, height_input, width_input],
outputs=[height_input, width_input],
)
ui_inputs = [
gallery_component,
mode_radio,
prompt_input,
height_input,
width_input,
negative_prompt_input,
duration_seconds_input,
guidance_scale_input,
steps_slider,
seed_input,
randomize_seed_checkbox,
remove_bg_checkbox,
]
generate_button.click(fn=generate_video, inputs=ui_inputs, outputs=[video_output, seed_input])
if __name__ == "__main__":
demo.queue().launch(mcp_server=True)