lllyasviel / lllyasviel/FramePack

Gradio Fails to Display or Download Generated Video in FramePack Despite Successful Generation

Open
#439 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
Python
Stars
17.3k
Forks
1.7k
PR merge metrics
No merged PRs in 30d

Description

Hi everyone,

I’m working on a video generation project using FramePack, and I’ve encountered an issue where Gradio fails to display or download the generated video, even though the generation process completes successfully. I’d greatly appreciate any insights or suggestions from the community!

Environment
I’m running FramePack on a system with an RTX 5070 Ti (14.57 GB VRAM). My setup includes:

OS: WSL (Windows Subsystem for Linux) on Ubuntu
Python: 3.10
PyTorch: 2.7.0+cu128 (installed with pip install -U xformers --index-url https://download.pytorch.org/whl/cu128)
CUDA: System version 12.9, PyTorch compatible with CUDA 12.8
Dependencies (matching the project’s requirements):
accelerate==1.6.0
diffusers==0.33.1
transformers==4.46.2
gradio==5.23.0
sentencepiece==0.2.0
pillow==11.1.0
av==12.1.0
numpy==1.26.2
scipy==1.12.0
requests==2.31.0
torchsde==0.2.6
flash-attn==2.7.4.post1
sageattention==2.1.1
xformers==0.0.30
The Gradio interface runs on http://127.0.0.1:7863.

What Works
Using the demo_gradio.py script (attached below), I can successfully generate a 5-second video with the prompt The girl dances gracefully, with clear movements, full of charm.:

The script processes 4 latent sections, generating a total of 289 frames (9.63 seconds at 30 FPS).
The video files are saved in the framepack/outputs directory:
250502_080351_652_5738_73.mp4 (original, 2.17 MB)
250502_080351_652_5738_73_reencoded.mp4 (re-encoded with ffmpeg to H.264/AAC for Gradio compatibility)
I can manually open the video using ffplay, confirming that the generation process works perfectly.
The log (framepack_20250502_075231.log, attached) shows the process completes without errors:

text

複製
2025-05-02 08:18:28,497 - INFO - Video file generated successfully: /home/kaworukevin/framepack_new/framepack/outputs/250502_080351_652_5738_73.mp4, size: 2173493 bytes
2025-05-02 08:18:28,497 - INFO - Using /outputs/ path for Gradio: /outputs/250502_080351_652_5738_73.mp4
2025-05-02 08:18:28,497 - INFO - Received 'file' event: output_filename=/outputs/250502_080351_652_5738_73.mp4
2025-05-02 08:18:28,858 - INFO - Received 'end' event, final output_filename=/outputs/250502_080351_652_5738_73.mp4
The Issue
Despite the successful generation, Gradio fails to display the video in the UI:

The progress bar remains stuck at the last message: Total generated frames: 249, Video length: 8.30 seconds (FPS-30). The video is being extended now ... (note: the frame count in the UI message is incorrect due to inverted sampling; the final video has 289 frames).
The Finished Frames video component doesn’t show the video, and the download button doesn’t work.
The Test Gradio Render tab (which tries to render an existing video at /outputs/250502_061840_966_8996_37.mp4) also fails to display the video.
What I’ve Tried
Static File Mapping:
I mapped the outputs directory as a static resource using additional_paths={"/outputs": outputs_folder} in block.launch.
The script returns the video path as /outputs/250502_080351_652_5738_73.mp4, which should be accessible via the Gradio server.
I confirmed the file exists and is accessible at /home/kaworukevin/framepack_new/framepack/outputs/250502_080351_652_5738_73.mp4.
File Path Variations:
Initially, the script used a relative path (outputs/xxx.mp4), which didn’t work.
I also tried an absolute path (/home/kaworukevin/framepack_new/framepack/outputs/xxx.mp4), but Gradio still couldn’t display the video.
Video Compatibility:
The video is re-encoded using ffmpeg to H.264 with AAC audio, which should be compatible with Gradio’s Video component.
The re-encoded file plays fine in ffplay and web browsers.
File Permissions:
I ensured the outputs directory and files have proper permissions (chmod -R 755 outputs).
UI State Updates:
The script pushes file and end events to Gradio, and the process function yields the video path and UI updates correctly (as shown in the log).
I modified the end event to clear the progress message ("Video generation completed."), but the video still doesn’t display.
Gradio Settings:
The Video component has show_share_button=True to enable downloading, but the button doesn’t work.
I’m using Gradio 5.23.0, as recommended by the project author.
Questions
Why is Gradio failing to display the video, even though the file path (/outputs/xxx.mp4) is correctly mapped and the file exists?
Could this be related to Gradio’s static file serving or the Video component’s compatibility with the file path format?
Are there specific Gradio configurations (e.g., for static file serving or the Video component) that I might be missing?
Is there a way to debug Gradio’s internal handling of the video path to see why it’s not rendering?
Would upgrading Gradio to a newer version (e.g., 5.28.0) help, or are there known issues with Gradio 5.23.0 in this context?
I’ve attached the script (demo_gradio.py) and the log (framepack_20250502_075231.log) for reference. Any help or suggestions would be greatly appreciated—I’m so close to getting this fully working!

Best regards,

Kevin
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------demo_gradio.py

framepack_20250502_075231.log

import os
import logging
import subprocess
import httpx
import sys
import traceback
from datetime import datetime
import gradio as gr
import torch
import einops
import safetensors.torch as sf
import numpy as np
import argparse
import math
from PIL import Image
import pkg_resources # 用於檢查 Gradio 版本

導入 FramePack 相關模組

from diffusers_helper.hf_login import login
from diffusers import AutoencoderKLHunyuanVideo
from transformers import LlamaModel, CLIPTextModel, LlamaTokenizerFast, CLIPTokenizer
from diffusers_helper.hunyuan import encode_prompt_conds, vae_decode, vae_encode, vae_decode_fake
from diffusers_helper.utils import save_bcthw_as_mp4, crop_or_pad_yield_mask, soft_append_bcthw, resize_and_center_crop, state_dict_weighted_merge, state_dict_offset_merge, generate_timestamp
from diffusers_helper.models.hunyuan_video_packed import HunyuanVideoTransformer3DModelPacked
from diffusers_helper.pipelines.k_diffusion_hunyuan import sample_hunyuan
from diffusers_helper.memory import cpu, gpu, get_cuda_free_memory_gb, move_model_to_device_with_memory_preservation, offload_model_from_device_for_memory_preservation, fake_diffusers_current_device, DynamicSwapInstaller, unload_complete_models, load_model_as_complete
from diffusers_helper.thread_utils import AsyncStream, async_run
from diffusers_helper.gradio.progress_bar import make_progress_bar_css, make_progress_bar_html
from transformers import SiglipImageProcessor, SiglipVisionModel
from diffusers_helper.clip_vision import hf_clip_vision_encode
from diffusers_helper.bucket_tools import find_nearest_bucket

設置 HF_HOME 路徑

os.environ['HF_HOME'] = os.path.abspath(os.path.realpath(os.path.join(os.path.dirname(file), './hf_download')))

啟用 Gradio 調試模式

os.environ["GRADIO_DEBUG"] = "1"

設置日誌記錄

logs_dir = os.path.join(os.path.dirname(file), 'logs')
os.makedirs(logs_dir, exist_ok=True)
log_filename = os.path.join(logs_dir, f'framepack_{datetime.now().strftime("%Y%m%d_%H%M%S")}.log')

創建日誌處理器

file_handler = logging.FileHandler(log_filename)
stream_handler = logging.StreamHandler()
formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
file_handler.setFormatter(formatter)
stream_handler.setFormatter(formatter)

logging.basicConfig(
level=logging.DEBUG,
handlers=[
file_handler,
stream_handler
]
)

logger = logging.getLogger(name)

設置 Gradio 及相關庫的日誌級別為 DEBUG,並使用相同的處理器

for name in ["gradio", "httpcore", "httpx", "uvicorn", "uvicorn.error", "uvicorn.access", "gradio.routes"]:
log = logging.getLogger(name)
log.setLevel(logging.DEBUG)
log.handlers = [file_handler, stream_handler]

設置 uvicorn 的日誌級別為 DEBUG

uvicorn_logger = logging.getLogger("uvicorn")
uvicorn_logger.setLevel(logging.DEBUG)
uvicorn_logger.handlers = [file_handler, stream_handler]

自定義異常處理器

def handle_exception(exc_type, exec_value, exc_traceback):
logger.error("Uncaught exception: %s", exec_value, exc_info=(exc_type, exec_value, exc_traceback))

設置全局異常處理器

sys.excepthook = handle_exception

解析命令行參數

parser = argparse.ArgumentParser()
parser.add_argument('--share', action='store_true', help='Share the Gradio app publicly')
parser.add_argument("--server", type=str, default='0.0.0.0', help='Server address to run Gradio')
parser.add_argument("--port", type=int, required=False, help='Port to run Gradio server')
parser.add_argument("--inbrowser", type=lambda x: x.lower() == 'true', default=False, help='Whether to open in browser automatically')
args = parser.parse_args()

logger.info(f"Parsed arguments: {args}")

檢查 Gradio 版本

try:
gradio_version = pkg_resources.get_distribution("gradio").version
logger.info(f"Current Gradio version: {gradio_version}")
if gradio_version != "5.23.0":
logger.warning("Author recommends using gradio==5.23.0. You may need to run 'pip install gradio==5.23.0' to match the recommended version.")
except pkg_resources.DistributionNotFound:
logger.error("Gradio is not installed. Please install it using 'pip install gradio==5.23.0'.")
sys.exit(1)

初始化環境

free_mem_gb = get_cuda_free_memory_gb(gpu)
high_vram = free_mem_gb > 60

logger.info(f'Free VRAM {free_mem_gb} GB')
logger.info(f'High-VRAM Mode: {high_vram}')

加載模型

text_encoder = LlamaModel.from_pretrained(
"hunyuanvideo-community/HunyuanVideo",
subfolder='text_encoder',
torch_dtype=torch.float16
).cpu()
text_encoder_2 = CLIPTextModel.from_pretrained(
"hunyuanvideo-community/HunyuanVideo",
subfolder='text_encoder_2',
torch_dtype=torch.float16
).cpu()
tokenizer = LlamaTokenizerFast.from_pretrained(
"hunyuanvideo-community/HunyuanVideo",
subfolder='tokenizer'
)
tokenizer_2 = CLIPTokenizer.from_pretrained(
"hunyuanvideo-community/HunyuanVideo",
subfolder='tokenizer_2'
)
vae = AutoencoderKLHunyuanVideo.from_pretrained(
"hunyuanvideo-community/HunyuanVideo",
subfolder='vae',
torch_dtype=torch.float16
).cpu()

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
).cpu()

