"""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