Spaces:
Running on Zero
Running on Zero
Download app.py from someone-in-the-world/HighQualityVideoGeneration: direct link, hf CLI and curl.
- Browser
- Download file 19.6 kB
-
https://huggingface.co/spaces/someone-in-the-world/HighQualityVideoGeneration/resolve/663dfc841b062121a770ddf43b2672fd17b2b81f/app.py
- Command line
-
hf download hf://spaces/someone-in-the-world/HighQualityVideoGeneration@663dfc841b062121a770ddf43b2672fd17b2b81f/app.py
-
curl -L -o app.py https://huggingface.co/spaces/someone-in-the-world/HighQualityVideoGeneration/resolve/663dfc841b062121a770ddf43b2672fd17b2b81f/app.py
19.6 kB
| import os | |
| import random | |
| import tempfile | |
| import threading | |
| import time as _time | |
| import gradio as gr | |
| import numpy as np | |
| import spaces | |
| import torch | |
| from diffusers.utils.export_utils import export_to_video | |
| from logging_utils import ( | |
| LogUploader, | |
| print_export_done, | |
| print_export_error, | |
| print_export_start, | |
| print_frames_info, | |
| print_infer_done, | |
| print_infer_error, | |
| print_infer_start, | |
| print_stage_done, | |
| print_stage_error, | |
| print_stage_start, | |
| print_startup_env, | |
| ) | |
| from model.pipeline import ( | |
| DEFAULT_NEGATIVE_PROMPT, | |
| DEFAULT_PROMPT, | |
| FIXED_FPS, | |
| MAX_DURATION, | |
| MIN_DURATION, | |
| estimate_generation_seconds, | |
| get_num_frames, | |
| load_pipeline, | |
| resize_and_crop_to_match, | |
| resize_image, | |
| ) | |
| from postprocess.interpolation import ensure_weights_downloaded as ensure_rife_weights_downloaded | |
| from postprocess.interpolation import interpolate_frames | |
| from postprocess.upscale.upscale import ensure_weights_downloaded as ensure_upscale_weights_downloaded | |
| MAX_SEED = np.iinfo(np.int32).max | |
| FRAME_MULTIPLIER_CHOICES = [FIXED_FPS, FIXED_FPS * 2, FIXED_FPS * 4, FIXED_FPS * 8] | |
| print_startup_env() | |
| pipe = load_pipeline() | |
| # RIFE/upscale weight fetches are CPU/network-only (no CUDA needed) — done here, at process | |
| # startup, so a cold container doesn't pay for them out of the metered @spaces.GPU allocation | |
| # on its first interpolation/upscale request. | |
| ensure_rife_weights_downloaded() | |
| ensure_upscale_weights_downloaded() | |
| # Debug logging (LOG-2): gated entirely by LOG_HF_TOKEN/LOG_DATASET_REPO — a silent no-op when | |
| # either is unset. Deliberately not the deploy HF_TOKEN: this uses its own write-scoped token so a | |
| # leak has a much smaller blast radius (append-only on one dataset repo, not the whole Space repo). | |
| _log_uploader = LogUploader( | |
| token=os.environ.get("LOG_HF_TOKEN"), | |
| repo_id=os.environ.get("LOG_DATASET_REPO"), | |
| max_bytes=int(float(os.environ.get("LOG_STORAGE_CAP_GB", "10")) * 1024**3), | |
| # Hub's commit endpoint rejects pushes once a directory holds >10000 files; stay well under | |
| # that per-directory cap (each logged stem adds one file to data/, images/, and videos/). | |
| max_files=int(os.environ.get("LOG_MAX_FILES", "8000")), | |
| batch_interval=int(os.environ.get("LOG_BATCH_INTERVAL", "60")), | |
| ) | |
| # SPACE_ID is set automatically by Hugging Face Spaces ("namespace/space_name") — used instead of a | |
| # hardcoded handle so the privacy notice stays correct for anyone who duplicates this Space. | |
| _SPACE_ID = os.environ.get("SPACE_ID") | |
| _OPERATOR_MD = ( | |
| f"[the Space operator](https://huggingface.co/{_SPACE_ID.split('/')[0]})" if _SPACE_ID else "the Space operator" | |
| ) | |
| _COMMUNITY_MD = ( | |
| f"[this Space's Community tab](https://huggingface.co/spaces/{_SPACE_ID}/discussions)" | |
| if _SPACE_ID else "this Space's Community tab" | |
| ) | |
| def _gpu_duration( | |
| resized_image, | |
| processed_last_image, | |
| prompt, | |
| negative_prompt, | |
| steps, | |
| num_frames, | |
| guidance_scale, | |
| seed, | |
| frame_multiplier, | |
| upscale_output, | |
| progress, | |
| ): | |
| duration = estimate_generation_seconds( | |
| resized_image.width, resized_image.height, num_frames, int(steps), float(guidance_scale) | |
| ) | |
| frame_factor = frame_multiplier // FIXED_FPS | |
| out_frames = num_frames | |
| if frame_factor > 1: | |
| # Matches the reference project's RIFE-time heuristic: ~0.02s per extra interpolated frame. | |
| extra_frames = (num_frames * frame_factor) - num_frames | |
| duration += extra_frames * 0.02 | |
| out_frames += extra_frames | |
| if upscale_output: | |
| # Rough per-frame tiled-SR heuristic (4xLSDIRCompact, fp16); not yet empirically calibrated | |
| # on the dev Space (same caveat as estimate_generation_seconds above), plus a flat allowance | |
| # for first-call weight download/model load. | |
| duration += 20 + out_frames * 0.3 | |
| return duration | |
| def _apply_interpolation(raw_frames, frame_factor, progress): | |
| if frame_factor <= 1: | |
| return list(raw_frames), FIXED_FPS | |
| def _report_interpolation_progress(done: int, total: int) -> None: | |
| progress(0.7 + 0.2 * (done / total), desc=f"Interpolating frames ({done}/{total})...") | |
| print_stage_start("interpolation") | |
| t0 = _time.perf_counter() | |
| try: | |
| interpolated = interpolate_frames( | |
| raw_frames, multiplier=int(frame_factor), progress_callback=_report_interpolation_progress | |
| ) | |
| print_stage_done("interpolation", _time.perf_counter() - t0) | |
| return interpolated, FIXED_FPS * frame_factor | |
| except Exception as e: | |
| # QF-2: don't discard a successfully generated base video over a post-processing failure. | |
| print_stage_error("interpolation", e) | |
| gr.Warning(f"Frame interpolation failed ({e}); returning the video without it.") | |
| return list(raw_frames), FIXED_FPS | |
| def _apply_upscale(frames, progress): | |
| from postprocess.upscale import upscale_frames | |
| def _report_upscale_progress(done: int, total: int) -> None: | |
| progress(0.9 + 0.09 * (done / total), desc=f"Upscaling 4x ({done}/{total} frames)...") | |
| progress(0.9, desc=f"Upscaling 4x (0/{len(frames)} frames)...") | |
| print_stage_start("upscaling") | |
| t0 = _time.perf_counter() | |
| try: | |
| upscaled = upscale_frames(frames, progress_callback=_report_upscale_progress) | |
| print_stage_done("upscaling", _time.perf_counter() - t0) | |
| return upscaled | |
| except Exception as e: | |
| # QF-2: a failed upscale still returns the pre-upscale (interpolated/base) result. | |
| print_stage_error("upscaling", e) | |
| gr.Warning(f"Upscaling failed ({e}); returning the video without it.") | |
| return None | |
| def run_inference( | |
| resized_image, | |
| processed_last_image, | |
| prompt, | |
| negative_prompt, | |
| steps, | |
| num_frames, | |
| guidance_scale, | |
| seed, | |
| frame_multiplier, | |
| upscale_output, | |
| progress=gr.Progress(), | |
| ): | |
| print_infer_start(prompt, negative_prompt, seed, steps, guidance_scale, frame_multiplier, upscale_output) | |
| t_start = _time.perf_counter() | |
| def _report_generation_progress(pipe_, step_index, timestep, callback_kwargs): | |
| progress(0.7 * (step_index + 1) / int(steps), desc=f"Generating ({step_index + 1}/{int(steps)} steps)...") | |
| return callback_kwargs | |
| print_stage_start("generation") | |
| t0 = _time.perf_counter() | |
| try: | |
| result = pipe( | |
| image=resized_image, | |
| last_image=processed_last_image, | |
| prompt=prompt, | |
| negative_prompt=negative_prompt, | |
| height=resized_image.height, | |
| width=resized_image.width, | |
| num_frames=num_frames, | |
| guidance_scale=float(guidance_scale), | |
| num_inference_steps=int(steps), | |
| generator=torch.Generator(device="cuda").manual_seed(seed), | |
| output_type="np", | |
| callback_on_step_end=_report_generation_progress, | |
| ) | |
| except Exception as e: | |
| print_stage_error("generation", e) | |
| raise | |
| print_stage_done("generation", _time.perf_counter() - t0) | |
| raw_frames = result.frames[0] # (T, H, W, C) float32 in [0, 1] | |
| frame_factor = frame_multiplier // FIXED_FPS | |
| final_frames, final_fps = _apply_interpolation(raw_frames, frame_factor, progress) | |
| upscaled_frames = _apply_upscale(final_frames, progress) if upscale_output else None | |
| if upscaled_frames is None: | |
| print_infer_done(_time.perf_counter() - t_start, final_fps, len(final_frames)) | |
| return final_frames, final_fps | |
| print_infer_done(_time.perf_counter() - t_start, final_fps, len(upscaled_frames)) | |
| return upscaled_frames, final_fps | |
| def _spawn_log( | |
| input_image, | |
| output_frames, | |
| output_fps, | |
| prompt, | |
| negative_prompt, | |
| seed, | |
| steps, | |
| guidance_scale, | |
| interpolation_enabled, | |
| interpolation_multiplier, | |
| upscale_enabled, | |
| output_width, | |
| output_height, | |
| generation_duration_seconds, | |
| success, | |
| error_message="", | |
| ): | |
| # Run on a background thread so committing to the Hub never adds latency to the request | |
| # (LOG-6) — must be spawned here (in the CPU-facing function), not inside the @spaces.GPU | |
| # run_inference call, since that runs in a subprocess that exits (killing any thread it spawns) | |
| # as soon as run_inference returns. | |
| threading.Thread( | |
| target=_log_uploader.log_inference, | |
| args=( | |
| input_image, output_frames, output_fps, prompt, negative_prompt, seed, steps, | |
| guidance_scale, interpolation_enabled, interpolation_multiplier, upscale_enabled, | |
| output_width, output_height, generation_duration_seconds, success, error_message, | |
| ), | |
| daemon=True, | |
| ).start() | |
| def generate_video( | |
| input_image, | |
| last_image, | |
| prompt, | |
| negative_prompt, | |
| duration_seconds, | |
| steps, | |
| guidance_scale, | |
| seed, | |
| randomize_seed, | |
| frame_multiplier, | |
| upscale_output, | |
| progress=gr.Progress(), | |
| ): | |
| if input_image is None: | |
| raise gr.Error("Please upload an input image.") | |
| resized_image = resize_image(input_image) | |
| processed_last_image = resize_and_crop_to_match(last_image, resized_image) if last_image is not None else None | |
| num_frames = get_num_frames(duration_seconds) | |
| current_seed = random.randint(0, MAX_SEED) if randomize_seed else int(seed) | |
| frame_factor = frame_multiplier // FIXED_FPS | |
| interpolation_enabled = frame_factor > 1 | |
| interpolation_multiplier = frame_factor if interpolation_enabled else 1 | |
| t0 = _time.perf_counter() | |
| try: | |
| final_frames, final_fps = run_inference( | |
| resized_image, | |
| processed_last_image, | |
| prompt, | |
| negative_prompt, | |
| steps, | |
| num_frames, | |
| guidance_scale, | |
| current_seed, | |
| frame_multiplier, | |
| upscale_output, | |
| progress, | |
| ) | |
| except Exception as e: | |
| import traceback as _tb | |
| duration = _time.perf_counter() - t0 | |
| print_infer_error(e, duration) | |
| print(f"[infer] traceback:\n{_tb.format_exc()}", flush=True) | |
| if _log_uploader.enabled: | |
| _spawn_log( | |
| resized_image, None, None, prompt, negative_prompt, current_seed, steps, | |
| guidance_scale, interpolation_enabled, interpolation_multiplier, upscale_output, | |
| None, None, duration, False, str(e), | |
| ) | |
| raise | |
| duration = _time.perf_counter() - t0 | |
| print_frames_info("final_frames", final_frames, final_fps) | |
| with tempfile.NamedTemporaryFile(suffix=".mp4", delete=False) as tmpfile: | |
| video_path = tmpfile.name | |
| print_export_start(video_path) | |
| t_export = _time.perf_counter() | |
| try: | |
| export_to_video(final_frames, video_path, fps=final_fps, quality=6) | |
| except Exception as e: | |
| import traceback as _tb | |
| print_export_error(e, _tb.format_exc()) | |
| raise | |
| print_export_done(_time.perf_counter() - t_export) | |
| if _log_uploader.enabled: | |
| # Interpolation/upscale failures are already caught-and-warned (not raised) inside | |
| # run_inference — a video was still produced, so this still counts as success=True. | |
| # interpolation_enabled/upscale_output reflect what the user requested, not whether an | |
| # internal fallback fired. | |
| _spawn_log( | |
| resized_image, final_frames, final_fps, prompt, negative_prompt, current_seed, | |
| steps, guidance_scale, interpolation_enabled, interpolation_multiplier, upscale_output, | |
| resized_image.width, resized_image.height, duration, True, | |
| ) | |
| return video_path, video_path, current_seed | |
| def preview_effect(input_image, duration_seconds, frame_multiplier, upscale_output): | |
| """QF-3: show the resulting fps/resolution/duration before the user hits Generate.""" | |
| num_frames = get_num_frames(duration_seconds) | |
| frame_factor = frame_multiplier // FIXED_FPS | |
| out_fps = FIXED_FPS * frame_factor if frame_factor > 1 else FIXED_FPS | |
| out_frame_count = ((num_frames - 1) * frame_factor + 1) if frame_factor > 1 else num_frames | |
| if input_image is not None: | |
| w, h = resize_image(input_image).size | |
| else: | |
| w = h = None | |
| if upscale_output and w is not None: | |
| w, h = w * 4, h * 4 | |
| resolution = f"{w}×{h}px" if w is not None else "resolution depends on the uploaded image" | |
| return f"**Output:** ~{out_frame_count} frames @ {out_fps} fps, {resolution}" | |
| with gr.Blocks() as demo: | |
| gr.Markdown("# High Quality Video Generation") | |
| gr.Markdown("Turn a still image into a short video clip guided by a text prompt.") | |
| gr.Markdown( | |
| "- Service availability and response times may vary as this Space runs on shared GPU infrastructure.\n" | |
| "- When using this Space, please comply with the " | |
| "[Hugging Face Content Policy](https://huggingface.co/content-policy).\n" | |
| "- This Space and its outputs are provided **\"as is\"**, without warranties of any kind. You are " | |
| "solely responsible for the content you generate and for how you use or share it; the operator " | |
| "accepts no liability for any loss, damage, or claim arising from use of this Space or its " | |
| "outputs.\n" | |
| f"- To monitor application performance and improve quality, input data (image/prompt) and generated " | |
| f"outputs are logged solely for debugging purposes and retained in a private Hugging Face dataset " | |
| f"accessible only to {_OPERATOR_MD}, automatically pruned once the dataset reaches its configured " | |
| f"limit (oldest records deleted first). This processing is based on legitimate interest (GDPR Art. " | |
| f"6(1)(f)) and the data is not shared with or sold to any third party. To request access to or " | |
| f"deletion of your data, open a discussion on {_COMMUNITY_MD}. See the **Privacy Policy** below for " | |
| "full details of your rights.\n" | |
| "- No user account or identifying information (such as IP address or session data) is collected." | |
| ) | |
| with gr.Accordion("Privacy Policy", open=False): | |
| gr.Markdown( | |
| f"**Space:** High Quality Video Generation \n" | |
| f"**Operator:** {_OPERATOR_MD} \n" | |
| f"**Last updated:** 2026-09-18\n" | |
| "\n" | |
| "#### What data is collected\n" | |
| "When you submit a request, the following is logged: your uploaded input image, the generated " | |
| "output video, your prompt and negative prompt, seed, inference " | |
| "steps, guidance scale, frame-interpolation and upscaling settings, output resolution/fps/duration, " | |
| "generation duration, and success/error status. No user account, IP address, or session data is " | |
| "collected.\n" | |
| "\n" | |
| "#### Why it is collected\n" | |
| "Solely for debugging and monitoring application performance (legitimate interest, GDPR Art. " | |
| "6(1)(f)).\n" | |
| "\n" | |
| "#### Where it is stored\n" | |
| f"In a private Hugging Face dataset accessible only to {_OPERATOR_MD}. The data is not shared with " | |
| "or sold to any third party.\n" | |
| "\n" | |
| "#### How long it is kept\n" | |
| "Entries are automatically pruned once the dataset reaches its configured storage cap (default " | |
| "10GB) — the oldest entries are deleted first. The operator may also delete data manually at any " | |
| "time.\n" | |
| "\n" | |
| "#### Your rights\n" | |
| "Use of this Space and submission of images is entirely voluntary. Under GDPR you have the right " | |
| "to:\n" | |
| "- **Access** (Art. 15): request a copy of data held about you\n" | |
| "- **Erasure** (Art. 17): request deletion of your data\n" | |
| "- **Restriction** (Art. 18): request that processing be limited\n" | |
| "- **Portability** (Art. 20): receive your data in a machine-readable format\n" | |
| "- **Objection** (Art. 21): object to processing based on legitimate interest\n" | |
| "\n" | |
| f"To exercise any of these rights, open a discussion on {_COMMUNITY_MD}.\n" | |
| "\n" | |
| "#### Right to complain\n" | |
| "You have the right to lodge a complaint with your national data protection authority, for example: " | |
| "ICO (UK), CNIL (France), BfDI (Germany), or your local EU member state authority listed at " | |
| "[edpb.europa.eu](https://www.edpb.europa.eu/about-edpb/about-edpb/members_en)." | |
| ) | |
| with gr.Row(): | |
| with gr.Column(): | |
| input_image_component = gr.Image(type="pil", label="Input Image", sources=["upload", "clipboard"]) | |
| prompt_input = gr.Textbox(label="Prompt", value=DEFAULT_PROMPT) | |
| duration_input = gr.Slider( | |
| minimum=MIN_DURATION, maximum=MAX_DURATION, step=0.1, value=3.5, label="Duration (seconds)" | |
| ) | |
| frame_multiplier_input = gr.Dropdown( | |
| choices=FRAME_MULTIPLIER_CHOICES, | |
| value=FIXED_FPS, | |
| label="Video Fluidity (Frames per Second)", | |
| info="Extra frames are generated with RIFE frame interpolation to smooth motion.", | |
| ) | |
| upscale_checkbox = gr.Checkbox( | |
| label="Upscale 4×", | |
| value=True, | |
| info="Upscales the finished video 4x on GPU after generation. Runs after frame " | |
| "interpolation, within the same GPU allocation, and adds to the GPU quota used.", | |
| ) | |
| with gr.Accordion("Advanced Settings", open=False): | |
| last_image_component = gr.Image( | |
| type="pil", | |
| label="Last Image (Optional) — end frame to animate towards", | |
| sources=["upload", "clipboard"], | |
| ) | |
| negative_prompt_input = gr.Textbox(label="Negative Prompt", value=DEFAULT_NEGATIVE_PROMPT, lines=3) | |
| steps_slider = gr.Slider(minimum=1, maximum=30, step=1, value=6, label="Inference Steps") | |
| guidance_scale_input = gr.Slider(minimum=0.0, maximum=10.0, step=0.5, value=1.0, label="Guidance Scale") | |
| seed_input = gr.Slider(label="Seed", minimum=0, maximum=MAX_SEED, step=1, value=42) | |
| randomize_seed_checkbox = gr.Checkbox(label="Randomize seed", value=True) | |
| effect_preview = gr.Markdown() | |
| generate_button = gr.Button("Generate Video", variant="primary") | |
| with gr.Column(): | |
| video_output = gr.Video(label="Generated Video", autoplay=True) | |
| file_output = gr.File(label="Download Video") | |
| generate_button.click( | |
| fn=generate_video, | |
| inputs=[ | |
| input_image_component, | |
| last_image_component, | |
| prompt_input, | |
| negative_prompt_input, | |
| duration_input, | |
| steps_slider, | |
| guidance_scale_input, | |
| seed_input, | |
| randomize_seed_checkbox, | |
| frame_multiplier_input, | |
| upscale_checkbox, | |
| ], | |
| outputs=[video_output, file_output, seed_input], | |
| ) | |
| preview_inputs = [input_image_component, duration_input, frame_multiplier_input, upscale_checkbox] | |
| for component in preview_inputs: | |
| component.change(fn=preview_effect, inputs=preview_inputs, outputs=effect_preview) | |
| demo.load(fn=preview_effect, inputs=preview_inputs, outputs=effect_preview) | |
| if __name__ == "__main__": | |
| demo.queue().launch(show_error=True, ssr_mode=False) | |