Initial commit of video upscaler

This commit is contained in:
2026-06-23 15:48:14 -04:00
commit 79d67ea6a5
27 changed files with 6779 additions and 0 deletions
+21
View File
@@ -0,0 +1,21 @@
# Python
venv/
.venv/
__pycache__/
*.pyc
*.pyo
# Runtime data
uploads/
outputs/
temp/
server.log
.uvicorn.pid
# OS
.DS_Store
Thumbs.db
# IDE
.vscode/
.idea/
+150
View File
@@ -0,0 +1,150 @@
# 🎬 AI Video Upscaler
A web-based video upscaling application powered by [Real-ESRGAN ncnn-vulkan](https://github.com/xinntao/Real-ESRGAN). Upload your videos through an intuitive dashboard, select an AI model, and produce high-resolution output — all accelerated by your Vulkan-compatible GPU.
---
## ✨ Features
| Category | Details |
|---|---|
| **AI Upscaling** | 2×/3×/4× upscaling via Real-ESRGAN ncnn-vulkan with GPU acceleration |
| **Web Dashboard** | Modern browser UI for uploading, configuring, and monitoring jobs |
| **Multiple Models** | Choose from bundled models (realesrgan-x4plus, realesrgan-x4plus-anime, realesr-animevideov3, etc.) |
| **Custom Model Upload** | Upload your own `.param` / `.bin` model files through the UI |
| **Frame Preview** | Preview a single upscaled frame before committing to a full job |
| **Video Trimming** | Trim start/end times before upscaling to process only the segment you need |
| **Post-Processing Filters** | Optional sharpening, denoising, and color-correction filters applied after upscaling |
| **Audio & Subtitle Preservation** | Original audio tracks and subtitle streams are automatically muxed into the output |
| **Job Queue** | Queue multiple jobs and track progress in real time via WebSocket |
| **Webhook Notifications** | Configure a webhook URL to receive a callback when a job completes or fails |
| **System Diagnostics** | Built-in diagnostics endpoint reports GPU info, Vulkan status, disk space, and dependency versions |
| **Output Gallery** | Browse, preview, and download completed videos from the dashboard |
---
## 📋 Requirements
- **OS:** Linux x86_64
- **Python:** 3.10+
- **FFmpeg / FFprobe:** installed and available on `$PATH`
- **GPU:** Vulkan-compatible GPU (NVIDIA, AMD, or Intel) with up-to-date drivers
---
## 🚀 Quick Start
```bash
# 1. Clone the repository
git clone <repo-url> video_upscaler
cd video_upscaler
# 2. Run the setup script (creates venv, installs deps, downloads Real-ESRGAN binary & models)
./setup.sh
# 3. Start the server
python start.py
```
The dashboard will be available at **http://localhost:8000** (or your machine's network IP) by default.
---
## 🖥️ Server Management
```bash
python start.py # Start on default port 8000 (binds to 0.0.0.0 for LAN access)
python start.py --port 9000 # Start on a custom port
python start.py --host 127.0.0.1 # Bind to loopback only (no LAN access)
python start.py --no-browser # Don't open browser automatically
python stop.py # Stop the server
python restart.py # Restart the server
python restart.py --port 9000 # Restart on a custom port
```
---
## 🔌 API Endpoints
| Method | Endpoint | Description |
|---|---|---|
| `POST` | `/api/upload` | Upload a video file for processing |
| `POST` | `/api/upscale/start` | Start an upscale job with the given configuration |
| `GET` | `/api/upscale/status/{job_id}` | Get the current status and progress of a job |
| `POST` | `/api/upscale/cancel/{job_id}` | Cancel a running or queued job |
| `POST` | `/api/preview/generate` | Generate an upscaled preview of a single frame |
| `GET` | `/api/models` | List all available upscaling models |
| `POST` | `/api/models/upload` | Upload a custom model (`.param` + `.bin`) |
| `GET` | `/api/outputs` | List completed output files |
| `GET` | `/api/download/{filename}` | Download a completed output video |
| `GET` | `/api/diagnostics` | System diagnostics (GPU, Vulkan, disk, dependencies) |
| `WS` | `/ws/progress/{job_id}` | WebSocket stream of real-time job progress updates |
---
## 📁 Project Structure
```
video_upscaler/
├── app/
│ ├── main.py # FastAPI application & API routes
│ └── upscaler.py # Video processing pipeline
├── static/
│ ├── index.html # Web dashboard
│ ├── styles.css # UI styles
│ └── app.js # Frontend logic
├── realesrgan-bin/
│ ├── realesrgan-ncnn-vulkan # Upscaling binary (x86_64 Linux)
│ └── models/ # AI model files (.param + .bin)
├── uploads/ # Uploaded video files (gitignored)
├── outputs/ # Upscaled video files (gitignored)
├── temp/ # Temporary frame data (gitignored)
├── start.py # Start the server
├── stop.py # Stop the server
├── restart.py # Restart the server
├── setup.sh # First-time setup script
├── requirements.txt # Python dependencies
└── README.md
```
---
## 🔧 Troubleshooting
### Vulkan not detected
```
Error: vkCreateInstance failed
```
- Ensure your GPU drivers are up to date.
- Verify Vulkan is working: `vulkaninfo | head -20`.
- Install the Vulkan loader if missing: `sudo apt install libvulkan1 vulkan-tools` (Debian/Ubuntu).
### Out of VRAM
```
Error: vkAllocateMemory failed
```
- Lower the **tile size** in the upscale settings (e.g., from `0` to `128` or `64`).
- Close other GPU-intensive applications.
- Use a model with lower memory requirements (e.g., `realesr-animevideov3`).
### Server won't start
- Check if the port is already in use: `ss -tlnp | grep 8000`.
- Review the log file: `cat server.log`.
- Ensure the virtual environment is activated or was created by `setup.sh`.
### FFmpeg errors
- Confirm `ffmpeg` and `ffprobe` are installed: `ffmpeg -version`.
- Make sure the input video is not corrupted: `ffprobe input.mp4`.
- Check available disk space — upscaling generates large intermediate frame sequences.
---
## 📄 License
This project uses [Real-ESRGAN](https://github.com/xinntao/Real-ESRGAN) by [xinntao](https://github.com/xinntao), licensed under the [BSD-3-Clause License](https://github.com/xinntao/Real-ESRGAN/blob/master/LICENSE).
+684
View File
@@ -0,0 +1,684 @@
import os
import uuid
import queue
import threading
import asyncio
import subprocess
import urllib.request
import json
from typing import Dict, List, Any
from fastapi import FastAPI, UploadFile, File, Form, BackgroundTasks, HTTPException, WebSocket, WebSocketDisconnect
from fastapi.responses import FileResponse, JSONResponse
from fastapi.staticfiles import StaticFiles
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel
import app.upscaler as upscaler
app = FastAPI(title="AI Video Upscaler Dashboard")
# CORS middleware for local development
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# In-memory databases
jobs_db: Dict[str, upscaler.UpscaleJob] = {}
ws_connections: Dict[str, List[WebSocket]] = {}
preview_db: Dict[str, Dict[str, str]] = {} # preview_id -> {orig, upscaled}
# FIFO queue for upscaling jobs to prevent GPU memory overload
job_queue = queue.Queue()
queue_lock = threading.Lock()
current_running_job_id = None
main_loop = None
@app.on_event("startup")
def startup_event():
global main_loop
main_loop = asyncio.get_event_loop()
global_webhook_url = None
class WebhookSetupRequest(BaseModel):
url: str
def send_webhook_notification(url: str, payload: dict):
def post():
try:
req = urllib.request.Request(
url,
data=json.dumps(payload).encode('utf-8'),
headers={'Content-Type': 'application/json'}
)
with urllib.request.urlopen(req, timeout=5) as response:
response.read()
except Exception as e:
print(f"Webhook notification failed to {url}: {e}")
threading.Thread(target=post, daemon=True).start()
# Broadcast updates to websockets and webhooks
def broadcast_progress(job_id: str, data: dict):
job = jobs_db.get(job_id)
if job:
data["is_preview"] = getattr(job, "is_preview", False)
if getattr(job, "is_preview", False):
data["original_preview_file"] = f"original_{job_id}.{getattr(job, 'transcode_format', 'mp4')}"
if job_id in ws_connections and main_loop is not None:
active_sockets = list(ws_connections[job_id])
for websocket in active_sockets:
try:
# Schedule the coroutine in the main event loop
asyncio.run_coroutine_threadsafe(websocket.send_json(data), main_loop)
except Exception as e:
print(f"Error sending websocket message: {e}")
# Process webhooks
job = jobs_db.get(job_id)
webhook_url = None
if job and getattr(job, "webhook_url", None):
webhook_url = job.webhook_url
elif global_webhook_url:
webhook_url = global_webhook_url
if webhook_url:
payload = {
"job_id": job_id,
"status": data.get("status"),
"progress": data.get("progress"),
"current_frame": data.get("current_frame"),
"total_frames": data.get("total_frames"),
"eta": data.get("eta"),
"error": data.get("error"),
"output_file": data.get("output_file")
}
send_webhook_notification(webhook_url, payload)
# Sequential queue processor thread
def queue_worker():
global current_running_job_id
while True:
try:
job_id = job_queue.get()
if job_id is None:
break
job = jobs_db.get(job_id)
if not job or job.status == "cancelled":
job_queue.task_done()
continue
with queue_lock:
current_running_job_id = job_id
print(f"Starting job {job_id}...")
upscaler.run_upscale_pipeline(job, broadcast_progress)
with queue_lock:
current_running_job_id = None
job_queue.task_done()
except Exception as e:
print(f"Error in queue worker: {e}")
# Start the background queue worker thread
worker_thread = threading.Thread(target=queue_worker, daemon=True)
worker_thread.start()
class StartUpscaleRequest(BaseModel):
file_id: str
model: str = "realesr-animevideov3"
scale: int = 4
tile_size: int = 256
preserve_audio: bool = True
ss: str | None = None
t: str | None = None
gpu_ids: str | None = None
tta: bool = False
unsharp: bool = False
double_fps: bool = False
preserve_subtitles: bool = True
start_sec: float | None = None
end_sec: float | None = None
crf: int = 18
preset: str = "medium"
denoise: bool = False
sharpen: bool = False
interpolation: bool = False
webhook_url: str | None = None
transcode_format: str = "mp4"
is_preview: bool = False
class PreviewRequest(BaseModel):
file_id: str
timestamp_sec: float = 1.0
model: str = "realesr-animevideov3"
scale: int = 4
tile_size: int = 256
gpu_ids: str | None = None
# Endpoints
@app.post("/api/upload")
async def upload_video(file: UploadFile = File(...)):
"""Upload video to workspace directory and parse metadata"""
file_id = str(uuid.uuid4())
ext = os.path.splitext(file.filename)[1].lower()
if ext not in [".mp4", ".mkv", ".avi", ".mov", ".webm"]:
raise HTTPException(status_code=400, detail="Invalid video format. Supported: .mp4, .mkv, .avi, .mov, .webm")
save_path = os.path.join(upscaler.UPLOAD_DIR, f"{file_id}{ext}")
with open(save_path, "wb") as buffer:
content = await file.read()
buffer.write(content)
info = upscaler.get_video_info(save_path)
if not info:
# cleanup if reading failed
os.remove(save_path)
raise HTTPException(status_code=400, detail="Could not read video file metadata. File may be corrupted.")
return {
"file_id": file_id,
"filename": file.filename,
"path": save_path,
"metadata": info
}
@app.post("/api/upscale/start")
def start_upscale(req: StartUpscaleRequest):
"""Queue upscaling task"""
# Find uploaded file
file_path = None
exts = [".mp4", ".mkv", ".avi", ".mov", ".webm"]
for ext in exts:
test_path = os.path.join(upscaler.UPLOAD_DIR, f"{req.file_id}{ext}")
if os.path.exists(test_path):
file_path = test_path
break
if not file_path:
raise HTTPException(status_code=404, detail="Uploaded file not found.")
job_id = str(uuid.uuid4())
job = upscaler.UpscaleJob(
job_id=job_id,
video_path=file_path,
model=req.model,
scale=req.scale,
tile_size=req.tile_size,
preserve_audio=req.preserve_audio,
ss=req.ss,
t=req.t,
gpu_ids=req.gpu_ids,
tta=req.tta,
unsharp=req.unsharp,
double_fps=req.double_fps,
preserve_subtitles=req.preserve_subtitles,
start_sec=req.start_sec,
end_sec=req.end_sec,
crf=req.crf,
preset=req.preset,
denoise=req.denoise,
sharpen=req.sharpen,
interpolation=req.interpolation,
webhook_url=req.webhook_url,
transcode_format=req.transcode_format,
is_preview=req.is_preview
)
jobs_db[job_id] = job
job_queue.put(job_id)
# Broadcast initial queued progress
broadcast_progress(job_id, {
"status": "queued",
"progress": 0.0,
"current_frame": 0,
"total_frames": 0,
"eta": "Calculating..."
})
return {
"job_id": job_id,
"status": "queued"
}
@app.get("/api/upscale/status/{job_id}")
def get_status(job_id: str):
"""Get status of upscale job"""
job = jobs_db.get(job_id)
if not job:
raise HTTPException(status_code=404, detail="Job not found.")
return {
"job_id": job.job_id,
"status": job.status,
"progress": job.progress,
"current_frame": job.current_frame,
"total_frames": job.total_frames,
"eta": job.eta,
"error": job.error,
"output_file": os.path.basename(job.output_file) if job.output_file else None,
"is_preview": getattr(job, "is_preview", False),
"original_preview_file": f"original_{job_id}.{getattr(job, 'transcode_format', 'mp4')}" if getattr(job, "is_preview", False) else None
}
@app.post("/api/upscale/cancel/{job_id}")
def cancel_job(job_id: str):
"""Cancel a running or queued job"""
job = jobs_db.get(job_id)
if not job:
raise HTTPException(status_code=404, detail="Job not found.")
job.cancel()
# Broadcast cancellation status
broadcast_progress(job_id, {
"status": "cancelled",
"progress": job.progress,
"current_frame": job.current_frame,
"total_frames": job.total_frames,
"eta": "N/A"
})
return {"job_id": job_id, "status": "cancelled"}
@app.post("/api/preview/generate")
async def generate_preview(req: PreviewRequest):
"""Generate high quality upscaled single frame preview"""
file_path = None
exts = [".mp4", ".mkv", ".avi", ".mov", ".webm"]
for ext in exts:
test_path = os.path.join(upscaler.UPLOAD_DIR, f"{req.file_id}{ext}")
if os.path.exists(test_path):
file_path = test_path
break
if not file_path:
raise HTTPException(status_code=404, detail="Video file not found.")
preview_id = str(uuid.uuid4())
preview_temp_dir = os.path.join(upscaler.TEMP_DIR, f"preview_{preview_id}")
os.makedirs(preview_temp_dir, exist_ok=True)
orig_path = os.path.join(preview_temp_dir, "orig.jpg")
upscaled_path = os.path.join(preview_temp_dir, "upscaled.jpg")
# Extract frame
extracted = upscaler.extract_single_frame(file_path, req.timestamp_sec, orig_path)
if not extracted:
raise HTTPException(status_code=500, detail="Failed to extract preview frame from video.")
# Upscale frame
upscaled = upscaler.upscale_image_file(orig_path, upscaled_path, req.model, req.scale, req.tile_size, req.gpu_ids)
if not upscaled:
raise HTTPException(status_code=500, detail="Failed to upscale preview frame.")
preview_db[preview_id] = {
"orig": orig_path,
"upscaled": upscaled_path
}
return {"preview_id": preview_id}
@app.get("/api/preview/original/{preview_id}")
def get_preview_original(preview_id: str):
paths = preview_db.get(preview_id)
if not paths or not os.path.exists(paths["orig"]):
raise HTTPException(status_code=404, detail="Preview frame not found.")
return FileResponse(paths["orig"])
@app.get("/api/preview/upscaled/{preview_id}")
def get_preview_upscaled(preview_id: str):
paths = preview_db.get(preview_id)
if not paths or not os.path.exists(paths["upscaled"]):
raise HTTPException(status_code=404, detail="Preview frame not found.")
return FileResponse(paths["upscaled"])
@app.get("/api/models")
def get_models():
"""Scan models directory and return list of available models"""
models_dir = os.path.join(upscaler.BASE_DIR, "realesrgan-bin", "models")
if not os.path.exists(models_dir):
return []
models = set()
for filename in os.listdir(models_dir):
if filename.endswith(".param"):
name = filename[:-6] # strip '.param'
# Strip scale suffix if present
for suffix in ["-x2", "-x3", "-x4"]:
if name.endswith(suffix):
name = name[:-len(suffix)]
break
models.add(name)
return sorted(list(models))
@app.get("/api/download/{filename}")
def download_file(filename: str):
file_path = os.path.join(upscaler.OUTPUT_DIR, filename)
if not os.path.exists(file_path):
raise HTTPException(status_code=404, detail="File not found.")
return FileResponse(file_path, media_type="application/octet-stream", filename=filename)
def safe_delete_file(file_path: str):
if file_path and os.path.exists(file_path):
try:
os.remove(file_path)
print(f"Deleted file: {file_path}")
except Exception as e:
print(f"Failed to delete {file_path}: {e}")
@app.get("/api/jobs")
def list_jobs():
"""List details of all submitted jobs"""
return [
{
"job_id": job.job_id,
"status": job.status,
"progress": job.progress,
"current_frame": job.current_frame,
"total_frames": job.total_frames,
"eta": job.eta,
"error": job.error,
"model": job.model,
"scale": job.scale,
"output_file": os.path.basename(job.output_file) if job.output_file else None,
"video_path": job.video_path,
"is_preview": getattr(job, "is_preview", False)
}
for job in jobs_db.values()
]
@app.delete("/api/jobs/{job_id}")
@app.post("/api/jobs/delete/{job_id}")
def delete_job(job_id: str):
job = jobs_db.get(job_id)
if not job:
raise HTTPException(status_code=404, detail="Job not found.")
# Safely delete uploaded input file
if job.video_path:
safe_delete_file(job.video_path)
# Safely delete original preview video if present
for ext in [".mp4", ".mkv", ".avi", ".mov", ".webm"]:
orig_prev_path = os.path.join(upscaler.OUTPUT_DIR, f"original_{job_id}{ext}")
if os.path.exists(orig_prev_path):
safe_delete_file(orig_prev_path)
# Safely delete upscaled output file
if job.output_file:
safe_delete_file(job.output_file)
# Clean up any residual temp folder
job_temp_dir = os.path.join(upscaler.TEMP_DIR, job_id)
if os.path.exists(job_temp_dir):
try:
import shutil
shutil.rmtree(job_temp_dir)
except Exception:
pass
# Delete from in-memory db
if job_id in jobs_db:
del jobs_db[job_id]
return {"job_id": job_id, "status": "purged"}
@app.post("/api/jobs/purge-all")
def purge_all_jobs():
# Purge uploads folder
for f in os.listdir(upscaler.UPLOAD_DIR):
safe_delete_file(os.path.join(upscaler.UPLOAD_DIR, f))
# Purge outputs folder
for f in os.listdir(upscaler.OUTPUT_DIR):
safe_delete_file(os.path.join(upscaler.OUTPUT_DIR, f))
# Purge temp folder
for f in os.listdir(upscaler.TEMP_DIR):
path = os.path.join(upscaler.TEMP_DIR, f)
try:
if os.path.isdir(path):
import shutil
shutil.rmtree(path)
else:
os.remove(path)
except Exception:
pass
# Reset in-memory database
jobs_db.clear()
return {"status": "all purged"}
# Websocket endpoint for real-time progress updates
@app.websocket("/ws/progress/{job_id}")
async def websocket_progress(websocket: WebSocket, job_id: str):
await websocket.accept()
if job_id not in ws_connections:
ws_connections[job_id] = []
ws_connections[job_id].append(websocket)
# Send current state immediately
job = jobs_db.get(job_id)
if job:
await websocket.send_json({
"status": job.status,
"progress": job.progress,
"current_frame": job.current_frame,
"total_frames": job.total_frames,
"eta": job.eta,
"error": job.error,
"output_file": os.path.basename(job.output_file) if job.output_file else None
})
try:
while True:
# Just keep the connection alive
await websocket.receive_text()
except WebSocketDisconnect:
if job_id in ws_connections:
ws_connections[job_id].remove(websocket)
if not ws_connections[job_id]:
del ws_connections[job_id]
# Clean up previews on exit
@app.on_event("shutdown")
def cleanup_previews():
for preview_id, paths in preview_db.items():
try:
parent_dir = os.path.dirname(paths["orig"])
if os.path.exists(parent_dir):
import shutil
shutil.rmtree(parent_dir)
except Exception:
pass
# System Diagnostics Endpoint
@app.get("/api/diagnostics")
def get_diagnostics():
# RAM Diagnostics
ram = {"total": 0, "available": 0, "used": 0, "percent": 0.0}
try:
if os.path.exists('/proc/meminfo'):
mem_info = {}
with open('/proc/meminfo', 'r') as f:
for line in f:
parts = line.split(':')
if len(parts) == 2:
name = parts[0].strip()
val = parts[1].replace('kB', '').strip()
mem_info[name] = int(val)
total = mem_info.get('MemTotal', 0) * 1024
free = mem_info.get('MemFree', 0) * 1024
available = mem_info.get('MemAvailable', total - free) * 1024
used = total - available
percent = round((used / total) * 100, 1) if total > 0 else 0.0
ram = {
"total": total,
"available": available,
"used": used,
"percent": percent
}
except Exception as e:
print(f"Error getting RAM diagnostics: {e}")
# CPU Diagnostics
cpu = {"count": os.cpu_count(), "load_avg": [], "percent": 0.0}
try:
load_1, load_5, load_15 = os.getloadavg()
cpu["load_avg"] = [load_1, load_5, load_15]
cpu["percent"] = min(100.0, round((load_1 / cpu["count"]) * 100, 1)) if cpu["count"] else 0.0
except Exception as e:
print(f"Error getting CPU diagnostics: {e}")
# GPU Diagnostics
gpu = {"available": False, "gpus": []}
try:
cmd = [
"nvidia-smi",
"--query-gpu=utilization.gpu,utilization.memory,memory.total,memory.free,memory.used,name,temperature.gpu",
"--format=csv,noheader,nounits"
]
res = subprocess.run(cmd, capture_output=True, text=True, check=True)
lines = res.stdout.strip().split('\n')
gpu_list = []
for line in lines:
if not line.strip():
continue
parts = [p.strip() for p in line.split(',')]
if len(parts) >= 7:
gpu_list.append({
"name": parts[5],
"gpu_utilization_percent": float(parts[0]),
"memory_utilization_percent": float(parts[1]),
"memory_total_mb": float(parts[2]),
"memory_free_mb": float(parts[3]),
"memory_used_mb": float(parts[4]),
"temperature_c": float(parts[6])
})
if gpu_list:
gpu["available"] = True
gpu["gpus"] = gpu_list
except Exception as e:
gpu["error"] = str(e)
return {
"ram": ram,
"cpu": cpu,
"gpu": gpu
}
# Outputs Gallery Endpoints
@app.get("/api/outputs")
def list_outputs():
output_dir = upscaler.OUTPUT_DIR
if not os.path.exists(output_dir):
return []
files = []
for filename in os.listdir(output_dir):
if filename.startswith("original_"):
continue
file_path = os.path.join(output_dir, filename)
if os.path.isfile(file_path):
stat = os.stat(file_path)
files.append({
"filename": filename,
"size": stat.st_size,
"modified": stat.st_mtime,
"url": f"/api/download/{filename}"
})
files.sort(key=lambda x: x["modified"], reverse=True)
return files
@app.delete("/api/outputs/delete/{filename}")
@app.post("/api/outputs/delete/{filename}")
def delete_output(filename: str):
file_path = os.path.join(upscaler.OUTPUT_DIR, filename)
resolved_path = os.path.abspath(file_path)
if not resolved_path.startswith(os.path.abspath(upscaler.OUTPUT_DIR)):
raise HTTPException(status_code=403, detail="Access denied.")
if not os.path.exists(file_path):
raise HTTPException(status_code=404, detail="File not found.")
try:
os.remove(file_path)
return {"filename": filename, "status": "deleted"}
except Exception as e:
raise HTTPException(status_code=500, detail=f"Failed to delete file: {e}")
# Custom Model Uploading Endpoint
@app.post("/api/models/upload")
async def upload_model(
param_file: UploadFile = File(None),
bin_file: UploadFile = File(None),
files: List[UploadFile] = File(None)
):
models_dir = os.path.join(upscaler.BASE_DIR, "realesrgan-bin", "models")
os.makedirs(models_dir, exist_ok=True)
uploaded_files = []
all_files = []
if files:
all_files.extend(files)
if param_file:
all_files.append(param_file)
if bin_file:
all_files.append(bin_file)
if not all_files:
raise HTTPException(status_code=400, detail="No files uploaded. Please upload .param or .bin files.")
for f in all_files:
filename = f.filename
ext = os.path.splitext(filename)[1].lower()
if ext not in [".param", ".bin"]:
raise HTTPException(
status_code=400,
detail=f"Invalid file extension: {ext}. Only .param and .bin files are allowed for models."
)
safe_filename = os.path.basename(filename)
save_path = os.path.join(models_dir, safe_filename)
with open(save_path, "wb") as buffer:
content = await f.read()
buffer.write(content)
uploaded_files.append(safe_filename)
return {
"status": "success",
"uploaded": uploaded_files
}
# Webhook Setup/Retrieval Endpoints
@app.post("/api/webhook/setup")
def setup_webhook(req: WebhookSetupRequest):
global global_webhook_url
global_webhook_url = req.url
return {"status": "success", "webhook_url": global_webhook_url}
@app.get("/api/uploads/{filename}")
def get_upload_file(filename: str):
file_path = os.path.join(upscaler.UPLOAD_DIR, filename)
resolved_path = os.path.abspath(file_path)
if not resolved_path.startswith(os.path.abspath(upscaler.UPLOAD_DIR)):
raise HTTPException(status_code=403, detail="Access denied.")
if not os.path.exists(file_path):
raise HTTPException(status_code=404, detail="File not found.")
return FileResponse(file_path)
# Mount static folder
app.mount("/", StaticFiles(directory=os.path.join(upscaler.BASE_DIR, "static"), html=True), name="static")
+519
View File
@@ -0,0 +1,519 @@
import os
import subprocess
import json
import shutil
import time
import threading
from typing import Dict, Any, Callable
# Paths config
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
BIN_PATH = os.path.join(BASE_DIR, "realesrgan-bin", "realesrgan-ncnn-vulkan")
UPLOAD_DIR = os.path.join(BASE_DIR, "uploads")
OUTPUT_DIR = os.path.join(BASE_DIR, "outputs")
TEMP_DIR = os.path.join(BASE_DIR, "temp")
# Ensure directories exist
for d in [UPLOAD_DIR, OUTPUT_DIR, TEMP_DIR]:
os.makedirs(d, exist_ok=True)
class UpscaleJob:
def __init__(self, job_id: str, video_path: str, model: str, scale: int, tile_size: int, preserve_audio: bool,
ss: str = None, t: str = None, gpu_ids: str = None, tta: bool = False,
unsharp: bool = False, double_fps: bool = False, preserve_subtitles: bool = True,
start_sec: float = None, end_sec: float = None, crf: int = 18, preset: str = "medium",
denoise: bool = False, sharpen: bool = False, interpolation: bool = False,
webhook_url: str = None, transcode_format: str = "mp4", is_preview: bool = False):
self.job_id = job_id
self.video_path = video_path
self.model = model
self.scale = scale
self.tile_size = tile_size
self.preserve_audio = preserve_audio
# Trim mapping
if ss is not None:
self.ss = ss
elif start_sec is not None:
self.ss = str(start_sec)
else:
self.ss = None
if t is not None:
self.t = t
elif end_sec is not None:
start = float(self.ss) if self.ss is not None else 0.0
self.t = str(max(0.0, end_sec - start))
else:
self.t = None
self.gpu_ids = gpu_ids
self.tta = tta
self.unsharp = unsharp
self.double_fps = double_fps
self.preserve_subtitles = preserve_subtitles
self.start_sec = start_sec
self.end_sec = end_sec
self.crf = crf
self.preset = preset
self.denoise = denoise
self.sharpen = sharpen
self.interpolation = interpolation
self.webhook_url = webhook_url
self.transcode_format = transcode_format
self.is_preview = is_preview
self.original_preview_file = None
self.status = "pending"
self.progress = 0.0
self.current_frame = 0
self.total_frames = 0
self.eta = "Calculating..."
self.error = None
self.start_time = None
self.output_file = None
# Track processes to allow cancellation
self._processes = []
self._is_cancelled = False
self._lock = threading.Lock()
def update_status(self, status: str, progress: float = None, current_frame: int = None, eta: str = None, error: str = None):
with self._lock:
self.status = status
if progress is not None:
self.progress = round(progress, 2)
if current_frame is not None:
self.current_frame = current_frame
if eta is not None:
self.eta = eta
if error is not None:
self.error = error
def cancel(self):
with self._lock:
self._is_cancelled = True
self.status = "cancelled"
self.eta = "N/A"
for p in self._processes:
try:
p.terminate()
p.wait(timeout=2)
except Exception:
try:
p.kill()
except Exception:
pass
self._processes.clear()
def run_command(self, cmd: list, shell=False) -> subprocess.Popen:
with self._lock:
if self._is_cancelled:
raise InterruptedError("Job was cancelled")
p = subprocess.Popen(
cmd,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
shell=shell
)
self._processes.append(p)
return p
def cleanup_process(self, p: subprocess.Popen):
with self._lock:
if p in self._processes:
self._processes.remove(p)
def get_video_info(video_path: str) -> dict:
"""Extract metadata using ffprobe"""
cmd = [
"ffprobe",
"-v", "error",
"-select_streams", "v:0",
"-show_entries", "stream=avg_frame_rate,duration,width,height,codec_name,nb_frames",
"-of", "json",
video_path
]
try:
result = subprocess.run(cmd, capture_output=True, text=True, check=True)
data = json.loads(result.stdout)
if not data.get("streams"):
return {}
stream = data["streams"][0]
# Calculate FPS
fps_str = stream.get("avg_frame_rate", "30/1")
if "/" in fps_str:
num, den = map(float, fps_str.split("/"))
fps = num / den if den != 0 else 30.0
else:
fps = float(fps_str) if fps_str else 30.0
# Get total frames
nb_frames = stream.get("nb_frames")
if nb_frames and nb_frames.isdigit():
total_frames = int(nb_frames)
else:
duration = float(stream.get("duration", 0))
total_frames = int(duration * fps)
return {
"width": int(stream.get("width", 0)),
"height": int(stream.get("height", 0)),
"fps": round(fps, 3),
"duration": round(float(stream.get("duration", 0)), 2),
"codec": stream.get("codec_name", "unknown"),
"total_frames": total_frames
}
except Exception as e:
print(f"Error reading video info: {e}")
return {}
def extract_single_frame(video_path: str, timestamp_sec: float, output_path: str) -> bool:
"""Extract a single frame at timestamp for preview"""
cmd = [
"ffmpeg", "-y",
"-ss", str(timestamp_sec),
"-i", video_path,
"-vframes", "1",
"-f", "image2",
output_path
]
try:
subprocess.run(cmd, capture_output=True, check=True)
return os.path.exists(output_path)
except Exception as e:
print(f"Error extracting single frame: {e}")
return False
def upscale_image_file(input_path: str, output_path: str, model: str, scale: int, tile_size: int, gpu_ids: str = None) -> bool:
"""Run Real-ESRGAN on a single image file"""
cmd = [
BIN_PATH,
"-i", input_path,
"-o", output_path,
"-n", model,
"-s", str(scale),
"-t", str(tile_size)
]
if gpu_ids is not None:
cmd.extend(["-g", str(gpu_ids)])
try:
subprocess.run(cmd, capture_output=True, check=True)
return os.path.exists(output_path)
except Exception as e:
print(f"Error upscaling single image: {e}")
return False
def run_upscale_pipeline(job: UpscaleJob, on_progress_update: Callable[[str, Dict[str, Any]], None]):
job.start_time = time.time()
job.update_status("analyzing", progress=5)
# Get video info
info = get_video_info(job.video_path)
if not info:
job.update_status("failed", error="Could not read video metadata.")
on_progress_update(job.job_id, {"status": "failed", "error": "Could not read video metadata"})
return
job.total_frames = info["total_frames"]
fps = info["fps"]
# Create job temp directories
job_temp_dir = os.path.join(TEMP_DIR, job.job_id)
input_frames_dir = os.path.join(job_temp_dir, "input_frames")
output_frames_dir = os.path.join(job_temp_dir, "output_frames")
os.makedirs(input_frames_dir, exist_ok=True)
os.makedirs(output_frames_dir, exist_ok=True)
try:
# If preview, extract the original 5s clip first
transcode_fmt = getattr(job, "transcode_format", "mp4")
if getattr(job, "is_preview", False):
orig_preview_filename = f"original_{job.job_id}.{transcode_fmt}"
orig_preview_filepath = os.path.join(OUTPUT_DIR, orig_preview_filename)
job.original_preview_file = orig_preview_filepath
cut_cmd = ["ffmpeg", "-y"]
if job.ss is not None:
cut_cmd.extend(["-ss", str(job.ss)])
if job.t is not None:
cut_cmd.extend(["-t", str(job.t)])
cut_cmd.extend([
"-i", job.video_path,
"-map", "0:v:0",
"-map", "0:a:0?",
"-c:v", "libx264",
"-c:a", "aac",
orig_preview_filepath
])
try:
subprocess.run(cut_cmd, capture_output=True, check=True)
except Exception as cut_err:
print(f"Error cutting original preview video: {cut_err}")
# Step 1: Extract Frames
job.update_status("extracting", progress=10)
on_progress_update(job.job_id, {"status": "extracting", "progress": 10})
# High quality JPG frames to balance disk usage and speed
extract_cmd = ["ffmpeg", "-y"]
if job.ss is not None:
extract_cmd.extend(["-ss", str(job.ss)])
if job.t is not None:
extract_cmd.extend(["-t", str(job.t)])
extract_cmd.extend(["-i", job.video_path])
# Apply unsharp pre-filter if enabled
if getattr(job, "unsharp", False):
extract_cmd.extend(["-vf", "unsharp"])
extract_cmd.extend([
"-q:v", "2",
os.path.join(input_frames_dir, "frame_%08d.jpg")
])
p_extract = job.run_command(extract_cmd)
stdout, stderr = p_extract.communicate()
job.cleanup_process(p_extract)
if p_extract.returncode != 0:
raise RuntimeError(f"FFmpeg frame extraction failed: {stderr}")
# Count actual frames extracted
extracted_files = sorted([f for f in os.listdir(input_frames_dir) if f.startswith("frame_")])
actual_total = len(extracted_files)
if actual_total == 0:
raise RuntimeError("No frames extracted from video")
job.total_frames = actual_total
# Step 2: Upscale Frames
job.update_status("upscaling", progress=20, current_frame=0)
on_progress_update(job.job_id, {"status": "upscaling", "progress": 20, "current_frame": 0, "total_frames": actual_total})
current_tile_size = job.tile_size
while True:
# Launch Real-ESRGAN on directory
upscale_cmd = [
BIN_PATH,
"-i", input_frames_dir,
"-o", output_frames_dir,
"-n", job.model,
"-s", str(job.scale),
"-t", str(current_tile_size),
"-f", "jpg"
]
if getattr(job, "gpu_ids", None) is not None:
upscale_cmd.extend(["-g", str(job.gpu_ids)])
if getattr(job, "tta", False):
upscale_cmd.append("-x")
upscale_start_time = time.time()
p_upscale = job.run_command(upscale_cmd)
# Monitor thread for output files
while p_upscale.poll() is None:
if job._is_cancelled:
return
processed_files = len(os.listdir(output_frames_dir))
progress_pct = 20.0 + (float(processed_files) / actual_total) * 60.0 # upscaling is 20% to 80%
# Estimate ETA
elapsed = time.time() - upscale_start_time
if processed_files > 0:
sec_per_frame = elapsed / processed_files
rem_frames = actual_total - processed_files
eta_sec = rem_frames * sec_per_frame
# Format ETA
if eta_sec > 60:
eta_str = f"{int(eta_sec // 60)}m {int(eta_sec % 60)}s"
else:
eta_str = f"{int(eta_sec)}s"
else:
eta_str = "Calculating..."
job.update_status("upscaling", progress=progress_pct, current_frame=processed_files, eta=eta_str)
on_progress_update(job.job_id, {
"status": "upscaling",
"progress": progress_pct,
"current_frame": processed_files,
"total_frames": actual_total,
"eta": eta_str
})
time.sleep(0.5)
stdout, stderr = p_upscale.communicate()
job.cleanup_process(p_upscale)
if job._is_cancelled:
return
if p_upscale.returncode != 0:
err_msg = (stdout or "") + "\n" + (stderr or "")
is_alloc_error = any(x in err_msg.lower() for x in ["vkallocatememory", "out of memory", "allocation", "vram", "failed to allocate"])
if is_alloc_error:
if current_tile_size <= 0:
next_tile_size = 256
else:
next_tile_size = current_tile_size // 2
if next_tile_size >= 32:
print(f"Job {job.job_id}: Real-ESRGAN failed with VRAM allocation error. Retrying with tile size halved from {current_tile_size} to {next_tile_size}.")
current_tile_size = next_tile_size
# Clean up output frames directory before retrying
for filename in os.listdir(output_frames_dir):
file_path = os.path.join(output_frames_dir, filename)
try:
if os.path.isfile(file_path) or os.path.islink(file_path):
os.unlink(file_path)
elif os.path.isdir(file_path):
shutil.rmtree(file_path)
except Exception as cleanup_err:
print(f"Error cleaning file {file_path}: {cleanup_err}")
continue
raise RuntimeError(f"Real-ESRGAN failed with exit code {p_upscale.returncode}: {err_msg}")
else:
break
# Final validation of upscale output
processed_files = len(os.listdir(output_frames_dir))
job.update_status("upscaling", progress=80.0, current_frame=processed_files)
# Step 3: Reassemble video
job.update_status("assembling", progress=85.0)
on_progress_update(job.job_id, {"status": "assembling", "progress": 85.0})
transcode_fmt = getattr(job, "transcode_format", "mp4")
out_filename = f"upscaled_{job.job_id}.{transcode_fmt}"
out_filepath = os.path.join(OUTPUT_DIR, out_filename)
job.output_file = out_filepath
# Choose codecs based on format
vcodec = "libx264"
acodec = "copy"
if transcode_fmt == "webm":
vcodec = "libvpx-vp9"
acodec = "libvorbis"
# Construct ffmpeg reassembly command
assemble_cmd = [
"ffmpeg", "-y",
"-framerate", str(fps),
"-i", os.path.join(output_frames_dir, "frame_%08d.jpg")
]
# We need the original video as the second input (index 1) if we preserve audio or subtitles
need_orig_input = job.preserve_audio or getattr(job, "preserve_subtitles", True)
if need_orig_input:
orig_input_cmd = []
if getattr(job, "ss", None) is not None:
orig_input_cmd.extend(["-ss", str(job.ss)])
if getattr(job, "t", None) is not None:
orig_input_cmd.extend(["-t", str(job.t)])
orig_input_cmd.extend(["-i", job.video_path])
assemble_cmd.extend(orig_input_cmd)
assemble_cmd.extend(["-map", "0:v:0"])
if job.preserve_audio:
assemble_cmd.extend([
"-map", "1:a:0?",
"-c:a", acodec
])
if getattr(job, "preserve_subtitles", True):
assemble_cmd.extend([
"-map", "1:s?",
"-c:s", "mov_text"
])
# Apply filters
vf_filters = []
if getattr(job, "denoise", False):
vf_filters.append("hqdn3d")
if getattr(job, "sharpen", False) or getattr(job, "unsharp", False):
vf_filters.append("unsharp=3:3:0.5:3:3:0.5")
if getattr(job, "interpolation", False) or getattr(job, "double_fps", False):
target_fps = fps * 2 if getattr(job, "double_fps", False) else 60
if target_fps < fps:
target_fps = fps
vf_filters.append(f"minterpolate=fps={target_fps}")
if vf_filters:
assemble_cmd.extend(["-vf", ",".join(vf_filters)])
assemble_cmd.extend([
"-c:v", vcodec,
"-pix_fmt", "yuv420p",
"-crf", str(getattr(job, "crf", 18)),
"-preset", getattr(job, "preset", "medium"),
out_filepath
])
p_assemble = job.run_command(assemble_cmd)
stdout, stderr = p_assemble.communicate()
job.cleanup_process(p_assemble)
if p_assemble.returncode != 0:
raise RuntimeError(f"FFmpeg video assembly failed: {stderr}")
# Step 4: Complete
job.update_status("completed", progress=100.0, eta="Done")
on_progress_update(job.job_id, {
"status": "completed",
"progress": 100.0,
"eta": "Done",
"output_file": out_filename
})
except Exception as e:
import traceback
traceback.print_exc()
if not job._is_cancelled:
job.update_status("failed", error=str(e))
on_progress_update(job.job_id, {"status": "failed", "error": str(e)})
finally:
# Clean up temp frames to save space
try:
if os.path.exists(job_temp_dir):
shutil.rmtree(job_temp_dir)
except Exception as cleanup_err:
print(f"Error during temp cleanup: {cleanup_err}")
# Trigger webhook if URL is provided
if getattr(job, "webhook_url", None):
def trigger_webhook_task():
import urllib.request
import json
try:
payload = {
"job_id": job.job_id,
"status": job.status,
"progress": job.progress,
"error": job.error,
"output_file": os.path.basename(job.output_file) if job.output_file else None,
"model": job.model,
"scale": job.scale,
"duration": round(time.time() - job.start_time, 2) if job.start_time else 0
}
req = urllib.request.Request(
job.webhook_url,
data=json.dumps(payload).encode('utf-8'),
headers={'Content-Type': 'application/json'},
method='POST'
)
with urllib.request.urlopen(req, timeout=5) as response:
pass
except Exception as ex:
print(f"Error triggering webhook: {ex}")
threading.Thread(target=trigger_webhook_task, daemon=True).start()
+45
View File
@@ -0,0 +1,45 @@
# RealESRGAN
We have provided the following models:
1. realesr-animevideov3 (default)
2. realesrgan-x4plus
3. realesrgan-x4plus-anime
Command:
You may need to change file mode before running.
chmod u+x realesrgan-ncnn-vulkan
1. ./realesrgan-ncnn-vulkan -i input.jpg -o output.png
2. ./realesrgan-ncnn-vulkan -i input.jpg -o output.png -n realesr-animevideov3
3. ./realesrgan-ncnn-vulkan -i input_folder -o outputfolder -n realesr-animevideov3 -s 2 -f jpg
4. ./realesrgan-ncnn-vulkan -i input_folder -o outputfolder -n realesr-animevideov3 -s 4 -f jpg
Commands for enhancing anime videos:
1. Use ffmpeg to extract frames from a video (Remember to create the folder `tmp_frames` ahead)
ffmpeg -i onepiece_demo.mp4 -qscale:v 1 -qmin 1 -qmax 1 -vsync 0 tmp_frames/frame%08d.jpg
2. Inference with Real-ESRGAN executable file (Remember to create the folder `out_frames` ahead)
./realesrgan-ncnn-vulkan -i tmp_frames -o out_frames -n realesr-animevideov3 -s 2 -f jpg
3. Merge the enhanced frames back into a video
ffmpeg -i out_frames/frame%08d.jpg -i onepiece_demo.mp4 -map 0:v:0 -map 1:a:0 -c:a copy -c:v libx264 -r 23.98 -pix_fmt yuv420p output_w_audio.mp4
------------------------
GitHub: https://github.com/xinntao/Real-ESRGAN/
Paper: https://arxiv.org/abs/2107.10833
------------------------
This executable file is **portable** and includes all the binaries and models required. No CUDA or PyTorch environment is needed.
Note that it may introduce block inconsistency (and also generate slightly different results from the PyTorch implementation), because this executable file first crops the input image into several tiles, and then processes them separately, finally stitches together.
This executable file is based on the wonderful [Tencent/ncnn](https://github.com/Tencent/ncnn) and [realsr-ncnn-vulkan](https://github.com/nihui/realsr-ncnn-vulkan) by [nihui](https://github.com/nihui).
Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.
@@ -0,0 +1,43 @@
7767517
41 42
Input input.1 0 1 data
Split splitncnn_input0 1 2 data input.1_splitncnn_0 input.1_splitncnn_1
Convolution Conv_0 1 1 input.1_splitncnn_1 54 0=64 1=3 4=1 5=1 6=1728
PReLU PRelu_1 1 1 54 56 0=64
Convolution Conv_2 1 1 56 57 0=64 1=3 4=1 5=1 6=36864
PReLU PRelu_3 1 1 57 59 0=64
Convolution Conv_4 1 1 59 60 0=64 1=3 4=1 5=1 6=36864
PReLU PRelu_5 1 1 60 62 0=64
Convolution Conv_6 1 1 62 63 0=64 1=3 4=1 5=1 6=36864
PReLU PRelu_7 1 1 63 65 0=64
Convolution Conv_8 1 1 65 66 0=64 1=3 4=1 5=1 6=36864
PReLU PRelu_9 1 1 66 68 0=64
Convolution Conv_10 1 1 68 69 0=64 1=3 4=1 5=1 6=36864
PReLU PRelu_11 1 1 69 71 0=64
Convolution Conv_12 1 1 71 72 0=64 1=3 4=1 5=1 6=36864
PReLU PRelu_13 1 1 72 74 0=64
Convolution Conv_14 1 1 74 75 0=64 1=3 4=1 5=1 6=36864
PReLU PRelu_15 1 1 75 77 0=64
Convolution Conv_16 1 1 77 78 0=64 1=3 4=1 5=1 6=36864
PReLU PRelu_17 1 1 78 80 0=64
Convolution Conv_18 1 1 80 81 0=64 1=3 4=1 5=1 6=36864
PReLU PRelu_19 1 1 81 83 0=64
Convolution Conv_20 1 1 83 84 0=64 1=3 4=1 5=1 6=36864
PReLU PRelu_21 1 1 84 86 0=64
Convolution Conv_22 1 1 86 87 0=64 1=3 4=1 5=1 6=36864
PReLU PRelu_23 1 1 87 89 0=64
Convolution Conv_24 1 1 89 90 0=64 1=3 4=1 5=1 6=36864
PReLU PRelu_25 1 1 90 92 0=64
Convolution Conv_26 1 1 92 93 0=64 1=3 4=1 5=1 6=36864
PReLU PRelu_27 1 1 93 95 0=64
Convolution Conv_28 1 1 95 96 0=64 1=3 4=1 5=1 6=36864
PReLU PRelu_29 1 1 96 98 0=64
Convolution Conv_30 1 1 98 99 0=64 1=3 4=1 5=1 6=36864
PReLU PRelu_31 1 1 99 101 0=64
Convolution Conv_32 1 1 101 102 0=64 1=3 4=1 5=1 6=36864
PReLU PRelu_33 1 1 102 104 0=64
Convolution Conv_34 1 1 104 105 0=48 1=3 4=1 5=1 6=27648
PixelShuffle DepthToSpace_35 1 1 105 106 0=4
Interp Resize_37 1 1 input.1_splitncnn_0 111 0=1 1=4.000000e+00 2=4.000000e+00
BinaryOp Add_38 2 1 106 111 112
Interp Resize_40 1 1 112 output 0=3 1=5.000000e-01 2=5.000000e-01
Binary file not shown.
@@ -0,0 +1,43 @@
7767517
41 42
Input input.1 0 1 data
Split splitncnn_input0 1 2 data input.1_splitncnn_0 input.1_splitncnn_1
Convolution Conv_0 1 1 input.1_splitncnn_1 54 0=64 1=3 4=1 5=1 6=1728
PReLU PRelu_1 1 1 54 56 0=64
Convolution Conv_2 1 1 56 57 0=64 1=3 4=1 5=1 6=36864
PReLU PRelu_3 1 1 57 59 0=64
Convolution Conv_4 1 1 59 60 0=64 1=3 4=1 5=1 6=36864
PReLU PRelu_5 1 1 60 62 0=64
Convolution Conv_6 1 1 62 63 0=64 1=3 4=1 5=1 6=36864
PReLU PRelu_7 1 1 63 65 0=64
Convolution Conv_8 1 1 65 66 0=64 1=3 4=1 5=1 6=36864
PReLU PRelu_9 1 1 66 68 0=64
Convolution Conv_10 1 1 68 69 0=64 1=3 4=1 5=1 6=36864
PReLU PRelu_11 1 1 69 71 0=64
Convolution Conv_12 1 1 71 72 0=64 1=3 4=1 5=1 6=36864
PReLU PRelu_13 1 1 72 74 0=64
Convolution Conv_14 1 1 74 75 0=64 1=3 4=1 5=1 6=36864
PReLU PRelu_15 1 1 75 77 0=64
Convolution Conv_16 1 1 77 78 0=64 1=3 4=1 5=1 6=36864
PReLU PRelu_17 1 1 78 80 0=64
Convolution Conv_18 1 1 80 81 0=64 1=3 4=1 5=1 6=36864
PReLU PRelu_19 1 1 81 83 0=64
Convolution Conv_20 1 1 83 84 0=64 1=3 4=1 5=1 6=36864
PReLU PRelu_21 1 1 84 86 0=64
Convolution Conv_22 1 1 86 87 0=64 1=3 4=1 5=1 6=36864
PReLU PRelu_23 1 1 87 89 0=64
Convolution Conv_24 1 1 89 90 0=64 1=3 4=1 5=1 6=36864
PReLU PRelu_25 1 1 90 92 0=64
Convolution Conv_26 1 1 92 93 0=64 1=3 4=1 5=1 6=36864
PReLU PRelu_27 1 1 93 95 0=64
Convolution Conv_28 1 1 95 96 0=64 1=3 4=1 5=1 6=36864
PReLU PRelu_29 1 1 96 98 0=64
Convolution Conv_30 1 1 98 99 0=64 1=3 4=1 5=1 6=36864
PReLU PRelu_31 1 1 99 101 0=64
Convolution Conv_32 1 1 101 102 0=64 1=3 4=1 5=1 6=36864
PReLU PRelu_33 1 1 102 104 0=64
Convolution Conv_34 1 1 104 105 0=48 1=3 4=1 5=1 6=27648
PixelShuffle DepthToSpace_35 1 1 105 106 0=4
Interp Resize_37 1 1 input.1_splitncnn_0 111 0=1 1=4.000000e+00 2=4.000000e+00
BinaryOp Add_38 2 1 106 111 112
Interp Resize_40 1 1 112 output 0=3 1=7.500000e-01 2=7.500000e-01
Binary file not shown.
@@ -0,0 +1,42 @@
7767517
40 41
Input input.1 0 1 data
Split splitncnn_input0 1 2 data input.1_splitncnn_0 input.1_splitncnn_1
Convolution Conv_0 1 1 input.1_splitncnn_1 54 0=64 1=3 4=1 5=1 6=1728
PReLU PRelu_1 1 1 54 56 0=64
Convolution Conv_2 1 1 56 57 0=64 1=3 4=1 5=1 6=36864
PReLU PRelu_3 1 1 57 59 0=64
Convolution Conv_4 1 1 59 60 0=64 1=3 4=1 5=1 6=36864
PReLU PRelu_5 1 1 60 62 0=64
Convolution Conv_6 1 1 62 63 0=64 1=3 4=1 5=1 6=36864
PReLU PRelu_7 1 1 63 65 0=64
Convolution Conv_8 1 1 65 66 0=64 1=3 4=1 5=1 6=36864
PReLU PRelu_9 1 1 66 68 0=64
Convolution Conv_10 1 1 68 69 0=64 1=3 4=1 5=1 6=36864
PReLU PRelu_11 1 1 69 71 0=64
Convolution Conv_12 1 1 71 72 0=64 1=3 4=1 5=1 6=36864
PReLU PRelu_13 1 1 72 74 0=64
Convolution Conv_14 1 1 74 75 0=64 1=3 4=1 5=1 6=36864
PReLU PRelu_15 1 1 75 77 0=64
Convolution Conv_16 1 1 77 78 0=64 1=3 4=1 5=1 6=36864
PReLU PRelu_17 1 1 78 80 0=64
Convolution Conv_18 1 1 80 81 0=64 1=3 4=1 5=1 6=36864
PReLU PRelu_19 1 1 81 83 0=64
Convolution Conv_20 1 1 83 84 0=64 1=3 4=1 5=1 6=36864
PReLU PRelu_21 1 1 84 86 0=64
Convolution Conv_22 1 1 86 87 0=64 1=3 4=1 5=1 6=36864
PReLU PRelu_23 1 1 87 89 0=64
Convolution Conv_24 1 1 89 90 0=64 1=3 4=1 5=1 6=36864
PReLU PRelu_25 1 1 90 92 0=64
Convolution Conv_26 1 1 92 93 0=64 1=3 4=1 5=1 6=36864
PReLU PRelu_27 1 1 93 95 0=64
Convolution Conv_28 1 1 95 96 0=64 1=3 4=1 5=1 6=36864
PReLU PRelu_29 1 1 96 98 0=64
Convolution Conv_30 1 1 98 99 0=64 1=3 4=1 5=1 6=36864
PReLU PRelu_31 1 1 99 101 0=64
Convolution Conv_32 1 1 101 102 0=64 1=3 4=1 5=1 6=36864
PReLU PRelu_33 1 1 102 104 0=64
Convolution Conv_34 1 1 104 105 0=48 1=3 4=1 5=1 6=27648
PixelShuffle DepthToSpace_35 1 1 105 106 0=4
Interp Resize_37 1 1 input.1_splitncnn_0 111 0=1 1=4.000000e+00 2=4.000000e+00
BinaryOp Add_38 2 1 106 111 output
Binary file not shown.
@@ -0,0 +1,270 @@
7767517
268 473
Input input.1 0 1 data
Convolution Conv_0 1 1 data 193 0=64 1=3 4=1 5=1 6=1728
Split splitncnn_0 1 8 193 193_splitncnn_0 193_splitncnn_1 193_splitncnn_2 193_splitncnn_3 193_splitncnn_4 193_splitncnn_5 193_splitncnn_6 193_splitncnn_7
Convolution Conv_1 1 1 193_splitncnn_7 195 0=32 1=3 4=1 5=1 6=18432 9=2 -23310=1,2.000000e-01
Split splitncnn_1 1 4 195 195_splitncnn_0 195_splitncnn_1 195_splitncnn_2 195_splitncnn_3
Concat Concat_3 2 1 193_splitncnn_6 195_splitncnn_3 196
Convolution Conv_4 1 1 196 198 0=32 1=3 4=1 5=1 6=27648 9=2 -23310=1,2.000000e-01
Split splitncnn_2 1 3 198 198_splitncnn_0 198_splitncnn_1 198_splitncnn_2
Concat Concat_6 3 1 193_splitncnn_5 195_splitncnn_2 198_splitncnn_2 199
Convolution Conv_7 1 1 199 201 0=32 1=3 4=1 5=1 6=36864 9=2 -23310=1,2.000000e-01
Split splitncnn_3 1 2 201 201_splitncnn_0 201_splitncnn_1
Concat Concat_9 4 1 193_splitncnn_4 195_splitncnn_1 198_splitncnn_1 201_splitncnn_1 202
Convolution Conv_10 1 1 202 204 0=32 1=3 4=1 5=1 6=46080 9=2 -23310=1,2.000000e-01
Concat Concat_12 5 1 193_splitncnn_3 195_splitncnn_0 198_splitncnn_0 201_splitncnn_0 204 205
Convolution Conv_13 1 1 205 206 0=64 1=3 4=1 5=1 6=110592
Eltwise Add_16 2 1 206 193_splitncnn_2 209 0=1 -23301=2,2.000000e-01,1.000000e+00
Split splitncnn_4 1 6 209 209_splitncnn_0 209_splitncnn_1 209_splitncnn_2 209_splitncnn_3 209_splitncnn_4 209_splitncnn_5
Convolution Conv_17 1 1 209_splitncnn_5 211 0=32 1=3 4=1 5=1 6=18432 9=2 -23310=1,2.000000e-01
Split splitncnn_5 1 4 211 211_splitncnn_0 211_splitncnn_1 211_splitncnn_2 211_splitncnn_3
Concat Concat_19 2 1 209_splitncnn_4 211_splitncnn_3 212
Convolution Conv_20 1 1 212 214 0=32 1=3 4=1 5=1 6=27648 9=2 -23310=1,2.000000e-01
Split splitncnn_6 1 3 214 214_splitncnn_0 214_splitncnn_1 214_splitncnn_2
Concat Concat_22 3 1 209_splitncnn_3 211_splitncnn_2 214_splitncnn_2 215
Convolution Conv_23 1 1 215 217 0=32 1=3 4=1 5=1 6=36864 9=2 -23310=1,2.000000e-01
Split splitncnn_7 1 2 217 217_splitncnn_0 217_splitncnn_1
Concat Concat_25 4 1 209_splitncnn_2 211_splitncnn_1 214_splitncnn_1 217_splitncnn_1 218
Convolution Conv_26 1 1 218 220 0=32 1=3 4=1 5=1 6=46080 9=2 -23310=1,2.000000e-01
Concat Concat_28 5 1 209_splitncnn_1 211_splitncnn_0 214_splitncnn_0 217_splitncnn_0 220 221
Convolution Conv_29 1 1 221 222 0=64 1=3 4=1 5=1 6=110592
Eltwise Add_32 2 1 222 209_splitncnn_0 225 0=1 -23301=2,2.000000e-01,1.000000e+00
Split splitncnn_8 1 6 225 225_splitncnn_0 225_splitncnn_1 225_splitncnn_2 225_splitncnn_3 225_splitncnn_4 225_splitncnn_5
Convolution Conv_33 1 1 225_splitncnn_5 227 0=32 1=3 4=1 5=1 6=18432 9=2 -23310=1,2.000000e-01
Split splitncnn_9 1 4 227 227_splitncnn_0 227_splitncnn_1 227_splitncnn_2 227_splitncnn_3
Concat Concat_35 2 1 225_splitncnn_4 227_splitncnn_3 228
Convolution Conv_36 1 1 228 230 0=32 1=3 4=1 5=1 6=27648 9=2 -23310=1,2.000000e-01
Split splitncnn_10 1 3 230 230_splitncnn_0 230_splitncnn_1 230_splitncnn_2
Concat Concat_38 3 1 225_splitncnn_3 227_splitncnn_2 230_splitncnn_2 231
Convolution Conv_39 1 1 231 233 0=32 1=3 4=1 5=1 6=36864 9=2 -23310=1,2.000000e-01
Split splitncnn_11 1 2 233 233_splitncnn_0 233_splitncnn_1
Concat Concat_41 4 1 225_splitncnn_2 227_splitncnn_1 230_splitncnn_1 233_splitncnn_1 234
Convolution Conv_42 1 1 234 236 0=32 1=3 4=1 5=1 6=46080 9=2 -23310=1,2.000000e-01
Concat Concat_44 5 1 225_splitncnn_1 227_splitncnn_0 230_splitncnn_0 233_splitncnn_0 236 237
Convolution Conv_45 1 1 237 238 0=64 1=3 4=1 5=1 6=110592
Eltwise Add_48 2 1 238 225_splitncnn_0 241 0=1 -23301=2,2.000000e-01,1.000000e+00
Eltwise Add_51 2 1 241 193_splitncnn_1 244 0=1 -23301=2,2.000000e-01,1.000000e+00
Split splitncnn_12 1 7 244 244_splitncnn_0 244_splitncnn_1 244_splitncnn_2 244_splitncnn_3 244_splitncnn_4 244_splitncnn_5 244_splitncnn_6
Convolution Conv_52 1 1 244_splitncnn_6 246 0=32 1=3 4=1 5=1 6=18432 9=2 -23310=1,2.000000e-01
Split splitncnn_13 1 4 246 246_splitncnn_0 246_splitncnn_1 246_splitncnn_2 246_splitncnn_3
Concat Concat_54 2 1 244_splitncnn_5 246_splitncnn_3 247
Convolution Conv_55 1 1 247 249 0=32 1=3 4=1 5=1 6=27648 9=2 -23310=1,2.000000e-01
Split splitncnn_14 1 3 249 249_splitncnn_0 249_splitncnn_1 249_splitncnn_2
Concat Concat_57 3 1 244_splitncnn_4 246_splitncnn_2 249_splitncnn_2 250
Convolution Conv_58 1 1 250 252 0=32 1=3 4=1 5=1 6=36864 9=2 -23310=1,2.000000e-01
Split splitncnn_15 1 2 252 252_splitncnn_0 252_splitncnn_1
Concat Concat_60 4 1 244_splitncnn_3 246_splitncnn_1 249_splitncnn_1 252_splitncnn_1 253
Convolution Conv_61 1 1 253 255 0=32 1=3 4=1 5=1 6=46080 9=2 -23310=1,2.000000e-01
Concat Concat_63 5 1 244_splitncnn_2 246_splitncnn_0 249_splitncnn_0 252_splitncnn_0 255 256
Convolution Conv_64 1 1 256 257 0=64 1=3 4=1 5=1 6=110592
Eltwise Add_67 2 1 257 244_splitncnn_1 260 0=1 -23301=2,2.000000e-01,1.000000e+00
Split splitncnn_16 1 6 260 260_splitncnn_0 260_splitncnn_1 260_splitncnn_2 260_splitncnn_3 260_splitncnn_4 260_splitncnn_5
Convolution Conv_68 1 1 260_splitncnn_5 262 0=32 1=3 4=1 5=1 6=18432 9=2 -23310=1,2.000000e-01
Split splitncnn_17 1 4 262 262_splitncnn_0 262_splitncnn_1 262_splitncnn_2 262_splitncnn_3
Concat Concat_70 2 1 260_splitncnn_4 262_splitncnn_3 263
Convolution Conv_71 1 1 263 265 0=32 1=3 4=1 5=1 6=27648 9=2 -23310=1,2.000000e-01
Split splitncnn_18 1 3 265 265_splitncnn_0 265_splitncnn_1 265_splitncnn_2
Concat Concat_73 3 1 260_splitncnn_3 262_splitncnn_2 265_splitncnn_2 266
Convolution Conv_74 1 1 266 268 0=32 1=3 4=1 5=1 6=36864 9=2 -23310=1,2.000000e-01
Split splitncnn_19 1 2 268 268_splitncnn_0 268_splitncnn_1
Concat Concat_76 4 1 260_splitncnn_2 262_splitncnn_1 265_splitncnn_1 268_splitncnn_1 269
Convolution Conv_77 1 1 269 271 0=32 1=3 4=1 5=1 6=46080 9=2 -23310=1,2.000000e-01
Concat Concat_79 5 1 260_splitncnn_1 262_splitncnn_0 265_splitncnn_0 268_splitncnn_0 271 272
Convolution Conv_80 1 1 272 273 0=64 1=3 4=1 5=1 6=110592
Eltwise Add_83 2 1 273 260_splitncnn_0 276 0=1 -23301=2,2.000000e-01,1.000000e+00
Split splitncnn_20 1 6 276 276_splitncnn_0 276_splitncnn_1 276_splitncnn_2 276_splitncnn_3 276_splitncnn_4 276_splitncnn_5
Convolution Conv_84 1 1 276_splitncnn_5 278 0=32 1=3 4=1 5=1 6=18432 9=2 -23310=1,2.000000e-01
Split splitncnn_21 1 4 278 278_splitncnn_0 278_splitncnn_1 278_splitncnn_2 278_splitncnn_3
Concat Concat_86 2 1 276_splitncnn_4 278_splitncnn_3 279
Convolution Conv_87 1 1 279 281 0=32 1=3 4=1 5=1 6=27648 9=2 -23310=1,2.000000e-01
Split splitncnn_22 1 3 281 281_splitncnn_0 281_splitncnn_1 281_splitncnn_2
Concat Concat_89 3 1 276_splitncnn_3 278_splitncnn_2 281_splitncnn_2 282
Convolution Conv_90 1 1 282 284 0=32 1=3 4=1 5=1 6=36864 9=2 -23310=1,2.000000e-01
Split splitncnn_23 1 2 284 284_splitncnn_0 284_splitncnn_1
Concat Concat_92 4 1 276_splitncnn_2 278_splitncnn_1 281_splitncnn_1 284_splitncnn_1 285
Convolution Conv_93 1 1 285 287 0=32 1=3 4=1 5=1 6=46080 9=2 -23310=1,2.000000e-01
Concat Concat_95 5 1 276_splitncnn_1 278_splitncnn_0 281_splitncnn_0 284_splitncnn_0 287 288
Convolution Conv_96 1 1 288 289 0=64 1=3 4=1 5=1 6=110592
Eltwise Add_99 2 1 289 276_splitncnn_0 292 0=1 -23301=2,2.000000e-01,1.000000e+00
Eltwise Add_102 2 1 292 244_splitncnn_0 295 0=1 -23301=2,2.000000e-01,1.000000e+00
Split splitncnn_24 1 7 295 295_splitncnn_0 295_splitncnn_1 295_splitncnn_2 295_splitncnn_3 295_splitncnn_4 295_splitncnn_5 295_splitncnn_6
Convolution Conv_103 1 1 295_splitncnn_6 297 0=32 1=3 4=1 5=1 6=18432 9=2 -23310=1,2.000000e-01
Split splitncnn_25 1 4 297 297_splitncnn_0 297_splitncnn_1 297_splitncnn_2 297_splitncnn_3
Concat Concat_105 2 1 295_splitncnn_5 297_splitncnn_3 298
Convolution Conv_106 1 1 298 300 0=32 1=3 4=1 5=1 6=27648 9=2 -23310=1,2.000000e-01
Split splitncnn_26 1 3 300 300_splitncnn_0 300_splitncnn_1 300_splitncnn_2
Concat Concat_108 3 1 295_splitncnn_4 297_splitncnn_2 300_splitncnn_2 301
Convolution Conv_109 1 1 301 303 0=32 1=3 4=1 5=1 6=36864 9=2 -23310=1,2.000000e-01
Split splitncnn_27 1 2 303 303_splitncnn_0 303_splitncnn_1
Concat Concat_111 4 1 295_splitncnn_3 297_splitncnn_1 300_splitncnn_1 303_splitncnn_1 304
Convolution Conv_112 1 1 304 306 0=32 1=3 4=1 5=1 6=46080 9=2 -23310=1,2.000000e-01
Concat Concat_114 5 1 295_splitncnn_2 297_splitncnn_0 300_splitncnn_0 303_splitncnn_0 306 307
Convolution Conv_115 1 1 307 308 0=64 1=3 4=1 5=1 6=110592
Eltwise Add_118 2 1 308 295_splitncnn_1 311 0=1 -23301=2,2.000000e-01,1.000000e+00
Split splitncnn_28 1 6 311 311_splitncnn_0 311_splitncnn_1 311_splitncnn_2 311_splitncnn_3 311_splitncnn_4 311_splitncnn_5
Convolution Conv_119 1 1 311_splitncnn_5 313 0=32 1=3 4=1 5=1 6=18432 9=2 -23310=1,2.000000e-01
Split splitncnn_29 1 4 313 313_splitncnn_0 313_splitncnn_1 313_splitncnn_2 313_splitncnn_3
Concat Concat_121 2 1 311_splitncnn_4 313_splitncnn_3 314
Convolution Conv_122 1 1 314 316 0=32 1=3 4=1 5=1 6=27648 9=2 -23310=1,2.000000e-01
Split splitncnn_30 1 3 316 316_splitncnn_0 316_splitncnn_1 316_splitncnn_2
Concat Concat_124 3 1 311_splitncnn_3 313_splitncnn_2 316_splitncnn_2 317
Convolution Conv_125 1 1 317 319 0=32 1=3 4=1 5=1 6=36864 9=2 -23310=1,2.000000e-01
Split splitncnn_31 1 2 319 319_splitncnn_0 319_splitncnn_1
Concat Concat_127 4 1 311_splitncnn_2 313_splitncnn_1 316_splitncnn_1 319_splitncnn_1 320
Convolution Conv_128 1 1 320 322 0=32 1=3 4=1 5=1 6=46080 9=2 -23310=1,2.000000e-01
Concat Concat_130 5 1 311_splitncnn_1 313_splitncnn_0 316_splitncnn_0 319_splitncnn_0 322 323
Convolution Conv_131 1 1 323 324 0=64 1=3 4=1 5=1 6=110592
Eltwise Add_134 2 1 324 311_splitncnn_0 327 0=1 -23301=2,2.000000e-01,1.000000e+00
Split splitncnn_32 1 6 327 327_splitncnn_0 327_splitncnn_1 327_splitncnn_2 327_splitncnn_3 327_splitncnn_4 327_splitncnn_5
Convolution Conv_135 1 1 327_splitncnn_5 329 0=32 1=3 4=1 5=1 6=18432 9=2 -23310=1,2.000000e-01
Split splitncnn_33 1 4 329 329_splitncnn_0 329_splitncnn_1 329_splitncnn_2 329_splitncnn_3
Concat Concat_137 2 1 327_splitncnn_4 329_splitncnn_3 330
Convolution Conv_138 1 1 330 332 0=32 1=3 4=1 5=1 6=27648 9=2 -23310=1,2.000000e-01
Split splitncnn_34 1 3 332 332_splitncnn_0 332_splitncnn_1 332_splitncnn_2
Concat Concat_140 3 1 327_splitncnn_3 329_splitncnn_2 332_splitncnn_2 333
Convolution Conv_141 1 1 333 335 0=32 1=3 4=1 5=1 6=36864 9=2 -23310=1,2.000000e-01
Split splitncnn_35 1 2 335 335_splitncnn_0 335_splitncnn_1
Concat Concat_143 4 1 327_splitncnn_2 329_splitncnn_1 332_splitncnn_1 335_splitncnn_1 336
Convolution Conv_144 1 1 336 338 0=32 1=3 4=1 5=1 6=46080 9=2 -23310=1,2.000000e-01
Concat Concat_146 5 1 327_splitncnn_1 329_splitncnn_0 332_splitncnn_0 335_splitncnn_0 338 339
Convolution Conv_147 1 1 339 340 0=64 1=3 4=1 5=1 6=110592
Eltwise Add_150 2 1 340 327_splitncnn_0 343 0=1 -23301=2,2.000000e-01,1.000000e+00
Eltwise Add_153 2 1 343 295_splitncnn_0 346 0=1 -23301=2,2.000000e-01,1.000000e+00
Split splitncnn_36 1 7 346 346_splitncnn_0 346_splitncnn_1 346_splitncnn_2 346_splitncnn_3 346_splitncnn_4 346_splitncnn_5 346_splitncnn_6
Convolution Conv_154 1 1 346_splitncnn_6 348 0=32 1=3 4=1 5=1 6=18432 9=2 -23310=1,2.000000e-01
Split splitncnn_37 1 4 348 348_splitncnn_0 348_splitncnn_1 348_splitncnn_2 348_splitncnn_3
Concat Concat_156 2 1 346_splitncnn_5 348_splitncnn_3 349
Convolution Conv_157 1 1 349 351 0=32 1=3 4=1 5=1 6=27648 9=2 -23310=1,2.000000e-01
Split splitncnn_38 1 3 351 351_splitncnn_0 351_splitncnn_1 351_splitncnn_2
Concat Concat_159 3 1 346_splitncnn_4 348_splitncnn_2 351_splitncnn_2 352
Convolution Conv_160 1 1 352 354 0=32 1=3 4=1 5=1 6=36864 9=2 -23310=1,2.000000e-01
Split splitncnn_39 1 2 354 354_splitncnn_0 354_splitncnn_1
Concat Concat_162 4 1 346_splitncnn_3 348_splitncnn_1 351_splitncnn_1 354_splitncnn_1 355
Convolution Conv_163 1 1 355 357 0=32 1=3 4=1 5=1 6=46080 9=2 -23310=1,2.000000e-01
Concat Concat_165 5 1 346_splitncnn_2 348_splitncnn_0 351_splitncnn_0 354_splitncnn_0 357 358
Convolution Conv_166 1 1 358 359 0=64 1=3 4=1 5=1 6=110592
Eltwise Add_169 2 1 359 346_splitncnn_1 362 0=1 -23301=2,2.000000e-01,1.000000e+00
Split splitncnn_40 1 6 362 362_splitncnn_0 362_splitncnn_1 362_splitncnn_2 362_splitncnn_3 362_splitncnn_4 362_splitncnn_5
Convolution Conv_170 1 1 362_splitncnn_5 364 0=32 1=3 4=1 5=1 6=18432 9=2 -23310=1,2.000000e-01
Split splitncnn_41 1 4 364 364_splitncnn_0 364_splitncnn_1 364_splitncnn_2 364_splitncnn_3
Concat Concat_172 2 1 362_splitncnn_4 364_splitncnn_3 365
Convolution Conv_173 1 1 365 367 0=32 1=3 4=1 5=1 6=27648 9=2 -23310=1,2.000000e-01
Split splitncnn_42 1 3 367 367_splitncnn_0 367_splitncnn_1 367_splitncnn_2
Concat Concat_175 3 1 362_splitncnn_3 364_splitncnn_2 367_splitncnn_2 368
Convolution Conv_176 1 1 368 370 0=32 1=3 4=1 5=1 6=36864 9=2 -23310=1,2.000000e-01
Split splitncnn_43 1 2 370 370_splitncnn_0 370_splitncnn_1
Concat Concat_178 4 1 362_splitncnn_2 364_splitncnn_1 367_splitncnn_1 370_splitncnn_1 371
Convolution Conv_179 1 1 371 373 0=32 1=3 4=1 5=1 6=46080 9=2 -23310=1,2.000000e-01
Concat Concat_181 5 1 362_splitncnn_1 364_splitncnn_0 367_splitncnn_0 370_splitncnn_0 373 374
Convolution Conv_182 1 1 374 375 0=64 1=3 4=1 5=1 6=110592
Eltwise Add_185 2 1 375 362_splitncnn_0 378 0=1 -23301=2,2.000000e-01,1.000000e+00
Split splitncnn_44 1 6 378 378_splitncnn_0 378_splitncnn_1 378_splitncnn_2 378_splitncnn_3 378_splitncnn_4 378_splitncnn_5
Convolution Conv_186 1 1 378_splitncnn_5 380 0=32 1=3 4=1 5=1 6=18432 9=2 -23310=1,2.000000e-01
Split splitncnn_45 1 4 380 380_splitncnn_0 380_splitncnn_1 380_splitncnn_2 380_splitncnn_3
Concat Concat_188 2 1 378_splitncnn_4 380_splitncnn_3 381
Convolution Conv_189 1 1 381 383 0=32 1=3 4=1 5=1 6=27648 9=2 -23310=1,2.000000e-01
Split splitncnn_46 1 3 383 383_splitncnn_0 383_splitncnn_1 383_splitncnn_2
Concat Concat_191 3 1 378_splitncnn_3 380_splitncnn_2 383_splitncnn_2 384
Convolution Conv_192 1 1 384 386 0=32 1=3 4=1 5=1 6=36864 9=2 -23310=1,2.000000e-01
Split splitncnn_47 1 2 386 386_splitncnn_0 386_splitncnn_1
Concat Concat_194 4 1 378_splitncnn_2 380_splitncnn_1 383_splitncnn_1 386_splitncnn_1 387
Convolution Conv_195 1 1 387 389 0=32 1=3 4=1 5=1 6=46080 9=2 -23310=1,2.000000e-01
Concat Concat_197 5 1 378_splitncnn_1 380_splitncnn_0 383_splitncnn_0 386_splitncnn_0 389 390
Convolution Conv_198 1 1 390 391 0=64 1=3 4=1 5=1 6=110592
Eltwise Add_201 2 1 391 378_splitncnn_0 394 0=1 -23301=2,2.000000e-01,1.000000e+00
Eltwise Add_204 2 1 394 346_splitncnn_0 397 0=1 -23301=2,2.000000e-01,1.000000e+00
Split splitncnn_48 1 7 397 397_splitncnn_0 397_splitncnn_1 397_splitncnn_2 397_splitncnn_3 397_splitncnn_4 397_splitncnn_5 397_splitncnn_6
Convolution Conv_205 1 1 397_splitncnn_6 399 0=32 1=3 4=1 5=1 6=18432 9=2 -23310=1,2.000000e-01
Split splitncnn_49 1 4 399 399_splitncnn_0 399_splitncnn_1 399_splitncnn_2 399_splitncnn_3
Concat Concat_207 2 1 397_splitncnn_5 399_splitncnn_3 400
Convolution Conv_208 1 1 400 402 0=32 1=3 4=1 5=1 6=27648 9=2 -23310=1,2.000000e-01
Split splitncnn_50 1 3 402 402_splitncnn_0 402_splitncnn_1 402_splitncnn_2
Concat Concat_210 3 1 397_splitncnn_4 399_splitncnn_2 402_splitncnn_2 403
Convolution Conv_211 1 1 403 405 0=32 1=3 4=1 5=1 6=36864 9=2 -23310=1,2.000000e-01
Split splitncnn_51 1 2 405 405_splitncnn_0 405_splitncnn_1
Concat Concat_213 4 1 397_splitncnn_3 399_splitncnn_1 402_splitncnn_1 405_splitncnn_1 406
Convolution Conv_214 1 1 406 408 0=32 1=3 4=1 5=1 6=46080 9=2 -23310=1,2.000000e-01
Concat Concat_216 5 1 397_splitncnn_2 399_splitncnn_0 402_splitncnn_0 405_splitncnn_0 408 409
Convolution Conv_217 1 1 409 410 0=64 1=3 4=1 5=1 6=110592
Eltwise Add_220 2 1 410 397_splitncnn_1 413 0=1 -23301=2,2.000000e-01,1.000000e+00
Split splitncnn_52 1 6 413 413_splitncnn_0 413_splitncnn_1 413_splitncnn_2 413_splitncnn_3 413_splitncnn_4 413_splitncnn_5
Convolution Conv_221 1 1 413_splitncnn_5 415 0=32 1=3 4=1 5=1 6=18432 9=2 -23310=1,2.000000e-01
Split splitncnn_53 1 4 415 415_splitncnn_0 415_splitncnn_1 415_splitncnn_2 415_splitncnn_3
Concat Concat_223 2 1 413_splitncnn_4 415_splitncnn_3 416
Convolution Conv_224 1 1 416 418 0=32 1=3 4=1 5=1 6=27648 9=2 -23310=1,2.000000e-01
Split splitncnn_54 1 3 418 418_splitncnn_0 418_splitncnn_1 418_splitncnn_2
Concat Concat_226 3 1 413_splitncnn_3 415_splitncnn_2 418_splitncnn_2 419
Convolution Conv_227 1 1 419 421 0=32 1=3 4=1 5=1 6=36864 9=2 -23310=1,2.000000e-01
Split splitncnn_55 1 2 421 421_splitncnn_0 421_splitncnn_1
Concat Concat_229 4 1 413_splitncnn_2 415_splitncnn_1 418_splitncnn_1 421_splitncnn_1 422
Convolution Conv_230 1 1 422 424 0=32 1=3 4=1 5=1 6=46080 9=2 -23310=1,2.000000e-01
Concat Concat_232 5 1 413_splitncnn_1 415_splitncnn_0 418_splitncnn_0 421_splitncnn_0 424 425
Convolution Conv_233 1 1 425 426 0=64 1=3 4=1 5=1 6=110592
Eltwise Add_236 2 1 426 413_splitncnn_0 429 0=1 -23301=2,2.000000e-01,1.000000e+00
Split splitncnn_56 1 6 429 429_splitncnn_0 429_splitncnn_1 429_splitncnn_2 429_splitncnn_3 429_splitncnn_4 429_splitncnn_5
Convolution Conv_237 1 1 429_splitncnn_5 431 0=32 1=3 4=1 5=1 6=18432 9=2 -23310=1,2.000000e-01
Split splitncnn_57 1 4 431 431_splitncnn_0 431_splitncnn_1 431_splitncnn_2 431_splitncnn_3
Concat Concat_239 2 1 429_splitncnn_4 431_splitncnn_3 432
Convolution Conv_240 1 1 432 434 0=32 1=3 4=1 5=1 6=27648 9=2 -23310=1,2.000000e-01
Split splitncnn_58 1 3 434 434_splitncnn_0 434_splitncnn_1 434_splitncnn_2
Concat Concat_242 3 1 429_splitncnn_3 431_splitncnn_2 434_splitncnn_2 435
Convolution Conv_243 1 1 435 437 0=32 1=3 4=1 5=1 6=36864 9=2 -23310=1,2.000000e-01
Split splitncnn_59 1 2 437 437_splitncnn_0 437_splitncnn_1
Concat Concat_245 4 1 429_splitncnn_2 431_splitncnn_1 434_splitncnn_1 437_splitncnn_1 438
Convolution Conv_246 1 1 438 440 0=32 1=3 4=1 5=1 6=46080 9=2 -23310=1,2.000000e-01
Concat Concat_248 5 1 429_splitncnn_1 431_splitncnn_0 434_splitncnn_0 437_splitncnn_0 440 441
Convolution Conv_249 1 1 441 442 0=64 1=3 4=1 5=1 6=110592
Eltwise Add_252 2 1 442 429_splitncnn_0 445 0=1 -23301=2,2.000000e-01,1.000000e+00
Eltwise Add_255 2 1 445 397_splitncnn_0 448 0=1 -23301=2,2.000000e-01,1.000000e+00
Split splitncnn_60 1 7 448 448_splitncnn_0 448_splitncnn_1 448_splitncnn_2 448_splitncnn_3 448_splitncnn_4 448_splitncnn_5 448_splitncnn_6
Convolution Conv_256 1 1 448_splitncnn_6 450 0=32 1=3 4=1 5=1 6=18432 9=2 -23310=1,2.000000e-01
Split splitncnn_61 1 4 450 450_splitncnn_0 450_splitncnn_1 450_splitncnn_2 450_splitncnn_3
Concat Concat_258 2 1 448_splitncnn_5 450_splitncnn_3 451
Convolution Conv_259 1 1 451 453 0=32 1=3 4=1 5=1 6=27648 9=2 -23310=1,2.000000e-01
Split splitncnn_62 1 3 453 453_splitncnn_0 453_splitncnn_1 453_splitncnn_2
Concat Concat_261 3 1 448_splitncnn_4 450_splitncnn_2 453_splitncnn_2 454
Convolution Conv_262 1 1 454 456 0=32 1=3 4=1 5=1 6=36864 9=2 -23310=1,2.000000e-01
Split splitncnn_63 1 2 456 456_splitncnn_0 456_splitncnn_1
Concat Concat_264 4 1 448_splitncnn_3 450_splitncnn_1 453_splitncnn_1 456_splitncnn_1 457
Convolution Conv_265 1 1 457 459 0=32 1=3 4=1 5=1 6=46080 9=2 -23310=1,2.000000e-01
Concat Concat_267 5 1 448_splitncnn_2 450_splitncnn_0 453_splitncnn_0 456_splitncnn_0 459 460
Convolution Conv_268 1 1 460 461 0=64 1=3 4=1 5=1 6=110592
Eltwise Add_271 2 1 461 448_splitncnn_1 464 0=1 -23301=2,2.000000e-01,1.000000e+00
Split splitncnn_64 1 6 464 464_splitncnn_0 464_splitncnn_1 464_splitncnn_2 464_splitncnn_3 464_splitncnn_4 464_splitncnn_5
Convolution Conv_272 1 1 464_splitncnn_5 466 0=32 1=3 4=1 5=1 6=18432 9=2 -23310=1,2.000000e-01
Split splitncnn_65 1 4 466 466_splitncnn_0 466_splitncnn_1 466_splitncnn_2 466_splitncnn_3
Concat Concat_274 2 1 464_splitncnn_4 466_splitncnn_3 467
Convolution Conv_275 1 1 467 469 0=32 1=3 4=1 5=1 6=27648 9=2 -23310=1,2.000000e-01
Split splitncnn_66 1 3 469 469_splitncnn_0 469_splitncnn_1 469_splitncnn_2
Concat Concat_277 3 1 464_splitncnn_3 466_splitncnn_2 469_splitncnn_2 470
Convolution Conv_278 1 1 470 472 0=32 1=3 4=1 5=1 6=36864 9=2 -23310=1,2.000000e-01
Split splitncnn_67 1 2 472 472_splitncnn_0 472_splitncnn_1
Concat Concat_280 4 1 464_splitncnn_2 466_splitncnn_1 469_splitncnn_1 472_splitncnn_1 473
Convolution Conv_281 1 1 473 475 0=32 1=3 4=1 5=1 6=46080 9=2 -23310=1,2.000000e-01
Concat Concat_283 5 1 464_splitncnn_1 466_splitncnn_0 469_splitncnn_0 472_splitncnn_0 475 476
Convolution Conv_284 1 1 476 477 0=64 1=3 4=1 5=1 6=110592
Eltwise Add_287 2 1 477 464_splitncnn_0 480 0=1 -23301=2,2.000000e-01,1.000000e+00
Split splitncnn_68 1 6 480 480_splitncnn_0 480_splitncnn_1 480_splitncnn_2 480_splitncnn_3 480_splitncnn_4 480_splitncnn_5
Convolution Conv_288 1 1 480_splitncnn_5 482 0=32 1=3 4=1 5=1 6=18432 9=2 -23310=1,2.000000e-01
Split splitncnn_69 1 4 482 482_splitncnn_0 482_splitncnn_1 482_splitncnn_2 482_splitncnn_3
Concat Concat_290 2 1 480_splitncnn_4 482_splitncnn_3 483
Convolution Conv_291 1 1 483 485 0=32 1=3 4=1 5=1 6=27648 9=2 -23310=1,2.000000e-01
Split splitncnn_70 1 3 485 485_splitncnn_0 485_splitncnn_1 485_splitncnn_2
Concat Concat_293 3 1 480_splitncnn_3 482_splitncnn_2 485_splitncnn_2 486
Convolution Conv_294 1 1 486 488 0=32 1=3 4=1 5=1 6=36864 9=2 -23310=1,2.000000e-01
Split splitncnn_71 1 2 488 488_splitncnn_0 488_splitncnn_1
Concat Concat_296 4 1 480_splitncnn_2 482_splitncnn_1 485_splitncnn_1 488_splitncnn_1 489
Convolution Conv_297 1 1 489 491 0=32 1=3 4=1 5=1 6=46080 9=2 -23310=1,2.000000e-01
Concat Concat_299 5 1 480_splitncnn_1 482_splitncnn_0 485_splitncnn_0 488_splitncnn_0 491 492
Convolution Conv_300 1 1 492 493 0=64 1=3 4=1 5=1 6=110592
Eltwise Add_303 2 1 493 480_splitncnn_0 496 0=1 -23301=2,2.000000e-01,1.000000e+00
Eltwise Add_306 2 1 496 448_splitncnn_0 499 0=1 -23301=2,2.000000e-01,1.000000e+00
Convolution Conv_307 1 1 499 500 0=64 1=3 4=1 5=1 6=36864
BinaryOp Add_308 2 1 193_splitncnn_0 500 501
Interp Resize_310 1 1 501 506 0=1 1=2.000000e+00 2=2.000000e+00
Convolution Conv_311 1 1 506 508 0=64 1=3 4=1 5=1 6=36864 9=2 -23310=1,2.000000e-01
Interp Resize_314 1 1 508 513 0=1 1=2.000000e+00 2=2.000000e+00
Convolution Conv_315 1 1 513 515 0=64 1=3 4=1 5=1 6=36864 9=2 -23310=1,2.000000e-01
Convolution Conv_317 1 1 515 517 0=64 1=3 4=1 5=1 6=36864 9=2 -23310=1,2.000000e-01
Convolution Conv_319 1 1 517 output 0=3 1=3 4=1 5=1 6=1728
Binary file not shown.
File diff suppressed because it is too large Load Diff
Binary file not shown.
Binary file not shown.
+9
View File
@@ -0,0 +1,9 @@
# AI Video Upscaler — Python Dependencies
# Install: pip install -r requirements.txt
fastapi>=0.115.0
uvicorn>=0.30.0
python-multipart>=0.0.9
websockets>=13.0
pydantic>=2.0
torch>=2.0
Executable
+40
View File
@@ -0,0 +1,40 @@
#!/usr/bin/env python3
"""
restart.py — Restart the AI Video Upscaler web application.
Usage:
python restart.py # Restart on default port 8000
python restart.py --port 9000 # Restart on a custom port
"""
import argparse
import os
import sys
import time
# Import siblings
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, BASE_DIR)
from stop import stop
from start import start
def restart(host: str, port: int, open_browser: bool = True) -> None:
stop()
# Brief pause to let the port fully release
time.sleep(1)
start(host, port, open_browser=open_browser)
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Restart the AI Video Upscaler")
parser.add_argument("--host", default="0.0.0.0",
help="Bind address (default: 0.0.0.0 to bind all interfaces, use 127.0.0.1 for local loopback only)")
parser.add_argument("--port", type=int, default=8000,
help="Port to listen on (default: 8000)")
parser.add_argument("--no-browser", action="store_true",
help="Don't open the browser automatically")
args = parser.parse_args()
restart(args.host, args.port, open_browser=not args.no_browser)
Executable
+113
View File
@@ -0,0 +1,113 @@
#!/usr/bin/env bash
# ── setup.sh — AI Video Upscaler environment setup ──────────────────────────
#
# Usage:
# chmod +x setup.sh && ./setup.sh
#
# Automates first-time setup on a new system:
# • Verifies required system tools (python3, ffmpeg, ffprobe)
# • Warns if Vulkan is unavailable
# • Creates a Python virtual environment and installs dependencies
# • Makes the Real-ESRGAN binary executable
# • Creates working directories (uploads, outputs, temp)
# ─────────────────────────────────────────────────────────────────────────────
set -euo pipefail
# ── Colors & Helpers ─────────────────────────────────────────────────────────
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
BOLD='\033[1m'
RESET='\033[0m'
info() { echo -e "${CYAN}ℹ $*${RESET}"; }
ok() { echo -e "${GREEN}✅ $*${RESET}"; }
warn() { echo -e "${YELLOW}⚠ $*${RESET}"; }
fail() { echo -e "${RED}❌ $*${RESET}"; exit 1; }
step() { echo -e "\n${BOLD}── $* ──${RESET}"; }
# ── Banner ───────────────────────────────────────────────────────────────────
echo ""
echo -e "${BOLD}🔧 AI Video Upscaler — Setup${RESET}"
echo ""
# ── Script Directory ─────────────────────────────────────────────────────────
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# ── 1. Check: python3 ───────────────────────────────────────────────────────
step "Checking dependencies"
if ! command -v python3 &>/dev/null; then
fail "python3 is not installed.\n Install it with your package manager (e.g. apt install python3, dnf install python3)."
fi
ok "python3 found — $(python3 --version)"
# ── 2. Check: ffmpeg ────────────────────────────────────────────────────────
if ! command -v ffmpeg &>/dev/null; then
fail "ffmpeg is not installed.\n Install it with your package manager:\n • Debian/Ubuntu : sudo apt install ffmpeg\n • Fedora/RHEL : sudo dnf install ffmpeg\n • Arch : sudo pacman -S ffmpeg"
fi
ok "ffmpeg found — $(ffmpeg -version | head -1)"
# ── 3. Check: ffprobe ───────────────────────────────────────────────────────
if ! command -v ffprobe &>/dev/null; then
fail "ffprobe is not installed (usually bundled with ffmpeg).\n Reinstall ffmpeg to get it."
fi
ok "ffprobe found"
# ── 4. Check: vulkaninfo (optional) ─────────────────────────────────────────
if ! command -v vulkaninfo &>/dev/null; then
warn "vulkaninfo not found — Vulkan GPU acceleration is recommended but not required."
warn "Install vulkan-tools for GPU-accelerated upscaling."
else
ok "vulkaninfo found — Vulkan support detected"
fi
# ── 5. Create Python venv ───────────────────────────────────────────────────
step "Setting up Python virtual environment"
if [ -d "$SCRIPT_DIR/venv" ]; then
info "venv already exists at $SCRIPT_DIR/venv — skipping creation."
else
info "Creating venv at $SCRIPT_DIR/venv …"
python3 -m venv "$SCRIPT_DIR/venv"
ok "Virtual environment created"
fi
# ── 6. Install pip dependencies ─────────────────────────────────────────────
step "Installing Python dependencies"
info "Running pip install -r requirements.txt …"
"$SCRIPT_DIR/venv/bin/pip" install --upgrade pip --quiet
"$SCRIPT_DIR/venv/bin/pip" install -r "$SCRIPT_DIR/requirements.txt"
ok "All Python dependencies installed"
# ── 7. Make Real-ESRGAN binary executable ────────────────────────────────────
step "Configuring Real-ESRGAN binary"
if [ -f "$SCRIPT_DIR/realesrgan-bin/realesrgan-ncnn-vulkan" ]; then
chmod +x "$SCRIPT_DIR/realesrgan-bin/realesrgan-ncnn-vulkan"
ok "realesrgan-ncnn-vulkan marked as executable"
else
warn "realesrgan-ncnn-vulkan binary not found at $SCRIPT_DIR/realesrgan-bin/"
warn "You may need to download it manually."
fi
# ── 8. Create working directories ───────────────────────────────────────────
step "Creating working directories"
for dir in uploads outputs temp; do
mkdir -p "$SCRIPT_DIR/$dir"
done
ok "Directories ready: uploads/ outputs/ temp/"
# ── Done ─────────────────────────────────────────────────────────────────────
echo ""
echo -e "${GREEN}${BOLD}────────────────────────────────────────────${RESET}"
echo -e "${GREEN}${BOLD} ✅ Setup complete!${RESET}"
echo -e "${GREEN}${BOLD}────────────────────────────────────────────${RESET}"
echo ""
echo -e " Launch the server with:"
echo -e " ${CYAN}python start.py${RESET}"
echo ""
Executable
+156
View File
@@ -0,0 +1,156 @@
#!/usr/bin/env python3
"""
start.py — Start the AI Video Upscaler web application.
Usage:
python start.py # Start on default port 8000 (binds to 0.0.0.0 for LAN access)
python start.py --port 9000 # Start on a custom port
python start.py --host 127.0.0.1 # Bind to loopback only (no LAN access)
"""
import argparse
import os
import signal
import socket
import subprocess
import sys
import time
import webbrowser
# ── Paths ────────────────────────────────────────────────────────────────────
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
PID_FILE = os.path.join(BASE_DIR, ".uvicorn.pid")
LOG_FILE = os.path.join(BASE_DIR, "server.log")
# Detect the right Python interpreter:
# 1. If running inside an activated venv, use that interpreter
# 2. Otherwise look for venv/ then .venv/ in the project
if sys.prefix != sys.base_prefix:
# Already inside an activated venv — use it
VENV_PYTHON = sys.executable
else:
VENV_PYTHON = os.path.join(BASE_DIR, "venv", "bin", "python")
if not os.path.isfile(VENV_PYTHON):
VENV_PYTHON = os.path.join(BASE_DIR, ".venv", "bin", "python")
if not os.path.isfile(VENV_PYTHON):
VENV_PYTHON = sys.executable # last resort: system python
def _is_running(pid: int) -> bool:
"""Check whether a process with the given PID is still alive and is our server."""
try:
os.kill(pid, 0)
except OSError:
return False
# Check /proc/{pid}/cmdline to verify it's our server process
try:
cmdline_path = f"/proc/{pid}/cmdline"
if os.path.exists(cmdline_path):
with open(cmdline_path, "r") as f:
cmdline = f.read()
# Check for indicators of our server
return "uvicorn" in cmdline or "app.main" in cmdline
except Exception:
pass
# If we can't read cmdline, default to True (safer fallback)
return True
def _read_pid() -> int | None:
"""Read and validate the PID from the pidfile."""
if not os.path.isfile(PID_FILE):
return None
try:
pid = int(open(PID_FILE).read().strip())
return pid if _is_running(pid) else None
except (ValueError, OSError):
return None
def _get_local_ip() -> str:
"""Get the primary local network IP address."""
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
try:
# Dummy connection to google DNS to get local route IP
s.connect(("8.8.8.8", 80))
ip = s.getsockname()[0]
except Exception:
try:
ip = socket.gethostbyname(socket.gethostname())
except Exception:
ip = "127.0.0.1"
finally:
s.close()
return ip
def start(host: str, port: int, open_browser: bool = True) -> None:
existing = _read_pid()
if existing:
print(f"⚠ Server is already running (PID {existing}).")
if host == "0.0.0.0":
local_ip = _get_local_ip()
print(f" → Local: http://localhost:{port}")
if local_ip and local_ip != "127.0.0.1":
print(f" → Network: http://{local_ip}:{port}")
else:
print(f" → http://{host}:{port}")
return
print(f"🚀 Starting AI Video Upscaler …")
print(f" Host : {host}")
print(f" Port : {port}")
print(f" Log : {LOG_FILE}")
log_fh = open(LOG_FILE, "a")
proc = subprocess.Popen(
[
VENV_PYTHON, "-m", "uvicorn",
"app.main:app",
"--host", host,
"--port", str(port),
],
cwd=BASE_DIR,
stdout=log_fh,
stderr=log_fh,
start_new_session=True, # detach from terminal
)
# Write PID
with open(PID_FILE, "w") as f:
f.write(str(proc.pid))
# Wait briefly to make sure it didn't crash immediately
time.sleep(1.5)
if proc.poll() is not None:
print("❌ Server failed to start. Check server.log for details.")
os.remove(PID_FILE)
sys.exit(1)
url = f"http://{'localhost' if host == '0.0.0.0' else host}:{port}"
print(f"✅ Server started (PID {proc.pid})")
if host == "0.0.0.0":
local_ip = _get_local_ip()
print(f" → Local: http://localhost:{port}")
if local_ip and local_ip != "127.0.0.1":
print(f" → Network: http://{local_ip}:{port}")
else:
print(f" → {url}")
if open_browser:
webbrowser.open(url)
# ── CLI ──────────────────────────────────────────────────────────────────────
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Start the AI Video Upscaler")
parser.add_argument("--host", default="0.0.0.0",
help="Bind address (default: 0.0.0.0 to bind all interfaces, use 127.0.0.1 for local loopback only)")
parser.add_argument("--port", type=int, default=8000,
help="Port to listen on (default: 8000)")
parser.add_argument("--no-browser", action="store_true",
help="Don't open the browser automatically")
args = parser.parse_args()
start(args.host, args.port, open_browser=not args.no_browser)
+1483
View File
File diff suppressed because it is too large Load Diff
+576
View File
@@ -0,0 +1,576 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Antigravity AI Video Upscaler</title>
<link rel="stylesheet" href="styles.css">
<!-- FontAwesome for modern icons -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
</head>
<body>
<div class="app-container">
<!-- Dashboard Header -->
<header>
<div class="logo-container">
<div class="logo-icon">▲</div>
<div class="logo-text">
<h1>ANTIGRAVITY // UPSCALE</h1>
<p>Open Source AI Video Enhancer</p>
</div>
</div>
<div class="header-actions">
<div class="theme-selector">
<label for="theme-select"><i class="fa-solid fa-palette"></i> Theme</label>
<select id="theme-select">
<option value="cyberpunk" selected>Cyberpunk Neon</option>
<option value="oled">OLED Obsidian</option>
<option value="slate">Slate Gray</option>
</select>
</div>
<div class="gpu-badge">
<div class="gpu-dot"></div>
<span id="gpu-info">GPU Active: Quadro P2000 (Vulkan)</span>
</div>
</div>
</header>
<main>
<!-- Step 1: Upload Video -->
<div id="step-upload" class="step-container active">
<div class="glass-card">
<div id="drop-zone" class="upload-area">
<input type="file" id="file-input" accept="video/*" style="display: none;">
<span class="upload-icon"><i class="fa-solid fa-cloud-arrow-up"></i></span>
<h3>Drag & Drop Video File</h3>
<p>Supports MP4, MKV, AVI, MOV, WEBM (Max 500MB recommended for local upscaling)</p>
<button type="button" class="btn" onclick="document.getElementById('file-input').click()">
<i class="fa-solid fa-folder-open"></i> Browse Files
</button>
</div>
</div>
</div>
<!-- Step 2: Configuration & Preview -->
<div id="step-config" class="step-container">
<div class="config-layout">
<!-- Config Sidebar -->
<div class="config-sidebar">
<!-- Video Metadata Display -->
<div class="glass-card meta-panel">
<h4>Video Source Info</h4>
<div class="meta-grid">
<div class="meta-item">
<span>File Name</span>
<span id="meta-name" class="text-truncate">-</span>
</div>
<div class="meta-item">
<span>Size</span>
<span id="meta-size">-</span>
</div>
</div>
<!-- Detailed Streams Accordion -->
<div class="streams-accordions">
<div class="accordion-item">
<div class="accordion-header" onclick="this.parentElement.classList.toggle('open')">
<span><i class="fa-solid fa-video"></i> Video Stream #0</span>
<i class="fa-solid fa-chevron-down toggle-icon"></i>
</div>
<div class="accordion-content">
<div class="secondary-grid">
<div class="meta-item">
<span>Codec</span>
<span id="meta-res-codec">-</span>
</div>
<div class="meta-item">
<span>Resolution</span>
<span id="meta-res">-</span>
</div>
<div class="meta-item">
<span>Frame Rate</span>
<span id="meta-fps">-</span>
</div>
<div class="meta-item">
<span>Duration</span>
<span id="meta-duration">-</span>
</div>
<div class="meta-item">
<span>Total Frames</span>
<span id="meta-total-frames">-</span>
</div>
<div class="meta-item">
<span>Aspect Ratio</span>
<span id="meta-aspect-ratio">-</span>
</div>
</div>
</div>
</div>
<div class="accordion-item">
<div class="accordion-header" onclick="this.parentElement.classList.toggle('open')">
<span><i class="fa-solid fa-volume-high"></i> Audio Stream #1</span>
<i class="fa-solid fa-chevron-down toggle-icon"></i>
</div>
<div class="accordion-content">
<div class="secondary-grid">
<div class="meta-item">
<span>Codec</span>
<span id="meta-audio-codec">-</span>
</div>
<div class="meta-item">
<span>Channels</span>
<span id="meta-audio-channels">-</span>
</div>
<div class="meta-item">
<span>Sample Rate</span>
<span id="meta-audio-samplerate">-</span>
</div>
<div class="meta-item">
<span>Bitrate</span>
<span id="meta-audio-bitrate">-</span>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Trimming Controls Panel -->
<div class="glass-card trimming-panel">
<h4 style="font-family: var(--font-heading); color: var(--primary); font-size: 1.1rem; margin-bottom: 0.5rem;"><i class="fa-solid fa-scissors"></i> Video Trimming</h4>
<p style="font-size: 0.75rem; color: var(--text-muted); margin-bottom: 0.75rem;">Specify range to upscale only a portion of the video.</p>
<div class="form-group">
<label for="trim-start-range">Trim Start: <span id="trim-start-value" class="badge">0.00s</span></label>
<div class="range-slider">
<input type="range" id="trim-start-range" min="0" max="100" step="0.05" value="0">
</div>
</div>
<div class="form-group" style="margin-top: 1rem;">
<label for="trim-end-range">Trim End: <span id="trim-end-value" class="badge">10.00s</span></label>
<div class="range-slider">
<input type="range" id="trim-end-range" min="0" max="100" step="0.05" value="10">
</div>
</div>
</div>
<!-- Config Parameters Form -->
<div class="glass-card settings-panel">
<h4 style="font-family: var(--font-heading); color: var(--primary);">Upscaling Profile</h4>
<!-- Model Selection -->
<div class="form-group">
<label for="model-select">AI Upscaling Model</label>
<select id="model-select">
<option value="realesr-animevideov3">Real-ESRGAN Anime Video V3 (Fast / Anime & Video)</option>
<option value="realesrgan-x4plus" selected>Real-ESRGAN X4 Plus (Ultra Quality / Real World)</option>
<option value="realesrgan-x4plus-anime">Real-ESRGAN X4 Plus Anime (Ultra Quality / Illustration)</option>
<option value="realesrnet-x4plus">Real-ESRNet X4 Plus (Alternate / Real World)</option>
</select>
</div>
<!-- Scale Factor -->
<div class="form-group">
<label for="scale-range">Scale Factor <span id="scale-value" class="badge">4x</span></label>
<div class="range-slider">
<input type="range" id="scale-range" min="2" max="4" step="1" value="4">
</div>
</div>
<!-- Advanced Settings Toggle Header -->
<div class="advanced-settings-header" onclick="this.classList.toggle('open'); document.getElementById('advanced-settings-content').classList.toggle('open')">
<span><i class="fa-solid fa-sliders"></i> Advanced Options</span>
<i class="fa-solid fa-chevron-down"></i>
</div>
<!-- Collapsible Content -->
<div id="advanced-settings-content" class="advanced-settings-content">
<!-- GPU Device Selection -->
<div class="form-group">
<label for="gpu-select">Vulkan GPU Selector</label>
<select id="gpu-select">
<option value="auto" selected>Auto Detect Device</option>
<option value="0">GPU Device #0</option>
<option value="1">GPU Device #1</option>
<option value="2">GPU Device #2</option>
</select>
</div>
<!-- GPU Tile Size -->
<div class="form-group">
<label for="tile-range">GPU Tile Size <span id="tile-value" class="badge">256</span></label>
<div class="range-slider">
<input type="range" id="tile-range" min="64" max="512" step="64" value="256">
</div>
<p style="font-size: 0.72rem; color: var(--text-muted); margin-top: 2px;">
Halves automatically in case of Vulkan VRAM allocation faults.
</p>
</div>
<!-- CRF Quality Slider -->
<div class="form-group">
<label for="crf-range">Constant Rate Factor (CRF) <span id="crf-value" class="badge">18</span></label>
<div class="range-slider">
<input type="range" id="crf-range" min="0" max="51" step="1" value="18">
</div>
<p style="font-size: 0.72rem; color: var(--text-muted); margin-top: 2px;">
0 (lossless) to 51 (worst). Default 18 (visually pristine).
</p>
</div>
<!-- Speed/Quality Preset -->
<div class="form-group">
<label for="preset-select">FFmpeg H.264 Encoder Preset</label>
<select id="preset-select">
<option value="ultrafast">Ultrafast (High Speed / Large Size)</option>
<option value="superfast">Superfast</option>
<option value="veryfast">Veryfast</option>
<option value="faster">Faster</option>
<option value="fast">Fast</option>
<option value="medium" selected>Medium (Balanced)</option>
<option value="slow">Slow (Max H.264 Quality Compression)</option>
<option value="slower">Slower</option>
<option value="veryslow">Veryslow (Maximum Compression)</option>
</select>
</div>
<!-- Output Video container -->
<div class="form-group">
<label for="transcode-select">Output Container Format</label>
<select id="transcode-select">
<option value="mp4" selected>MP4 (MPEG-4 H.264 Wrapper)</option>
<option value="mkv">MKV (Matroska Video Container)</option>
<option value="mov">MOV (QuickTime Video Wrapper)</option>
<option value="avi">AVI (Legacy Audio Video Interleave)</option>
<option value="webm">WebM (VP9 / Web Native HTML5)</option>
</select>
</div>
<!-- Pre-filters selection checkboxes -->
<div class="filter-group">
<span style="font-weight: 600; font-size: 0.85rem; display: block; color: var(--primary);">Pre-Filters & Enhancing</span>
<div class="filter-checkboxes">
<label class="checkbox-container">
<input type="checkbox" id="filter-denoise"> Denoise (hqdn3d)
</label>
<label class="checkbox-container">
<input type="checkbox" id="filter-sharpen"> Sharpen (unsharp)
</label>
<label class="checkbox-container">
<input type="checkbox" id="filter-fps"> Double FPS (60FPS Interpolate)
</label>
</div>
</div>
<!-- Webhook Notify Input -->
<div class="form-group">
<label for="webhook-url">Webhook Notification Callback</label>
<input type="text" id="webhook-url" placeholder="https://your-server.com/webhook" style="background: rgba(255, 255, 255, 0.04); border: 1px solid var(--border-color); color: var(--text-main); padding: 0.8rem 1rem; border-radius: 10px; font-family: var(--font-body); font-size: 0.95rem; outline: none; transition: border 0.3s; width: 100%;">
</div>
<!-- Custom Model Upload Input -->
<div class="form-group">
<label>Upload Custom Model (.param + .bin)</label>
<div class="model-upload-zone" onclick="document.getElementById('custom-model-input').click()">
<input type="file" id="custom-model-input" accept=".param,.bin" multiple style="display: none;">
<i class="fa-solid fa-cubes-stacked"></i>
<p>Select both model files to load into realesrgan</p>
<span id="model-upload-filename" style="font-size: 0.72rem; color: var(--primary); font-weight: bold; word-break: break-all;">No files selected</span>
</div>
<div class="upload-model-btn-container" style="display: none;" id="upload-model-btn-wrapper">
<button type="button" class="btn btn-secondary btn-small" id="upload-model-btn" style="width: 100%;">
<i class="fa-solid fa-cloud-arrow-up"></i> Upload Model Files
</button>
</div>
</div>
<!-- Preserve Audio Toggle -->
<div class="toggle-container">
<div>
<span style="font-weight: 600; font-size: 0.9rem; display: block;">Preserve Audio</span>
<span style="font-size: 0.75rem; color: var(--text-muted);">Copy audio streams into upscaled video</span>
</div>
<label class="switch">
<input type="checkbox" id="audio-toggle" checked>
<span class="slider"></span>
</label>
</div>
</div>
<!-- Action -->
<button type="button" id="start-upscale-btn" class="btn" style="margin-top: 1rem;">
<i class="fa-solid fa-play"></i> Start Full Upscale
</button>
</div>
</div>
<!-- Comparison Preview Area -->
<div class="glass-card preview-panel">
<div class="preview-header">
<h3>Clarity Preview</h3>
<button type="button" id="prev-btn" class="btn btn-secondary" onclick="window.location.reload()">
<i class="fa-solid fa-arrow-left"></i> Change Video
</button>
</div>
<!-- Timeline Selection -->
<div class="timeline-picker">
<div style="display: flex; justify-content: space-between; align-items: center; width: 100%; margin-bottom: 0.25rem;">
<label for="timeline-slider" style="margin: 0; padding-bottom: 0;">
Select Frame Position: <span id="timeline-time" style="color: var(--primary); font-weight: bold;">0.00s</span>
</label>
<div style="display: flex; gap: 0.5rem;">
<button type="button" id="generate-preview-btn" class="btn btn-secondary btn-small" style="padding: 0.4rem 1rem !important;">
<i class="fa-solid fa-wand-magic-sparkles"></i> Frame Preview
</button>
<button type="button" id="generate-video-preview-btn" class="btn btn-primary btn-small" style="padding: 0.4rem 1rem !important;">
<i class="fa-solid fa-video"></i> 5s Video Preview
</button>
</div>
</div>
<input type="range" id="timeline-slider" min="0" max="100" step="0.1" value="10">
</div>
<!-- Interactive Comparison Container -->
<div style="position: relative;">
<!-- Placeholder -->
<div id="preview-placeholder-box" class="preview-placeholder">
<i class="fa-solid fa-image-portrait"></i>
<h4>Clarity Preview Frame</h4>
<p>Extracts a single frame at the chosen timestamp and runs the AI model to show side-by-side comparative clarity enhancement.</p>
<p style="font-size: 0.82rem; color: var(--text-muted); margin-top: 0.5rem;">Click "Render Preview Frame" above to generate the comparative view.</p>
</div>
<!-- Interactive Slider (Hidden by default) -->
<div id="comparison-slider-container" class="comparison-wrapper" style="display: none;">
<img id="img-original" class="comparison-image original" src="" alt="Before Upscaling" style="--zoom: 1; --pan-x: 0px; --pan-y: 0px;">
<div id="img-upscaled-wrapper" class="upscaled-wrapper">
<img id="img-upscaled" class="comparison-image upscaled" src="" alt="After Upscaling" style="--zoom: 1; --pan-x: 0px; --pan-y: 0px;">
</div>
<!-- Video Elements for Synced Video Preview -->
<video id="video-preview-original" class="comparison-image original" loop muted playsinline style="display: none; --zoom: 1; --pan-x: 0px; --pan-y: 0px;"></video>
<div id="video-preview-upscaled-wrapper" class="upscaled-wrapper" style="display: none;">
<video id="video-preview-upscaled" class="comparison-image upscaled" loop muted playsinline style="--zoom: 1; --pan-x: 0px; --pan-y: 0px;"></video>
</div>
<div id="slider-split" class="slider-bar">
<div class="slider-handle"></div>
</div>
<span class="badge-orig">Before (Low Res)</span>
<span class="badge-up">After (AI Upscaled)</span>
</div>
<!-- Zoom & Pan Controls -->
<div id="zoom-pan-controls" class="zoom-pan-bar" style="display: none;">
<div class="zoom-control-item">
<label for="zoom-slider">Zoom: <span id="zoom-val">1.0x</span></label>
<input type="range" id="zoom-slider" min="1" max="5" step="0.1" value="1">
</div>
<div class="zoom-control-item">
<label for="pan-x-slider">Pan X: <span id="pan-x-val">0px</span></label>
<input type="range" id="pan-x-slider" min="-300" max="300" step="1" value="0">
</div>
<div class="zoom-control-item">
<label for="pan-y-slider">Pan Y: <span id="pan-y-val">0px</span></label>
<input type="range" id="pan-y-slider" min="-300" max="300" step="1" value="0">
</div>
<button type="button" id="reset-zoom-pan-btn" class="btn btn-secondary btn-small">
<i class="fa-solid fa-arrows-to-eye"></i> Reset
</button>
</div>
<!-- Video Preview Playback Controls -->
<div id="video-preview-controls" class="zoom-pan-bar" style="display: none; justify-content: center; align-items: center; gap: 1rem; margin-top: 1rem;">
<button type="button" id="vprep-play-btn" class="btn btn-primary btn-circular">
<i class="fa-solid fa-play"></i>
</button>
<div class="time-scrubber-container">
<span id="vprep-time-current">0:00</span>
<input type="range" id="vprep-scrubber" min="0" max="5" step="0.05" value="0">
<span id="vprep-time-duration">0:05</span>
</div>
</div>
<!-- Loader overlay -->
<div id="preview-loader" class="preview-loading">
<div class="spinner" style="margin-bottom: 1rem;"></div>
<h4 id="preview-loader-title" style="font-family: var(--font-heading); margin-bottom: 0.5rem;">Orchestrating Vulkan GPU...</h4>
<div id="preview-loader-progress-container" style="width: 100%; max-width: 250px; height: 6px; background: rgba(255,255,255,0.1); border-radius: 3px; overflow: hidden; margin: 0.5rem 0; display: none;">
<div id="preview-loader-progress-bar" style="width: 0%; height: 100%; background: var(--primary); transition: width 0.3s ease;"></div>
</div>
<p id="preview-loader-text" style="color: var(--text-muted); font-size: 0.85rem;">Extracting and upscaling frame. Please wait...</p>
</div>
</div>
</div>
</div>
</div>
<!-- Step 3: Processing Progress -->
<div id="step-progress" class="step-container">
<div class="glass-card progress-card">
<div class="progress-header">
<div>
<h2 style="font-family: var(--font-heading); margin-bottom: 0.5rem;">Upscaling Video Pipeline</h2>
<p style="color: var(--text-muted); font-size: 0.9rem;">Job ID: <span id="job-id-display" style="font-family: monospace; color: var(--text-main);">-</span></p>
</div>
<span id="status-badge" class="status-badge queued">Queued</span>
</div>
<!-- Progress Bar -->
<div>
<div style="display: flex; justify-content: space-between; font-weight: bold; margin-bottom: 0.5rem; font-size: 0.95rem;">
<span>Progress</span>
<span id="progress-percent" style="color: var(--primary);">0%</span>
</div>
<div class="bar-container">
<div id="progress-bar-fill" class="bar-fill"></div>
</div>
</div>
<!-- Statistics grid -->
<div class="progress-stats">
<div class="stat-card">
<span>Status</span>
<span id="stat-status">Queued</span>
</div>
<div class="stat-card">
<span>Processed Frames</span>
<span id="stat-frames">0 / 0</span>
</div>
<div class="stat-card">
<span>Estimated ETA</span>
<span id="stat-eta" style="color: var(--primary);">Calculating...</span>
</div>
</div>
<!-- Realtime Logs Panel -->
<div>
<h4 style="font-family: var(--font-heading); margin-bottom: 0.5rem; color: var(--text-muted); font-size: 0.9rem;">Pipeline Live Activity Logs</h4>
<div id="log-terminal" class="log-panel">
<div class="log-line">Waiting for worker thread...</div>
</div>
</div>
<!-- Action control -->
<div style="display: flex; justify-content: flex-end;">
<button type="button" id="cancel-upscale-btn" class="btn btn-danger">
<i class="fa-solid fa-circle-stop"></i> Abort Upscaling Job
</button>
</div>
</div>
</div>
<!-- Step 4: Finished/Completed -->
<div id="step-finished" class="step-container">
<div class="glass-card finished-card">
<div class="success-icon">
<i class="fa-solid fa-circle-check"></i>
</div>
<h2>Video Enhancement Completed!</h2>
<p style="color: var(--text-muted); max-width: 500px; margin: 0 auto;">
Your video was successfully upscaled and optimized using the Real-ESRGAN Vulkan backend model. Original audio streams have been merged.
</p>
<div class="action-buttons">
<a id="download-link" href="#" class="btn">
<i class="fa-solid fa-download"></i> Download Upscaled Video
</a>
<button type="button" id="finished-play-comparison-btn" class="btn btn-secondary">
<i class="fa-solid fa-film"></i> Compare Playback
</button>
<button type="button" class="btn btn-secondary" onclick="window.location.reload()">
<i class="fa-solid fa-rotate-left"></i> Process Another File
</button>
</div>
</div>
</div>
<!-- Active Jobs & Queue Section -->
<section class="queue-section glass-card" style="margin-top: 3rem;">
<div class="queue-header" style="display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid var(--border-color); padding-bottom: 0.75rem; margin-bottom: 1rem;">
<h2><i class="fa-solid fa-list-check"></i> Active Jobs & Pipeline Queue</h2>
<button type="button" id="refresh-queue-btn" class="btn btn-secondary btn-small">
<i class="fa-solid fa-arrows-rotate"></i> Refresh Queue
</button>
</div>
<p style="color: var(--text-muted); font-size: 0.9rem; margin-bottom: 1.5rem;">Monitor running and queued jobs in the queue pipeline. Preview jobs are marked as well.</p>
<div id="queue-list" class="queue-list" style="display: flex; flex-direction: column; gap: 0.75rem;">
<!-- Dynamically populated queue items -->
<div class="queue-empty-state" style="text-align: center; padding: 2rem; border: 1px dashed var(--border-color); border-radius: 12px; color: var(--text-muted);">
<i class="fa-solid fa-list-ul" style="font-size: 2rem; margin-bottom: 0.5rem; color: var(--primary);"></i>
<p>No active or queued jobs at the moment.</p>
</div>
</div>
</section>
<!-- Completed Video Gallery -->
<section class="gallery-section glass-card" style="margin-top: 3rem;">
<div class="gallery-header">
<h2><i class="fa-solid fa-photo-film"></i> Completed Enhancements</h2>
<div style="display: flex; gap: 0.5rem;">
<button type="button" id="refresh-gallery-btn" class="btn btn-secondary btn-small">
<i class="fa-solid fa-arrows-rotate"></i> Refresh
</button>
<button type="button" id="purge-all-btn" class="btn btn-danger btn-small">
<i class="fa-solid fa-trash-can-arrow-up"></i> Purge All Data
</button>
</div>
</div>
<p style="color: var(--text-muted); font-size: 0.9rem; margin-bottom: 1.5rem;">Access all your upscaled videos, play them side-by-side with original sources, or manage the storage.</p>
<div id="gallery-grid" class="gallery-grid">
<!-- Dynamically populated completed items -->
<div class="gallery-empty-state">
<i class="fa-solid fa-photo-film"></i>
<p>No completed jobs found. Start upscaling a video above to see it in the gallery!</p>
</div>
</div>
</section>
</main>
</div>
<!-- Synced Video Playback Modal -->
<div id="video-modal" class="modal-overlay" style="display: none;">
<div class="modal-card">
<div class="modal-header">
<h3><i class="fa-solid fa-film"></i> Side-by-Side Synced Video Comparison</h3>
<button type="button" class="close-btn" id="modal-close-btn">&times;</button>
</div>
<div class="modal-body">
<div class="video-compare-wrapper">
<div class="video-container">
<h4>Original (Before)</h4>
<video id="modal-video-original" playsinline></video>
</div>
<div class="video-container">
<h4>Upscaled (AI Enhanced)</h4>
<video id="modal-video-upscaled" playsinline></video>
</div>
</div>
<!-- Synced Controls -->
<div class="video-sync-controls">
<button type="button" id="modal-play-btn" class="btn btn-circular">
<i class="fa-solid fa-play"></i>
</button>
<div class="time-scrubber-container">
<span id="modal-time-current">00:00</span>
<input type="range" id="modal-time-slider" min="0" max="100" value="0" step="0.01">
<span id="modal-time-duration">00:00</span>
</div>
<button type="button" id="modal-mute-btn" class="btn btn-secondary btn-circular">
<i class="fa-solid fa-volume-high"></i>
</button>
</div>
</div>
</div>
</div>
<!-- Javascript Orchestration -->
<script src="app.js"></script>
</body>
</html>
+1499
View File
File diff suppressed because it is too large Load Diff
Executable
+85
View File
@@ -0,0 +1,85 @@
#!/usr/bin/env python3
"""
stop.py — Stop the AI Video Upscaler web application.
Usage:
python stop.py
"""
import os
import signal
import sys
import time
# ── Paths ────────────────────────────────────────────────────────────────────
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
PID_FILE = os.path.join(BASE_DIR, ".uvicorn.pid")
def _is_running(pid: int) -> bool:
"""Check whether a process with the given PID is still alive and is our server."""
try:
os.kill(pid, 0)
except OSError:
return False
# Check /proc/{pid}/cmdline to verify it's our server process
try:
cmdline_path = f"/proc/{pid}/cmdline"
if os.path.exists(cmdline_path):
with open(cmdline_path, "r") as f:
cmdline = f.read()
# Check for indicators of our server
return "uvicorn" in cmdline or "app.main" in cmdline
except Exception:
pass
# If we can't read cmdline, default to True (safer fallback)
return True
def stop() -> None:
if not os.path.isfile(PID_FILE):
print("ℹ No server appears to be running (no pidfile found).")
return
try:
pid = int(open(PID_FILE).read().strip())
except (ValueError, OSError):
print("⚠ Pidfile exists but is unreadable. Removing it.")
os.remove(PID_FILE)
return
if not _is_running(pid):
print(f"ℹ Server (PID {pid}) is not running. Cleaning up pidfile.")
os.remove(PID_FILE)
return
print(f"🛑 Stopping server (PID {pid}) …")
# Graceful shutdown (SIGTERM)
os.kill(pid, signal.SIGTERM)
# Wait up to 10 seconds for graceful exit
for i in range(20):
time.sleep(0.5)
if not _is_running(pid):
break
else:
# Force kill if still alive
print(f" ⚠ Server didn't stop gracefully, sending SIGKILL …")
os.kill(pid, signal.SIGKILL)
time.sleep(0.5)
# Clean up pidfile
if os.path.isfile(PID_FILE):
os.remove(PID_FILE)
if _is_running(pid):
print(f"❌ Failed to stop server (PID {pid}).")
sys.exit(1)
else:
print(f"✅ Server stopped.")
if __name__ == "__main__":
stop()