transformer = HunyuanVideoTransformer3DModelPacked.from_pretrained(
'lllyasviel/FramePackI2V_HY',
torch_dtype=torch.bfloat16
).cpu()

vae.eval()
text_encoder.eval()
text_encoder_2.eval()
image_encoder.eval()
transformer.eval()

if not high_vram:
vae.enable_slicing()
vae.enable_tiling()

transformer.high_quality_fp32_output_for_inference = True
logger.info('transformer.high_quality_fp32_output_for_inference = True')

transformer.to(dtype=torch.bfloat16)
vae.to(dtype=torch.float16)
image_encoder.to(dtype=torch.float16)
text_encoder.to(dtype=torch.float16)
text_encoder_2.to(dtype=torch.float16)

vae.requires_grad_(False)
text_encoder.requires_grad_(False)
text_encoder_2.requires_grad_(False)
image_encoder.requires_grad_(False)
transformer.requires_grad_(False)

if not high_vram:
DynamicSwapInstaller.install_model(transformer, device=gpu)
DynamicSwapInstaller.install_model(text_encoder, device=gpu)
else:
text_encoder.to(gpu)
text_encoder_2.to(gpu)
image_encoder.to(gpu)
vae.to(gpu)
transformer.to(gpu)

stream = AsyncStream()

