Spaces:
Running
Running
| #!/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()}") | |
| 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}) | |
| def job_status(job_id): | |
| return jsonify(JOBS.get(job_id, {"status": "not_found"})) | |
| 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) | |
| def health(): | |
| return jsonify({"status": "ok", "jobs": len(JOBS)}) | |
| if __name__ == "__main__": | |
| app.run(host="0.0.0.0", port=7860) |