#!/usr/bin/env python3 """ HuggingFace FFmpeg Space v2 65% main / 35% gameplay, zero gap using overlay on black canvas """ from flask import Flask, request, jsonify, send_file import subprocess, os, uuid, threading import urllib.request as ur app = Flask(__name__) JOBS = {} OUTPUT_DIR = "/tmp/outputs" os.makedirs(OUTPUT_DIR, exist_ok=True) def run_ffmpeg(job_id, video_id, main_url, gameplay_url, duration=58): try: work_dir = f"/tmp/job_{job_id}" os.makedirs(work_dir, exist_ok=True) main_path = f"{work_dir}/main.mp4" gameplay_path = f"{work_dir}/gameplay.mp4" output_path = f"{OUTPUT_DIR}/{video_id}_final.mp4" # Download main video print(f"[HF] Downloading main: {main_url[:80]}") req = ur.Request(main_url, headers={"User-Agent": "Mozilla/5.0"}) with ur.urlopen(req, timeout=120) as r: with open(main_path, "wb") as f: f.write(r.read()) # Download gameplay print(f"[HF] Downloading gameplay: {gameplay_url[:80]}") req2 = ur.Request(gameplay_url, headers={"User-Agent": "Mozilla/5.0"}) with ur.urlopen(req2, timeout=120) as r: with open(gameplay_path, "wb") as f: f.write(r.read()) # Canvas: 1080x1920 (9:16) # Main: top 65% = 1248px (y=0) # Gameplay: bottom 35% = 672px (y=1248) # Total: 1248+672 = 1920 exactly — NO gap canvas_w = 1080 main_h = 1248 # 65% of 1920 game_h = 672 # 35% of 1920 game_y = 1248 # starts exactly where main ends filter_complex = ( # Black canvas 1080x1920 f"color=black:{canvas_w}x1920:rate=30[canvas];" # Scale main to 1080x1248 (fill + crop, no black bars) f"[0:v]trim=0:{duration},setpts=PTS-STARTPTS," f"scale={canvas_w}:{main_h}:force_original_aspect_ratio=increase," f"crop={canvas_w}:{main_h},setsar=1[main];" # Scale gameplay to 1080x672 (fill + crop, no black bars) f"[1:v]trim=0:{duration},setpts=PTS-STARTPTS," f"scale={canvas_w}:{game_h}:force_original_aspect_ratio=increase," f"crop={canvas_w}:{game_h},setsar=1[game];" # Overlay main at y=0, then gameplay at y=1248 f"[canvas][main]overlay=0:0[tmp];" f"[tmp][game]overlay=0:{game_y}[outv];" # Audio from main only f"[0:a]atrim=0:{duration},asetpts=PTS-STARTPTS,aresample=44100[outa]" ) cmd = [ "ffmpeg", "-y", "-f", "lavfi", "-i", f"color=black:{canvas_w}x1920:rate=30:duration={duration}", "-i", main_path, "-stream_loop", "-1", "-i", gameplay_path, "-filter_complex", ( # Scale main to 1080x1248 f"[1:v]trim=0:{duration},setpts=PTS-STARTPTS," f"scale={canvas_w}:{main_h}:force_original_aspect_ratio=increase," f"crop={canvas_w}:{main_h},setsar=1[main];" # Scale gameplay to 1080x672 f"[2:v]trim=0:{duration},setpts=PTS-STARTPTS," f"scale={canvas_w}:{game_h}:force_original_aspect_ratio=increase," f"crop={canvas_w}:{game_h},setsar=1[game];" # Overlay on black canvas: main at top, game at bottom f"[0:v][main]overlay=0:0[tmp];" f"[tmp][game]overlay=0:{game_y}[outv];" # Audio from main only f"[1:a]atrim=0:{duration},asetpts=PTS-STARTPTS,aresample=44100[outa]" ), "-map", "[outv]", "-map", "[outa]", "-c:v", "libx264", "-preset", "ultrafast", "-crf", "26", "-c:a", "aac", "-b:a", "128k", "-pix_fmt", "yuv420p", "-t", str(duration), "-avoid_negative_ts", "make_zero", "-async", "1", "-movflags", "+faststart", output_path ] print(f"[HF] Running FFmpeg...") result = subprocess.run(cmd, capture_output=True, text=True, timeout=600) if result.returncode != 0: print(f"[HF] FFmpeg error: {result.stderr[-800:]}") JOBS[job_id] = {"status": "error", "error": result.stderr[-500:]} return import shutil shutil.rmtree(work_dir, ignore_errors=True) size = os.path.getsize(output_path) print(f"[HF] Done! {size/1024/1024:.1f}MB") JOBS[job_id] = { "status": "done", "videoId": video_id, "downloadUrl": f"/download/{video_id}_final.mp4", "size": size } except Exception as e: import traceback JOBS[job_id] = {"status": "error", "error": traceback.format_exc()} print(f"[HF] Exception: {traceback.format_exc()}") @app.route("/clip", methods=["POST"]) def clip(): data = request.json video_id = data.get("videoId") main_url = data.get("mainVideoUrl") gameplay_url = data.get("gameplayUrl") duration = int(data.get("duration", 58)) job_id = str(uuid.uuid4()) JOBS[job_id] = {"status": "running", "videoId": video_id} t = threading.Thread( target=run_ffmpeg, args=(job_id, video_id, main_url, gameplay_url, duration), daemon=True ) t.start() return jsonify({"job_id": job_id, "status": "started", "videoId": video_id}) @app.route("/job/", methods=["GET"]) def job_status(job_id): return jsonify(JOBS.get(job_id, {"status": "not_found"})) @app.route("/download/", methods=["GET"]) def download(filename): path = f"{OUTPUT_DIR}/{filename}" if not os.path.exists(path): return jsonify({"error": "Not found"}), 404 return send_file(path, mimetype="video/mp4", as_attachment=False) @app.route("/health", methods=["GET"]) def health(): return jsonify({"status": "ok", "jobs": len(JOBS)}) if __name__ == "__main__": app.run(host="0.0.0.0", port=7860)