確保 outputs_folder 路徑正確

outputs_folder = os.path.abspath(os.path.join(os.path.dirname(file), './outputs/'))
os.makedirs(outputs_folder, exist_ok=True)
logger.info(f"Outputs folder created or exists at: {outputs_folder}")

檢查 ffmpeg 是否可用

def check_ffmpeg():
try:
subprocess.run(["ffmpeg", "-version"], check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
logger.info("ffmpeg is available")
return True
except (subprocess.CalledProcessError, FileNotFoundError):
logger.warning("ffmpeg is not available, video re-encoding will be skipped")
return False

重新編碼視頻為 Gradio 支援的格式

def reencode_video(input_path, output_path):
if not check_ffmpeg():
logger.warning(f"Skipping re-encoding due to missing ffmpeg: {input_path}")
return input_path
try:
cmd = [
"ffmpeg", "-y", "-i", input_path,
"-c:v", "libx264", "-preset", "medium", "-crf", "23",
"-c:a", "aac", "-b:a", "128k", "-strict", "-2",
output_path
]
subprocess.run(cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
logger.info(f"Successfully re-encoded video: {output_path}")
return output_path
except subprocess.CalledProcessError as e:
logger.error(f"Error re-encoding video: {str(e)}", exc_info=True)
return input_path

@torch.no_grad()
def worker(input_image, prompt, n_prompt, seed, total_second_length, latent_window_size, steps, cfg, gs, rs, gpu_memory_preservation, use_teacache, mp4_crf):
total_latent_sections = (total_second_length * 30) / (latent_window_size * 4)
total_latent_sections = int(max(round(total_latent_sections), 1))
logger.info(f"Calculated total_latent_sections: {total_latent_sections}")

job_id = generate_timestamp()
logger.info(f"Generated job_id: {job_id}")

stream.output_queue.push(('progress', (None, '', make_progress_bar_html(0, 'Starting ...'))))
logger.info('Worker started: Job ID %s', job_id)

try:
    if not high_vram:
        logger.info("Unloading models due to low VRAM")
        unload_complete_models(
            text_encoder, text_encoder_2, image_encoder, vae, transformer
        )

    stream.output_queue.push(('progress', (None, '', make_progress_bar_html(0, 'Text encoding ...'))))
    logger.info('Text encoding started')

    if not high_vram:
        logger.info("Moving text_encoder to GPU")
        fake_diffusers_current_device(text_encoder, gpu)
        logger.info("Loading text_encoder_2 to GPU")
        load_model_as_complete(text_encoder_2, target_device=gpu)

    logger.info("Encoding prompt conditions")
    llama_vec, clip_l_pooler = encode_prompt_conds(prompt, text_encoder, text_encoder_2, tokenizer, tokenizer_2)
    logger.info(f"Encoded prompt: llama_vec shape {llama_vec.shape}, clip_l_pooler shape {clip_l_pooler.shape}")

    if cfg == 1:
        logger.info("CFG Scale is 1, setting negative prompt embeddings to zeros")
        llama_vec_n, clip_l_pooler_n = torch.zeros_like(llama_vec), torch.zeros_like(clip_l_pooler)
    else:
        logger.info("Encoding negative prompt conditions")
        llama_vec_n, clip_l_pooler_n = encode_prompt_conds(n_prompt, text_encoder, text_encoder_2, tokenizer, tokenizer_2)
    logger.info(f"Encoded negative prompt: llama_vec_n shape {llama_vec_n.shape}, clip_l_pooler_n shape {clip_l_pooler_n.shape}")

    logger.info("Cropping or padding prompt embeddings")
    llama_vec, llama_attention_mask = crop_or_pad_yield_mask(llama_vec, length=512)
    llama_vec_n, llama_attention_mask_n = crop_or_pad_yield_mask(llama_vec_n, length=512)

    stream.output_queue.push(('progress', (None, '', make_progress_bar_html(0, 'Image processing ...'))))
    logger.info('Image processing started')

    H, W, C = input_image.shape
    logger.info(f"Input image dimensions: H={H}, W={W}, C={C}")
    height, width = find_nearest_bucket(H, W, resolution=640)
    logger.info(f"Nearest bucket dimensions: height={height}, width={width}")
    input_image_np = resize_and_center_crop(input_image, target_width=width, target_height=height)

    logger.info(f"Saving processed image to {os.path.join(outputs_folder, f'{job_id}.png')}")
    Image.fromarray(input_image_np).save(os.path.join(outputs_folder, f'{job_id}.png'))

    input_image_pt = torch.from_numpy(input_image_np).float() / 127.5 - 1
    input_image_pt = input_image_pt.permute(2, 0, 1)[None, :, None]
    logger.info(f"Converted image to tensor: shape {input_image_pt.shape}")

    stream.output_queue.push(('progress', (None, '', make_progress_bar_html(0, 'VAE encoding ...'))))
    logger.info('VAE encoding started')

    if not high_vram:
        logger.info("Loading VAE to GPU")
        load_model_as_complete(vae, target_device=gpu)

    start_latent = vae_encode(input_image_pt, vae)
    logger.info(f"VAE encoded latent: shape {start_latent.shape}")

    stream.output_queue.push(('progress', (None, '', make_progress_bar_html(0, 'CLIP Vision encoding ...'))))
    logger.info('CLIP Vision encoding started')

    if not high_vram:
        logger.info("Loading image_encoder to GPU")
        load_model_as_complete(image_encoder, target_device=gpu)

    image_encoder_output = hf_clip_vision_encode(input_image_np, feature_extractor, image_encoder)
    image_encoder_last_hidden_state = image_encoder_output.last_hidden_state
    logger.info(f"CLIP Vision encoded: last_hidden_state shape {image_encoder_last_hidden_state.shape}")

    logger.info("Converting embeddings to transformer dtype")
    llama_vec = llama_vec.to(transformer.dtype)
    llama_vec_n = llama_vec_n.to(transformer.dtype)
    clip_l_pooler = clip_l_pooler.to(transformer.dtype)
    clip_l_pooler_n = clip_l_pooler_n.to(transformer.dtype)
    image_encoder_last_hidden_state = image_encoder_last_hidden_state.to(transformer.dtype)

    stream.output_queue.push(('progress', (None, '', make_progress_bar_html(0, 'Start sampling ...'))))
    logger.info('Start sampling')

    rnd = torch.Generator("cpu").manual_seed(seed)
    logger.info(f"Initialized random generator with seed: {seed}")
    num_frames = latent_window_size * 4 - 3
    logger.info(f"Calculated number of frames: {num_frames}")

    history_latents = torch.zeros(size=(1, 16, 1 + 2 + 16, height // 8, width // 8), dtype=torch.float32).cpu()
    history_pixels = None
    total_generated_latent_frames = 0
    logger.info(f"Initialized history_latents: shape {history_latents.shape}")

    # 修復迭代器耗盡問題:將 reversed(range()) 轉換為列表
    latent_paddings = list(reversed(range(total_latent_sections)))
    if total_latent_sections > 4:
        latent_paddings = [3] + [2] * (total_latent_sections - 3) + [1, 0]
    logger.info(f"Latent paddings: {latent_paddings}")

    if not latent_paddings:
        logger.error("latent_paddings is empty, cannot proceed with sampling")
        raise ValueError("latent_paddings is empty, cannot proceed with sampling")

    for latent_padding in latent_paddings:
        is_last_section = latent_padding == 0
        latent_padding_size = latent_padding * latent_window_size

        if stream.input_queue.top() == 'end':
            stream.output_queue.push(('end', None))
            logger.info('Worker ended by user')
            return

        logger.info(f'latent_padding_size = {latent_padding_size}, is_last_section = {is_last_section}')

        indices = torch.arange(0, sum([1, latent_padding_size, latent_window_size, 1, 2, 16])).unsqueeze(0)
        clean_latent_indices_pre, blank_indices, latent_indices, clean_latent_indices_post, clean_latent_2x_indices, clean_latent_4x_indices = indices.split([1, latent_padding_size, latent_window_size, 1, 2, 16], dim=1)
        clean_latent_indices = torch.cat([clean_latent_indices_pre, clean_latent_indices_post], dim=1)
        logger.info(f"Indices split: clean_latent_indices_pre={clean_latent_indices_pre.shape}, blank_indices={blank_indices.shape}, latent_indices={latent_indices.shape}, clean_latent_indices_post={clean_latent_indices_post.shape}, clean_latent_2x_indices={clean_latent_2x_indices.shape}, clean_latent_4x_indices={clean_latent_4x_indices.shape}")

        clean_latents_pre = start_latent.to(history_latents)
        clean_latents_post, clean_latents_2x, clean_latents_4x = history_latents[:, :, :1 + 2 + 16, :, :].split([1, 2, 16], dim=2)
        clean_latents = torch.cat([clean_latents_pre, clean_latents_post], dim=2)
        logger.info(f"Prepared latents: clean_latents_pre shape {clean_latents_pre.shape}, clean_latents_post shape {clean_latents_post.shape}, clean_latents_2x shape {clean_latents_2x.shape}, clean_latents_4x shape {clean_latents_4x.shape}")

        if not high_vram:
            logger.info("Unloading models before sampling")
            unload_complete_models()
            logger.info(f"Moving transformer to GPU with preserved memory: {gpu_memory_preservation} GB")
            move_model_to_device_with_memory_preservation(transformer, target_device=gpu, preserved_memory_gb=gpu_memory_preservation)

        if use_teacache:
            logger.info(f"Initializing TeaCache with {steps} steps")
            transformer.initialize_teacache(enable_teacache=True, num_steps=steps)
        else:
            logger.info("Disabling TeaCache")
            transformer.initialize_teacache(enable_teacache=False)

        def callback(d):
            preview = d['denoised']
            preview = vae_decode_fake(preview)

            preview = (preview * 255.0).detach().cpu().numpy().clip(0, 255).astype(np.uint8)
            preview = einops.rearrange(preview, 'b c t h w -> (b h) (t w) c')
            logger.info(f"Callback: Generated preview with shape {preview.shape}")

            if stream.input_queue.top() == 'end':
                logger.info("Callback: User ended the task")
                stream.output_queue.push(('end', None))
                logger.info('User ended the task during sampling')
                raise KeyboardInterrupt('User ends the task.')

            current_step = d['i'] + 1
            percentage = int(100.0 * current_step / steps)
            hint = f'Sampling {current_step}/{steps}'
            desc = f'Total generated frames: {int(max(0, total_generated_latent_frames * 4 - 3))}, Video length: {max(0, (total_generated_latent_frames * 4 - 3) / 30) :.2f} seconds (FPS-30). The video is being extended now ...'
            logger.info(f"Callback: Pushing progress - step {current_step}/{steps}, percentage {percentage}%, desc: {desc}")
            stream.output_queue.push(('progress', (preview, desc, make_progress_bar_html(percentage, hint))))
            logger.info(f'Sampling step {current_step}/{steps}')
            return

        logger.info("Starting sample_hunyuan with parameters: "
                    f"width={width}, height={height}, frames={num_frames}, "
                    f"real_guidance_scale={cfg}, distilled_guidance_scale={gs}, "
                    f"guidance_rescale={rs}, num_inference_steps={steps}")
        generated_latents = 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_attention_mask,
            prompt_poolers=clip_l_pooler,
            negative_prompt_embeds=llama_vec_n,
            negative_prompt_embeds_mask=llama_attention_mask_n,
            negative_prompt_poolers=clip_l_pooler_n,
            device=gpu,
            dtype=torch.bfloat16,
            image_embeddings=image_encoder_last_hidden_state,
            latent_indices=latent_indices,
            clean_latents=clean_latents,
            clean_latent_indices=clean_latent_indices,
            clean_latents_2x=clean_latents_2x,
            clean_latent_2x_indices=clean_latent_2x_indices,
            clean_latents_4x=clean_latents_4x,
            clean_latent_4x_indices=clean_latent_4x_indices,
            callback=callback,
        )
        logger.info(f"Generated latents: shape {generated_latents.shape}")

        if is_last_section:
            logger.info("Last section: Concatenating start_latent with generated_latents")
            generated_latents = torch.cat([start_latent.to(generated_latents), generated_latents], dim=2)

        total_generated_latent_frames += int(generated_latents.shape[2])
        history_latents = torch.cat([generated_latents.to(history_latents), history_latents], dim=2)
        logger.info(f"Updated history_latents: total_generated_latent_frames={total_generated_latent_frames}, history_latents shape {history_latents.shape}")

        if not high_vram:
            logger.info("Offloading transformer from GPU with preserved memory: 8 GB")
            offload_model_from_device_for_memory_preservation(transformer, target_device=gpu, preserved_memory_gb=8)
            logger.info("Loading VAE to GPU for decoding")
            load_model_as_complete(vae, target_device=gpu)

        real_history_latents = history_latents[:, :, :total_generated_latent_frames, :, :]
        logger.info(f"Real history latents: shape {real_history_latents.shape}")

        if history_pixels is None:
            logger.info("Decoding real_history_latents to history_pixels")
            history_pixels = vae_decode(real_history_latents, vae).cpu()
        else:
            section_latent_frames = (latent_window_size * 2 + 1) if is_last_section else (latent_window_size * 2)
            overlapped_frames = latent_window_size * 4 - 3
            logger.info(f"Decoding section_latent_frames: {section_latent_frames}, overlapped_frames: {overlapped_frames}")
            current_pixels = vae_decode(real_history_latents[:, :, :section_latent_frames], vae).cpu()
            history_pixels = soft_append_bcthw(current_pixels, history_pixels, overlapped_frames)
        logger.info(f"History pixels: shape {history_pixels.shape}")

        if not high_vram:
            logger.info("Unloading models after decoding")
            unload_complete_models()

        output_filename = os.path.join(outputs_folder, f'{job_id}_{total_generated_latent_frames}.mp4')
        logger.info(f"Saving video to {output_filename} with fps=30, crf={mp4_crf}")

        save_bcthw_as_mp4(history_pixels, output_filename, fps=30, crf=mp4_crf)

        logger.info(f'Decoded. Current latent shape {real_history_latents.shape}; pixel shape {history_pixels.shape}')

        # 檢查檔案是否成功生成並可訪問
        if os.path.exists(output_filename):
            logger.info(f"Video file generated successfully: {output_filename}, size: {os.path.getsize(output_filename)} bytes")
        else:
            logger.error(f"Video file not found after saving: {output_filename}")
            raise FileNotFoundError(f"Video file not found: {output_filename}")

        # 重新編碼視頻
        reencoded_filename = os.path.join(outputs_folder, f'{job_id}_{total_generated_latent_frames}_reencoded.mp4')
        final_output = reencode_video(output_filename, reencoded_filename)

        # 使用 /outputs/ 路徑傳回 Gradio
        final_output_rel = f"/outputs/{os.path.basename(final_output)}"
        logger.info(f"Using /outputs/ path for Gradio: {final_output_rel}")
        # 確認檔案是否存在
        if not os.path.exists(final_output):
            logger.error(f"File does not exist: {final_output}")
            raise FileNotFoundError(f"File does not exist: {final_output}")
        stream.output_queue.push(('file', final_output_rel))

        if is_last_section:
            logger.info("Last section reached, breaking loop")
            break
except Exception as e:
    logger.error('Error in worker: %s', str(e), exc_info=True)

    if not high_vram:
        logger.info("Unloading models due to error")
        unload_complete_models(
            text_encoder, text_encoder_2, image_encoder, vae, transformer
        )

logger.info("Pushing end event to stream")
stream.output_queue.push(('end', None))
logger.info('Worker completed')
return

@torch.no_grad()
def process(input_image, prompt, n_prompt, seed, total_second_length, latent_window_size, steps, cfg, gs, rs, gpu_memory_preservation, use_teacache, mp4_crf):
global stream
assert input_image is not None, 'No input image!'

logger.info("Starting process with parameters: "
            f"prompt={prompt}, n_prompt={n_prompt}, seed={seed}, "
            f"total_second_length={total_second_length}, latent_window_size={latent_window_size}, "
            f"steps={steps}, cfg={cfg}, gs={gs}, rs={rs}, "
            f"gpu_memory_preservation={gpu_memory_preservation}, use_teacache={use_teacache}, mp4_crf={mp4_crf}")

logger.info("Yielding initial UI state: disabling Start button, enabling End button")
try:
    yield None, None, '', '', gr.update(interactive=False), gr.update(interactive=True)
except Exception as e:
    logger.error(f"Error while yielding initial UI state: {str(e)}", exc_info=True)
    raise

stream = AsyncStream()
logger.info("Initialized AsyncStream for processing")

logger.info("Starting async_run for worker")
try:
    async_run(worker, input_image, prompt, n_prompt, seed, total_second_length, latent_window_size, steps, cfg, gs, rs, gpu_memory_preservation, use_teacache, mp4_crf)
except Exception as e:
    logger.error(f"Error in async_run: {str(e)}", exc_info=True)
    yield None, None, f"Error: {str(e)}", '', gr.update(interactive=True), gr.update(interactive=False)
    return

output_filename = None
logger.info("Initialized output_filename as None")

while True:
    logger.info("Waiting for next event from stream.output_queue")
    try:
        flag, data = stream.output_queue.next()
        logger.info(f"Received event from stream: flag={flag}, data={data}")
    except Exception as e:
        logger.error(f"Error while fetching next event from stream: {str(e)}", exc_info=True)
        yield None, None, f"Error: {str(e)}", '', gr.update(interactive=True), gr.update(interactive=False)
        break

    if flag == 'file':
        output_filename = data
        logger.info(f"Received 'file' event: output_filename={output_filename}")
        logger.info(f"Yielding file event to Gradio: {output_filename}")
        try:
            yield output_filename, gr.update(), gr.update(), gr.update(), gr.update(interactive=False), gr.update(interactive=True)
        except Exception as e:
            logger.error(f"Error while yielding file event to Gradio: {str(e)}", exc_info=True)
            yield None, None, f"Error: {str(e)}", '', gr.update(interactive=True), gr.update(interactive=False)
            continue

    if flag == 'progress':
        preview, desc, html = data
        logger.info(f"Received 'progress' event: desc={desc}, preview shape={preview.shape if preview is not None else 'None'}")
        logger.info(f"Yielding progress event to Gradio: desc={desc}")
        try:
            yield gr.update(), gr.update(visible=True, value=preview), desc, html, gr.update(interactive=False), gr.update(interactive=True)
        except Exception as e:
            logger.error(f"Error while yielding progress event to Gradio: {str(e)}", exc_info=True)
            yield None, None, f"Error: {str(e)}", '', gr.update(interactive=True), gr.update(interactive=False)
            continue

    if flag == 'end':
        logger.info(f"Received 'end' event, final output_filename={output_filename}")
        logger.info("Yielding end event to Gradio")
        try:
            yield output_filename, gr.update(visible=False), "Video generation completed.", '', gr.update(interactive=True), gr.update(interactive=False)
        except Exception as e:
            logger.error(f"Error while yielding end event to Gradio: {str(e)}", exc_info=True)
            yield None, None, f"Error: {str(e)}", '', gr.update(interactive=True), gr.update(interactive=False)
        break

def end_process():
logger.info("End process triggered by user")
stream.input_queue.push('end')
logger.info('Process ended by user')

獨立測試 Gradio 渲染

def test_gradio_render():
video_path = "outputs/250502_061840_966_8996_37.mp4"
final_path = f"/outputs/{os.path.basename(video_path)}"
if os.path.exists(os.path.join(os.path.dirname(file), video_path)):
logger.info(f"Testing Gradio render with video: {final_path}")
return final_path
else:
logger.error(f"Video file for testing not found: {video_path}")
return "Video file not found"

quick_prompts = [
'The girl dances gracefully, with clear movements, full of charm.',
'A character doing some simple body movements.',
]
quick_prompts = [[x] for x in quick_prompts]

css = make_progress_bar_css()
block = gr.Blocks(css=css).queue()
with block:
gr.Markdown('# FramePack')
with gr.Tabs():
with gr.Tab(label="Generate Video"):
with gr.Row():
with gr.Column():
input_image = gr.Image(sources='upload', type="numpy", label="Image", height=320)
prompt = gr.Textbox(label="Prompt", value='')
example_quick_prompts = gr.Dataset(samples=quick_prompts, label='Quick List', samples_per_page=1000, components=[prompt])
example_quick_prompts.click(lambda x: x[0], inputs=[example_quick_prompts], outputs=prompt, show_progress=False, queue=False)

                with gr.Row():
                    start_button = gr.Button(value="Start Generation")
                    end_button = gr.Button(value="End Generation", interactive=False)

                with gr.Group():
                    use_teacache = gr.Checkbox(label='Use TeaCache', value=True, info='Faster speed, but often makes hands and fingers slightly worse.')
                    n_prompt = gr.Textbox(label="Negative Prompt", value="", visible=False)
                    seed = gr.Number(label="Seed", value=31337, precision=0)
                    total_second_length = gr.Slider(label="Total Video Length (Seconds)", minimum=1, maximum=120, value=5, step=0.1)
                    latent_window_size = gr.Slider(label="Latent Window Size", minimum=1, maximum=33, value=9, step=1, visible=False)
                    steps = gr.Slider(label="Steps", minimum=1, maximum=100, value=25, step=1, info='Changing this value is not recommended.')
                    cfg = gr.Slider(label="CFG Scale", minimum=1.0, maximum=32.0, value=1.0, step=0.01, visible=False)
                    gs = gr.Slider(label="Distilled CFG Scale", minimum=1.0, maximum=32.0, value=10.0, step=0.01, info='Changing this value is not recommended.')
                    rs = gr.Slider(label="CFG Re-Scale", minimum=0.0, maximum=1.0, value=0.0, step=0.01, visible=False)
                    gpu_memory_preservation = gr.Slider(label="GPU Inference Preserved Memory (GB) (larger means slower)", minimum=6, maximum=128, value=6, step=0.1, info="Set this number to a larger value if you encounter OOM. Larger value causes slower speed.")
                    mp4_crf = gr.Slider(label="MP4 Compression", minimum=0, maximum=100, value=16, step=1, info="Lower means better quality. 0 is uncompressed. Change to 16 if you get black outputs.")

            with gr.Column():
                preview_image = gr.Image(label="Next Latents", height=200, visible=False)
                result_video = gr.Video(label="Finished Frames", autoplay=True, show_share_button=True, height=512, loop=True, format="mp4")
                gr.Markdown('Note that the ending actions will be generated before the starting actions due to the inverted sampling. If the starting action is not in the video, you just need to wait, and it will be generated later.')
                progress_desc = gr.Markdown('', elem_classes='no-generating-animation')
                progress_bar = gr.HTML('', elem_classes='no-generating-animation')

        gr.HTML('<div style="text-align:center; margin-top:20px;">Share your results and find ideas at the <a href="https://x.com/search?q=framepack&f=live" target="_blank">FramePack Twitter (X) thread</a></div>')

        ips = [input_image, prompt, n_prompt, seed, total_second_length, latent_window_size, steps, cfg, gs, rs, gpu_memory_preservation, use_teacache, mp4_crf]
        start_button.click(fn=process, inputs=ips, outputs=[result_video, preview_image, progress_desc, progress_bar, start_button, end_button])
        end_button.click(fn=end_process)

    with gr.Tab(label="Test Gradio Render"):
        test_button = gr.Button(value="Test Gradio Video Rendering")
        test_output = gr.Video(label="Test Render", format="mp4", show_share_button=True)
        test_button.click(fn=test_gradio_render, outputs=test_output)

獲取 WSL 的 IP 地址

try:
ip_output = subprocess.check_output("ip addr show eth0 | grep 'inet ' | awk '{print $2}' | cut -d/ -f1", shell=True, text=True).strip()
wsl_ip = ip_output if ip_output else "172.17.0.1"
except subprocess.CalledProcessError:
wsl_ip = "172.17.0.1"
logger.info(f"WSL IP address: {wsl_ip}")

設置 Gradio 環境變數

os.environ["GRADIO_SERVER_NAME"] = wsl_ip
os.environ["GRADIO_SERVER_PORT"] = str(args.port)

禁用 Gradio 的 localhost 檢查

def mock_httpx_head(*args, **kwargs):
# 模擬成功的 HTTP 響應,繞過檢查
return httpx.Response(200)

Monkey patch httpx.head 方法

httpx.head = mock_httpx_head

使用 block.launch 啟動 Gradio 伺服器,並映射 outputs 目錄

try:
logger.info("Launching Gradio server with static file mapping")
block.launch(
server_name=args.server,
server_port=args.port,
share=args.share,
inbrowser=args.inbrowser,
root_path="/",
additional_paths={"/outputs": outputs_folder} # 映射 outputs 目錄
)
logger.info("Gradio app launched successfully")
except Exception as e:
logger.error("Error launching Gradio app: %s", str(e), exc_info=True)

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with demo_gradio.py, the outputs directory, and the attached framepack log; run the Gradio interface with the recorded debug settings and inspect the video component's file handling. Done means the generated video appears in the Finished Frames component and can be downloaded, including through the Test Gradio Render tab.

Written by the indexing model from the issue text.

Assessment

Tech stack
python
Domain
frontend, machine-learning
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Stale
Clarity
Needs clarification
Newbie friendliness
25/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.