| """
|
| 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")
|
|
|
|
|
| RESOLUTIONS = {
|
| "720p": (1280, 720),
|
| "1080p": (1920, 1080),
|
| "1440p": (2560, 1440),
|
| "2160p": (3840, 2160),
|
| }
|
|
|
|
|
| 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>
|
| """
|
|
|
| @app.get("/", response_class=HTMLResponse)
|
| async def home():
|
| return HTML_PAGE
|
|
|
| @app.post("/upscale")
|
| async def upscale_video(
|
| video: UploadFile = File(...),
|
| resolution: str = Form("2160p"),
|
| quality: str = Form("balanced")
|
| ):
|
| try:
|
|
|
| if resolution not in RESOLUTIONS:
|
| resolution = "2160p"
|
| width, height = RESOLUTIONS[resolution]
|
|
|
|
|
| quality_map = {
|
| "fast": ("23", "fast"),
|
| "balanced": ("18", "medium"),
|
| "best": ("15", "slow")
|
| }
|
| crf, preset = quality_map.get(quality, ("18", "medium"))
|
|
|
|
|
| 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)
|
|
|
|
|
| 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
|
| ]
|
|
|
|
|
| process = subprocess.run(cmd, capture_output=True, text=True, timeout=3600)
|
|
|
|
|
| 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)}
|
|
|
| @app.get("/download/{upload_id}")
|
| 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)
|
|
|