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)