Spaces:
Sleeping
Sleeping
| """ | |
| Speed Reader β WebM Converter | |
| Flask app for HuggingFace Spaces (Docker SDK). | |
| Endpoints: | |
| GET / β health check / status page | |
| POST /convert β multipart upload: file=<webm>, format=mp4|gif, fps=15 | |
| returns the converted file as a binary download | |
| """ | |
| import io | |
| import os | |
| import subprocess | |
| import tempfile | |
| from pathlib import Path | |
| from flask import Flask, request, send_file, jsonify | |
| from flask_cors import CORS | |
| app = Flask(__name__) | |
| CORS(app, resources={r"/*": {"origins": "*"}}) | |
| # ββ Health check ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def index(): | |
| return jsonify({ | |
| "status": "ok", | |
| "service": "Speed Reader β WebM Converter", | |
| "endpoints": { | |
| "POST /convert": { | |
| "params": { | |
| "file": "WebM file (multipart)", | |
| "format": "'mp4' or 'gif'", | |
| "fps": "GIF frame rate, default 15 (ignored for mp4)" | |
| }, | |
| "returns": "Converted file download" | |
| } | |
| } | |
| }) | |
| # ββ Conversion endpoint βββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def convert(): | |
| if "file" not in request.files: | |
| return jsonify({"error": "No file uploaded. Send as multipart field 'file'."}), 400 | |
| uploaded = request.files["file"] | |
| fmt = request.form.get("format", "mp4").lower().strip() | |
| fps = request.form.get("fps", "15") | |
| if fmt not in ("mp4", "gif"): | |
| return jsonify({"error": f"Unsupported format '{fmt}'. Use 'mp4' or 'gif'."}), 400 | |
| try: | |
| fps = max(5, min(30, int(fps))) | |
| except ValueError: | |
| fps = 15 | |
| # ββ Write upload to temp dir, run ffmpeg, read result into memory βββββββββ | |
| # | |
| # IMPORTANT: send_file is called AFTER the with-block so the tempdir is | |
| # already deleted by then. We must read the output file into a BytesIO | |
| # buffer while we are still inside the with-block. | |
| # | |
| buf = None | |
| with tempfile.TemporaryDirectory() as tmpdir: | |
| tmp = Path(tmpdir) | |
| src = tmp / "input.webm" | |
| out = tmp / f"output.{fmt}" | |
| uploaded.save(str(src)) | |
| if src.stat().st_size == 0: | |
| return jsonify({"error": "Uploaded file is empty."}), 400 | |
| try: | |
| if fmt == "mp4": | |
| _run([ | |
| "ffmpeg", "-y", | |
| "-i", str(src), | |
| "-c:v", "libx264", | |
| "-preset", "fast", | |
| "-crf", "22", | |
| "-pix_fmt", "yuv420p", | |
| "-movflags", "+faststart", | |
| "-an", | |
| str(out) | |
| ]) | |
| else: # gif β two-pass palette encode | |
| palette = tmp / "palette.png" | |
| # Pass 1: build optimised palette from the full video | |
| _run([ | |
| "ffmpeg", "-y", | |
| "-i", str(src), | |
| "-vf", f"fps={fps},scale=iw:ih:flags=lanczos,palettegen=stats_mode=diff", | |
| str(palette) | |
| ]) | |
| # Pass 2: encode GIF using palette | |
| # [0:v] is required to tell ffmpeg which stream feeds the filter | |
| _run([ | |
| "ffmpeg", "-y", | |
| "-i", str(src), | |
| "-i", str(palette), | |
| "-filter_complex", | |
| f"[0:v]fps={fps},scale=iw:ih:flags=lanczos[x];[x][1:v]paletteuse=dither=bayer", | |
| str(out) | |
| ]) | |
| except subprocess.CalledProcessError as e: | |
| return jsonify({"error": f"ffmpeg failed:\n{e.stderr[-800:]}"}), 500 | |
| if not out.exists() or out.stat().st_size == 0: | |
| return jsonify({"error": "Conversion produced an empty file."}), 500 | |
| # Read into memory NOW, while the tempdir still exists | |
| buf = io.BytesIO(out.read_bytes()) | |
| # tempdir is deleted here β but buf is safely in memory | |
| buf.seek(0) | |
| mime = "video/mp4" if fmt == "mp4" else "image/gif" | |
| return send_file( | |
| buf, | |
| mimetype=mime, | |
| as_attachment=True, | |
| download_name=f"speed-read.{fmt}" | |
| ) | |
| # ββ Helper ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def _run(cmd: list[str]): | |
| result = subprocess.run(cmd, capture_output=True, text=True) | |
| if result.returncode != 0: | |
| raise subprocess.CalledProcessError( | |
| result.returncode, cmd, output=result.stdout, stderr=result.stderr | |
| ) | |
| return result | |
| # ββ Entry point βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| if __name__ == "__main__": | |
| port = int(os.environ.get("PORT", 7860)) | |
| app.run(host="0.0.0.0", port=port) |