import os import time import uuid from pathlib import Path from typing import Any, Optional import gradio as gr from gradio_client import Client, handle_file # MoviePy 2.x moved VideoFileClip out of moviepy.editor. try: from moviepy import VideoFileClip except ImportError: from moviepy.editor import VideoFileClip # ── config ──────────────────────────────────────────────────────────── HF_TOKEN = os.environ.get("TOKEN") OUTPUT_DIR = Path("uploads/output").resolve() OUTPUT_DIR.mkdir(parents=True, exist_ok=True) client = Client( "leonormar009/Gen-Z", token=HF_TOKEN, download_files=str(OUTPUT_DIR), ) UTM = ( "utm_source=hugging_face_space" "&utm_medium=banner" "&utm_campaign=pro_cta" ) PRO_URL = f"https://huggingface.co/spaces/leonormar009/Gen-Z?{UTM}" # ── helpers ─────────────────────────────────────────────────────────── def _get_file_path(value: Any) -> Optional[str]: """ Convert common Gradio file values to a local file path. Depending on the Gradio component configuration and Gradio version, uploaded files may be returned as a string, pathlib.Path, or dict. """ if value is None: return None if isinstance(value, (str, os.PathLike)): return os.fspath(value) if isinstance(value, dict): for key in ("path", "filepath", "name"): path = value.get(key) if path: return os.fspath(path) # Some Gradio versions return objects with a `path` attribute. path = getattr(value, "path", None) if path: return os.fspath(path) return None def _resize_clip(clip, width: int, height: int): """Support both MoviePy 1.x and MoviePy 2.x APIs.""" if hasattr(clip, "resized"): # MoviePy 2.x return clip.resized(new_size=(width, height)) # MoviePy 1.x return clip.resize(newsize=(width, height)) def _set_clip_fps(clip, fps: int): """Support both MoviePy 1.x and MoviePy 2.x APIs.""" if hasattr(clip, "with_fps"): # MoviePy 2.x return clip.with_fps(fps) # MoviePy 1.x return clip.set_fps(fps) def _subclip(clip, start: float, end: float): """Support both MoviePy 1.x and MoviePy 2.x APIs.""" if hasattr(clip, "subclipped"): # MoviePy 2.x return clip.subclipped(start, end) # MoviePy 1.x return clip.subclip(start, end) def preprocess_video( path: str, target_fps: int = 12, target_size: int = 800, target_len: float = 4, ) -> str: """ Trim, resize, and re-encode a video for the remote API. `target_size` is applied to the video's longest edge. """ if not path or not os.path.isfile(path): raise ValueError(f"Input video does not exist: {path!r}") if target_fps <= 0: raise ValueError("target_fps must be greater than zero.") if target_size <= 0: raise ValueError("target_size must be greater than zero.") source_clip = None processed_clip = None try: source_clip = VideoFileClip(path) duration = float(source_clip.duration or 0) if duration <= 0: raise ValueError("The input video has no valid duration.") processed_clip = source_clip if duration > target_len: processed_clip = _subclip(processed_clip, 0, target_len) source_width, source_height = map(int, source_clip.size) if source_width >= source_height: new_width = target_size new_height = round(target_size * source_height / source_width) else: new_height = target_size new_width = round(target_size * source_width / source_height) # H.264 generally requires even dimensions. new_width = max(2, new_width - (new_width % 2)) new_height = max(2, new_height - (new_height % 2)) processed_clip = _resize_clip( processed_clip, width=new_width, height=new_height, ) processed_clip = _set_clip_fps(processed_clip, target_fps) output_path = OUTPUT_DIR / f"pre_{uuid.uuid4().hex}.mp4" # Do not pass `verbose=False`: that argument was removed in MoviePy 2.x. processed_clip.write_videofile( str(output_path), codec="libx264", audio_codec="aac", fps=target_fps, logger=None, ) return str(output_path) finally: # Closing the clips prevents leaked file handles and locked temp files. if processed_clip is not None: try: processed_clip.close() except Exception: pass if source_clip is not None and source_clip is not processed_clip: try: source_clip.close() except Exception: pass def _find_video_path(value: Any) -> Optional[str]: """ Extract a video path or URL from the different result formats that gradio_client may return. """ if value is None: return None if isinstance(value, (str, os.PathLike)): return os.fspath(value) if isinstance(value, dict): # The API may return {"video": ...}, {"path": ...}, or FileData. for key in ("video", "path", "filepath", "url", "name"): if key in value: result = _find_video_path(value[key]) if result: return result # Handle nested result structures. for nested_value in value.values(): result = _find_video_path(nested_value) if result: return result if isinstance(value, (list, tuple)): for item in value: result = _find_video_path(item) if result: return result path = getattr(value, "path", None) if path: return os.fspath(path) return None # ── main generate ───────────────────────────────────────────────────── def generate(input_image, input_video, gender): image_path = _get_file_path(input_image) video_path = _get_file_path(input_video) if not image_path: raise gr.Error("Missing source image.") if not video_path: raise gr.Error("Missing target video.") pre_video = None try: print("Starting generation...", flush=True) pre_video = preprocess_video(video_path) job = client.submit( input_image=handle_file(image_path), input_video=handle_file(pre_video), device="cpu", selector="many", gender=None if gender == "all" else gender, race=None, order=None, api_name="/generate", ) # result() waits for completion and propagates remote errors. result = job.result() output_video = _find_video_path(result) if not output_video: raise RuntimeError( f"Remote Space returned no usable output video. " f"Received: {result!r}" ) print("Generation finished.", flush=True) gr.Info( "✨ Your preview is ready.
" "Get HD " "(4× quality, no watermark, priority) — " '' "Upgrade on face-swap.co", duration=8, ) return output_video except gr.Error: # Do not wrap an existing Gradio error a second time. raise except Exception as exc: print(f"Generation failed: {exc}", flush=True) raise gr.Error(f"Generation failed: {exc}") from exc finally: # The preprocessed file is only needed while submitting the job. if pre_video and os.path.isfile(pre_video): try: os.remove(pre_video) except OSError as exc: print( f"Warning: could not remove temporary file " f"{pre_video}: {exc}", flush=True, )