Spaces:
Running
Running
| """ | |
| Video Upscaler - HuggingFace Spaces (FastAPI + HTML) | |
| Simple, reliable video upscaling using FFmpeg. | |
| No Gradio - cleaner and fewer dependencies. | |
| """ | |
| from fastapi import FastAPI, File, UploadFile, Form | |
| from fastapi.responses import HTMLResponse, FileResponse | |
| from fastapi.staticfiles import StaticFiles | |
| import subprocess | |
| import os | |
| import tempfile | |
| import shutil | |
| import uuid | |
| app = FastAPI(title="Video Upscaler 4K") | |
| # Upscale resolutions | |
| RESOLUTIONS = { | |
| "720p": (1280, 720), | |
| "1080p": (1920, 1080), | |
| "1440p": (2560, 1440), | |
| "2160p": (3840, 2160), | |
| } | |
| # Create temp directory for outputs | |
| OUTPUT_DIR = "/tmp/upscaled" | |
| os.makedirs(OUTPUT_DIR, exist_ok=True) | |
| HTML_PAGE = """ | |
| <!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>π¬ Video Upscaler 4K</title> | |
| <style> | |
| * { margin: 0; padding: 0; box-sizing: border-box; } | |
| body { | |
| font-family: 'Segoe UI', system-ui, sans-serif; | |
| background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%); | |
| min-height: 100vh; | |
| color: #fff; | |
| padding: 20px; | |
| } | |
| .container { | |
| max-width: 600px; | |
| margin: 0 auto; | |
| background: rgba(255,255,255,0.1); | |
| backdrop-filter: blur(10px); | |
| border-radius: 20px; | |
| padding: 30px; | |
| box-shadow: 0 8px 32px rgba(0,0,0,0.3); | |
| } | |
| h1 { | |
| text-align: center; | |
| margin-bottom: 10px; | |
| font-size: 2em; | |
| } | |
| .subtitle { | |
| text-align: center; | |
| color: #aaa; | |
| margin-bottom: 30px; | |
| } | |
| .form-group { | |
| margin-bottom: 20px; | |
| } | |
| label { | |
| display: block; | |
| margin-bottom: 8px; | |
| font-weight: 500; | |
| } | |
| input[type="file"] { | |
| width: 100%; | |
| padding: 15px; | |
| background: rgba(255,255,255,0.1); | |
| border: 2px dashed #666; | |
| border-radius: 10px; | |
| color: #fff; | |
| cursor: pointer; | |
| } | |
| input[type="file"]:hover { | |
| border-color: #00d9ff; | |
| } | |
| select { | |
| width: 100%; | |
| padding: 12px; | |
| border-radius: 8px; | |
| border: none; | |
| background: rgba(255,255,255,0.2); | |
| color: #fff; | |
| font-size: 16px; | |
| } | |
| select option { | |
| background: #1a1a2e; | |
| color: #fff; | |
| } | |
| button { | |
| width: 100%; | |
| padding: 15px; | |
| background: linear-gradient(135deg, #00d9ff, #0066ff); | |
| border: none; | |
| border-radius: 10px; | |
| color: #fff; | |
| font-size: 18px; | |
| font-weight: 600; | |
| cursor: pointer; | |
| transition: transform 0.2s, box-shadow 0.2s; | |
| } | |
| button:hover { | |
| transform: translateY(-2px); | |
| box-shadow: 0 5px 20px rgba(0,217,255,0.4); | |
| } | |
| button:disabled { | |
| background: #555; | |
| cursor: not-allowed; | |
| transform: none; | |
| } | |
| .status { | |
| margin-top: 20px; | |
| padding: 15px; | |
| border-radius: 10px; | |
| text-align: center; | |
| display: none; | |
| } | |
| .status.show { display: block; } | |
| .status.processing { | |
| background: rgba(255,193,7,0.2); | |
| border: 1px solid #ffc107; | |
| } | |
| .status.success { | |
| background: rgba(40,167,69,0.2); | |
| border: 1px solid #28a745; | |
| } | |
| .status.error { | |
| background: rgba(220,53,69,0.2); | |
| border: 1px solid #dc3545; | |
| } | |
| .download-link { | |
| display: inline-block; | |
| margin-top: 10px; | |
| padding: 10px 20px; | |
| background: #28a745; | |
| color: #fff; | |
| text-decoration: none; | |
| border-radius: 8px; | |
| } | |
| .tips { | |
| margin-top: 30px; | |
| padding: 15px; | |
| background: rgba(255,255,255,0.05); | |
| border-radius: 10px; | |
| font-size: 14px; | |
| color: #aaa; | |
| } | |
| .tips h3 { color: #fff; margin-bottom: 10px; } | |
| .tips li { margin: 5px 0; } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <h1>π¬ Video Upscaler 4K</h1> | |
| <p class="subtitle">Upscale your videos to Ultra HD quality</p> | |
| <form id="uploadForm" enctype="multipart/form-data"> | |
| <div class="form-group"> | |
| <label>π€ Upload Video (MP4, MKV, AVI, MOV)</label> | |
| <input type="file" name="video" id="videoInput" accept="video/*" required> | |
| </div> | |
| <div class="form-group"> | |
| <label>π― Target Resolution</label> | |
| <select name="resolution" id="resolution"> | |
| <option value="720p">720p (HD)</option> | |
| <option value="1080p">1080p (Full HD)</option> | |
| <option value="1440p">1440p (2K)</option> | |
| <option value="2160p" selected>2160p (4K Ultra HD)</option> | |
| </select> | |
| </div> | |
| <div class="form-group"> | |
| <label>βοΈ Quality</label> | |
| <select name="quality" id="quality"> | |
| <option value="fast">Fast (Lower Quality)</option> | |
| <option value="balanced" selected>Balanced</option> | |
| <option value="best">Best Quality (Slow)</option> | |
| </select> | |
| </div> | |
| <button type="submit" id="submitBtn">π Upscale Video</button> | |
| </form> | |
| <div id="status" class="status"></div> | |
| <div class="tips"> | |
| <h3>π‘ Tips:</h3> | |
| <ul> | |
| <li>4K (2160p) = 3840x2160 pixels</li> | |
| <li>Processing time: ~2-5 min per minute of video</li> | |
| <li>Best Quality uses CRF 15 (near-lossless)</li> | |
| <li>Max file size: 100MB for free tier</li> | |
| </ul> | |
| </div> | |
| </div> | |
| <script> | |
| const form = document.getElementById('uploadForm'); | |
| const statusDiv = document.getElementById('status'); | |
| const submitBtn = document.getElementById('submitBtn'); | |
| form.addEventListener('submit', async (e) => { | |
| e.preventDefault(); | |
| const formData = new FormData(form); | |
| submitBtn.disabled = true; | |
| submitBtn.textContent = 'β³ Processing...'; | |
| statusDiv.className = 'status show processing'; | |
| statusDiv.innerHTML = 'β³ Uploading and processing... This may take a few minutes.'; | |
| try { | |
| const response = await fetch('/upscale', { | |
| method: 'POST', | |
| body: formData | |
| }); | |
| const result = await response.json(); | |
| if (result.success) { | |
| statusDiv.className = 'status show success'; | |
| statusDiv.innerHTML = ` | |
| β Success! Video upscaled to ${result.resolution}<br> | |
| π Size: ${result.size_mb} MB<br> | |
| <a href="${result.download_url}" class="download-link" download>π₯ Download Video</a> | |
| `; | |
| } else { | |
| statusDiv.className = 'status show error'; | |
| statusDiv.innerHTML = 'β Error: ' + result.error; | |
| } | |
| } catch (error) { | |
| statusDiv.className = 'status show error'; | |
| statusDiv.innerHTML = 'β Error: ' + error.message; | |
| } | |
| submitBtn.disabled = false; | |
| submitBtn.textContent = 'π Upscale Video'; | |
| }); | |
| </script> | |
| </body> | |
| </html> | |
| """ | |
| async def home(): | |
| return HTML_PAGE | |
| async def upscale_video( | |
| video: UploadFile = File(...), | |
| resolution: str = Form("2160p"), | |
| quality: str = Form("balanced") | |
| ): | |
| try: | |
| # Get target dimensions | |
| if resolution not in RESOLUTIONS: | |
| resolution = "2160p" | |
| width, height = RESOLUTIONS[resolution] | |
| # Quality settings | |
| quality_map = { | |
| "fast": ("23", "fast"), | |
| "balanced": ("18", "medium"), | |
| "best": ("15", "slow") | |
| } | |
| crf, preset = quality_map.get(quality, ("18", "medium")) | |
| # Save uploaded file | |
| upload_id = str(uuid.uuid4())[:8] | |
| input_path = f"/tmp/input_{upload_id}.mp4" | |
| output_path = f"{OUTPUT_DIR}/upscaled_{upload_id}.mp4" | |
| with open(input_path, "wb") as f: | |
| content = await video.read() | |
| f.write(content) | |
| # FFmpeg command for high-quality upscaling | |
| cmd = [ | |
| 'ffmpeg', '-y', | |
| '-i', input_path, | |
| '-vf', f'scale={width}:{height}:flags=lanczos', | |
| '-c:v', 'libx264', | |
| '-crf', crf, | |
| '-preset', preset, | |
| '-c:a', 'aac', '-b:a', '192k', | |
| '-movflags', '+faststart', | |
| output_path | |
| ] | |
| # Run FFmpeg | |
| process = subprocess.run(cmd, capture_output=True, text=True, timeout=3600) | |
| # Clean up input | |
| os.remove(input_path) | |
| if process.returncode == 0 and os.path.exists(output_path): | |
| size_mb = round(os.path.getsize(output_path) / (1024 * 1024), 1) | |
| return { | |
| "success": True, | |
| "resolution": f"{width}x{height}", | |
| "size_mb": size_mb, | |
| "download_url": f"/download/{upload_id}" | |
| } | |
| else: | |
| return {"success": False, "error": "FFmpeg processing failed"} | |
| except subprocess.TimeoutExpired: | |
| return {"success": False, "error": "Processing timeout (>1 hour)"} | |
| except Exception as e: | |
| return {"success": False, "error": str(e)} | |
| async def download_file(upload_id: str): | |
| file_path = f"{OUTPUT_DIR}/upscaled_{upload_id}.mp4" | |
| if os.path.exists(file_path): | |
| return FileResponse(file_path, filename=f"upscaled_{upload_id}.mp4", media_type="video/mp4") | |
| return {"error": "File not found"} | |
| if __name__ == "__main__": | |
| import uvicorn | |
| uvicorn.run(app, host="0.0.0.0", port=7860) | |