import os import spaces import torch from diffusers import AutoencoderKLWan, WanImageToVideoPipeline, UniPCMultistepScheduler from diffusers.utils import export_to_video from transformers import CLIPVisionModel import gradio as gr import tempfile from huggingface_hub import hf_hub_download import numpy as np from PIL import Image import random MODEL_ID = "Wan-AI/Wan2.1-I2V-14B-480P-Diffusers" LORA_REPO_ID = "vrgamedevgirl84/Wan14BT2VFusioniX" LORA_FILENAME = "FusionX_LoRa/Wan2.1_I2V_14B_FusionX_LoRA.safetensors" #LORA_FILENAME = "Wan14Bi2vFusioniX_fp16.safetensors" image_encoder = CLIPVisionModel.from_pretrained(MODEL_ID, subfolder="image_encoder", torch_dtype=torch.float32) vae = AutoencoderKLWan.from_pretrained(MODEL_ID, subfolder="vae", torch_dtype=torch.float32) pipe = WanImageToVideoPipeline.from_pretrained( MODEL_ID, vae=vae, image_encoder=image_encoder, torch_dtype=torch.bfloat16 ) pipe.scheduler = UniPCMultistepScheduler.from_config(pipe.scheduler.config, flow_shift=8.0) pipe.to("cuda") try: causvid_path = hf_hub_download(repo_id=LORA_REPO_ID, filename=LORA_FILENAME) print("✅ LoRA downloaded to:", causvid_path) pipe.load_lora_weights(causvid_path, adapter_name="causvid_lora") pipe.set_adapters(["causvid_lora"], adapter_weights=[0.75]) pipe.fuse_lora() except Exception as e: import traceback print("❌ Error during LoRA loading:") traceback.print_exc() MOD_VALUE = 32 DEFAULT_H_SLIDER_VALUE = 640 DEFAULT_W_SLIDER_VALUE = 1024 NEW_FORMULA_MAX_AREA = 640.0 * 1024.0 SLIDER_MIN_H, SLIDER_MAX_H = 128, 1024 SLIDER_MIN_W, SLIDER_MAX_W = 128, 1024 MAX_SEED = np.iinfo(np.int32).max FIXED_FPS = 24 MIN_FRAMES_MODEL = 8 MAX_FRAMES_MODEL = 81 default_prompt_i2v = "make this image come alive, cinematic motion, smooth animation" 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" HF_MODEL = os.environ.get("HF_UPLOAD_REPO", "rahul7star/VideoExplain") from huggingface_hub import HfApi, upload_file import os import uuid import logging import os import uuid import logging from datetime import datetime from huggingface_hub import HfApi, upload_file import subprocess import tempfile import logging import shutil import os from huggingface_hub import HfApi, upload_file from datetime import datetime import uuid HF_MODEL = os.environ.get("HF_UPLOAD_REPO", "rahul7star/VideoExplain") def upscale_and_upload_4k(input_video_path: str, summary_text: str) -> str: """ Upscale a video to 4K and upload it to Hugging Face Hub without replacing the original file. Args: input_video_path (str): Path to the original video. summary_text (str): Text summary to upload alongside the video. Returns: str: Hugging Face folder path where the video and summary were uploaded. """ logging.info(f"Upscaling video to 4K for upload: {input_video_path}") # Create a temporary file for the upscaled video with tempfile.NamedTemporaryFile(suffix=".mp4", delete=False) as tmp_upscaled: upscaled_path = tmp_upscaled.name # FFmpeg upscale command cmd = [ "ffmpeg", "-i", input_video_path, "-vf", "scale=3840:2160:flags=lanczos", "-c:v", "libx264", "-crf", "18", "-preset", "slow", "-y", upscaled_path, ] try: subprocess.run(cmd, check=True, capture_output=True) logging.info(f"✅ Upscaled video created at: {upscaled_path}") except subprocess.CalledProcessError as e: logging.error(f"FFmpeg failed:\n{e.stderr.decode()}") raise # Create a date-based folder on HF today_str = datetime.now().strftime("%Y-%m-%d") unique_subfolder = f"Upload-4K-{uuid.uuid4().hex[:8]}" hf_folder = f"{today_str}/{unique_subfolder}" # Upload video video_filename = os.path.basename(input_video_path) video_hf_path = f"{hf_folder}/{video_filename}" upload_file( path_or_fileobj=upscaled_path, path_in_repo=video_hf_path, repo_id=HF_MODEL, repo_type="model", token=os.environ.get("HUGGINGFACE_HUB_TOKEN"), ) logging.info(f"✅ Uploaded 4K video to HF: {video_hf_path}") # Upload summary.txt summary_file = tempfile.NamedTemporaryFile(delete=False, suffix=".txt").name with open(summary_file, "w", encoding="utf-8") as f: f.write(summary_text) summary_hf_path = f"{hf_folder}/summary.txt" upload_file( path_or_fileobj=summary_file, path_in_repo=summary_hf_path, repo_id=HF_MODEL, repo_type="model", token=os.environ.get("HUGGINGFACE_HUB_TOKEN"), ) logging.info(f"✅ Uploaded summary to HF: {summary_hf_path}") # Cleanup temporary files os.remove(upscaled_path) os.remove(summary_file) return hf_folder def upload_to_hf(video_path, summary_text): api = HfApi() # Create a date-based folder (YYYY-MM-DD) today_str = datetime.now().strftime("%Y-%m-%d") date_folder = today_str # Generate a unique subfolder for this upload unique_subfolder = f"Wan21-I2V-upload_{uuid.uuid4().hex[:8]}" hf_folder = f"{date_folder}/{unique_subfolder}" logging.info(f"Uploading files to HF folder: {hf_folder} in repo {HF_MODEL}") # Upload video video_filename = os.path.basename(video_path) video_hf_path = f"{hf_folder}/{video_filename}" upload_file( path_or_fileobj=video_path, path_in_repo=video_hf_path, repo_id=HF_MODEL, repo_type="model", token=os.environ.get("HUGGINGFACE_HUB_TOKEN"), ) logging.info(f"✅ Uploaded video to HF: {video_hf_path}") # Upload summary.txt summary_file = "/tmp/summary.txt" with open(summary_file, "w", encoding="utf-8") as f: f.write(summary_text) summary_hf_path = f"{hf_folder}/summary.txt" upload_file( path_or_fileobj=summary_file, path_in_repo=summary_hf_path, repo_id=HF_MODEL, repo_type="model", token=os.environ.get("HUGGINGFACE_HUB_TOKEN"), ) logging.info(f"✅ Uploaded summary to HF: {summary_hf_path}") return hf_folder def _calculate_new_dimensions_wan(pil_image, mod_val, calculation_max_area, min_slider_h, max_slider_h, min_slider_w, max_slider_w, default_h, default_w): 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_image_upload_for_dims_wan(uploaded_pil_image, current_h_val, current_w_val): if uploaded_pil_image is None: return gr.update(value=DEFAULT_H_SLIDER_VALUE), gr.update(value=DEFAULT_W_SLIDER_VALUE) try: new_h, new_w = _calculate_new_dimensions_wan( uploaded_pil_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 as e: gr.Warning("Error attempting to calculate new dimensions") return gr.update(value=DEFAULT_H_SLIDER_VALUE), gr.update(value=DEFAULT_W_SLIDER_VALUE) def get_duration(input_image, prompt, height, width, negative_prompt, duration_seconds, guidance_scale, steps, seed, randomize_seed, progress): if steps > 4 and duration_seconds > 2: return 90 elif steps > 4 or duration_seconds > 2: return 75 else: return 60 @spaces.GPU(duration=get_duration) def generate_video(input_image, prompt, height, width, negative_prompt=default_negative_prompt, duration_seconds = 2, guidance_scale = 1, steps = 4, seed = 42, randomize_seed = False, progress=gr.Progress(track_tqdm=True)): if input_image is None: raise gr.Error("Please upload an input image.") target_h = max(MOD_VALUE, (int(height) // MOD_VALUE) * MOD_VALUE) target_w = max(MOD_VALUE, (int(width) // MOD_VALUE) * MOD_VALUE) num_frames = 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) resized_image = input_image.resize((target_w, target_h)) with torch.inference_mode(): output_frames_list = pipe( image=resized_image, 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=torch.Generator(device="cuda").manual_seed(current_seed) ).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) upscale_and_upload_4k(video_path, prompt) return video_path, current_seed with gr.Blocks() as demo: gr.Markdown("# Fast 4 steps Wan 2.1 I2V (14B) fusionx-lora") #gr.Markdown("[CausVid](https://github.com/tianweiy/CausVid) is a distilled version of Wan 2.1 to run faster in just 4-8 steps, [extracted as LoRA by Kijai](https://huggingface.co/Kijai/WanVideo_comfy/blob/main/Wan21_CausVid_14B_T2V_lora_rank32.safetensors) and is compatible with 🧨 diffusers") with gr.Row(): with gr.Column(): input_image_component = gr.Image(type="pil", label="Input Image (auto-resized to target H/W)") prompt_input = gr.Textbox(label="Prompt", value=default_prompt_i2v) 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, 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=30, step=1, value=4, label="Inference Steps") guidance_scale_input = gr.Slider(minimum=0.0, maximum=20.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) input_image_component.upload( fn=handle_image_upload_for_dims_wan, inputs=[input_image_component, height_input, width_input], outputs=[height_input, width_input] ) input_image_component.clear( fn=handle_image_upload_for_dims_wan, inputs=[input_image_component, height_input, width_input], outputs=[height_input, width_input] ) ui_inputs = [ input_image_component, prompt_input, height_input, width_input, negative_prompt_input, duration_seconds_input, guidance_scale_input, steps_slider, seed_input, randomize_seed_checkbox ] generate_button.click(fn=generate_video, inputs=ui_inputs, outputs=[video_output, seed_input]) gr.Examples( examples=[ ["peng.png", "a penguin playfully dancing in the snow, Antarctica", 896, 512], ["forg.jpg", "the frog jumps around", 448, 832], ], inputs=[input_image_component, prompt_input, height_input, width_input], outputs=[video_output, seed_input], fn=generate_video, cache_examples="lazy" ) if __name__ == "__main__": demo.queue().launch()