Spaces:
Running
Running
File size: 6,066 Bytes
34119d6 51d360b 34119d6 51d360b 1e0585d 34119d6 1e0585d 34119d6 1e0585d 51d360b 1e0585d 51d360b 1e0585d 51d360b 1e0585d 51d360b 1e0585d 51d360b 1e0585d 51d360b 1e0585d 51d360b 1e0585d 51d360b 1e0585d 51d360b 1e0585d 51d360b 1e0585d 34119d6 1e0585d 34119d6 1e0585d 34119d6 1e0585d 34119d6 1e0585d 34119d6 1e0585d 34119d6 1e0585d 34119d6 1e0585d | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 | #!/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/<job_id>", methods=["GET"])
def job_status(job_id):
return jsonify(JOBS.get(job_id, {"status": "not_found"}))
@app.route("/download/<filename>", 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) |