minimax-h3 / export.py
observantdistressed's picture
fix: robust video display + download delivery
a89b492
Raw History Blame Contribute Delete
5.45 kB
"""Frame export (last-frame extraction from a generated video).
`export_last_frame` is bound to the "Export Last Frame" button in the UI.
It validates that the input path lies inside the managed task temp root or the
legacy `/tmp/h3-outputs` directory, then runs ffmpeg to extract the final frame.
This is the only place that exposes the disk to client-supplied paths; the
path validation is intentionally strict.
"""
from __future__ import annotations
import os
import subprocess
import tempfile
import uuid
import gradio as gr
from task import _TASK_TEMP_ROOT
def export_last_frame(video_path):
"""Extract the last frame from a generated video and return it as an image file.
Path is validated to lie under the managed task/temp root; arbitrary client paths are rejected.
Destination is unique per call.
"""
if not video_path:
raise gr.Error("No video to extract from.")
# Resolve and constrain to managed roots (task temp or legacy h3-outputs).
try:
resolved = os.path.realpath(video_path)
except OSError:
raise gr.Error("Invalid video path.")
# Only managed task/output roots β€” never the bare system tempdir (symlink surface).
allowed_roots = [
os.path.realpath(_TASK_TEMP_ROOT),
os.path.realpath(os.path.join(tempfile.gettempdir(), "h3-outputs")),
]
if not any(resolved == root or resolved.startswith(root + os.sep) for root in allowed_roots):
raise gr.Error("Video path is outside the managed output area.")
# Unique destination under the same parent or task intermediates.
parent = os.path.dirname(resolved)
out = os.path.join(parent, f"lastframe-{uuid.uuid4().hex[:12]}.png")
result = subprocess.run(
["ffmpeg", "-sseof", "-0.1", "-i", resolved, "-frames:v", "1", "-y", out],
capture_output=True, text=True,
)
if result.returncode != 0:
raise gr.Error(f"ffmpeg failed: {result.stderr[-300:]}")
return out
# ── Durable video finalization (display + download) ─────────────────────────
# Public-ish mirror of task outputs. Gradio serves these more reliably than deep
# task dirs, and clients can still download after the request finishes.
_H3_OUTPUT_ROOT = os.environ.get(
"H3_OUTPUT_ROOT", os.path.join(tempfile.gettempdir(), "h3-outputs")
)
def _ensure_output_roots():
for d in (_H3_OUTPUT_ROOT, _TASK_TEMP_ROOT):
try:
os.makedirs(d, exist_ok=True)
except OSError:
pass
def _finalize_output_video(frames, audio, sampling_rate, *, task=None, fps=24):
"""Mux frames (+ optional audio) to a durable mp4 and return an absolute path.
Hardens Gradio Video / Download against:
- missing/empty files after encode
- encode failures with audio (retries silent)
- non-absolute paths Gradio cannot serve
- deep task-only paths that disappear from the UI file server
"""
import shutil
from diffusers.utils import encode_video as _encode_video
_ensure_output_roots()
# Normalize frames to a numpy-friendly object encode_video accepts.
if hasattr(frames, "detach"):
frames = frames.detach().cpu().numpy()
if frames is None:
raise gr.Error("No frames were produced β€” nothing to encode.")
tid = (task.task_id if task is not None else None) or uuid.uuid4().hex
work = (task.work_dir if task is not None and getattr(task, "work_dir", None) else None)
if work:
try:
os.makedirs(work, exist_ok=True)
except OSError:
work = None
primary = os.path.join(work or _H3_OUTPUT_ROOT, f"h3-{tid}.mp4")
mirror = os.path.join(_H3_OUTPUT_ROOT, f"h3-{tid}.mp4")
def _try_encode(path, with_audio: bool):
kwargs = dict(fps=fps, output_path=path)
if with_audio and audio is not None:
kwargs["audio"] = audio
kwargs["audio_sample_rate"] = sampling_rate
_encode_video(frames, **kwargs)
last_err = None
encoded = False
for with_audio in (True, False):
try:
_try_encode(primary, with_audio=with_audio)
encoded = True
if not with_audio and audio is not None:
# Silent fallback β€” still deliver a playable file.
pass
break
except Exception as e:
last_err = e
try:
if os.path.isfile(primary):
os.remove(primary)
except OSError:
pass
continue
if not encoded:
raise gr.Error(
f"Failed to mux the video file ({type(last_err).__name__}: {last_err}). "
"Try a shorter duration or fewer steps."
)
if not os.path.isfile(primary) or os.path.getsize(primary) < 1024:
raise gr.Error(
"Video file was not written correctly (missing or under 1 KB). "
"Please retry the generation."
)
# Mirror into h3-outputs so Gradio always has a stable root to serve from.
try:
if os.path.realpath(primary) != os.path.realpath(mirror):
shutil.copy2(primary, mirror)
serve_path = mirror
else:
serve_path = primary
except OSError:
serve_path = primary
serve_path = os.path.realpath(serve_path)
if task is not None:
task.output_path = serve_path
return serve_path