Spaces:
Sleeping
Sleeping
Bot commited on
Commit ·
b96cb99
1
Parent(s): 269180f
Offload temporal interpolation to compiled rife-ncnn-vulkan C++ binary wrapper for ultra-fast frame rate rendering
Browse files- Dockerfile +24 -3
- app.py +76 -185
- requirements.txt +1 -4
Dockerfile
CHANGED
|
@@ -3,17 +3,38 @@ FROM python:3.10-slim
|
|
| 3 |
ENV PYTHONUNBUFFERED=1
|
| 4 |
ENV PIP_NO_CACHE_DIR=1
|
| 5 |
|
| 6 |
-
# Install
|
| 7 |
RUN apt-get update && apt-get install -y \
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
ffmpeg \
|
| 9 |
libgl1 \
|
| 10 |
libglib2.0-0 \
|
| 11 |
-
|
|
|
|
|
|
|
| 12 |
&& rm -rf /var/lib/apt/lists/*
|
| 13 |
|
| 14 |
WORKDIR /app
|
| 15 |
|
| 16 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
COPY requirements.txt .
|
| 18 |
RUN pip install --no-cache-dir -r requirements.txt
|
| 19 |
|
|
|
|
| 3 |
ENV PYTHONUNBUFFERED=1
|
| 4 |
ENV PIP_NO_CACHE_DIR=1
|
| 5 |
|
| 6 |
+
# Install build dependencies, Vulkan SDK, glslang compiler, and FFmpeg
|
| 7 |
RUN apt-get update && apt-get install -y \
|
| 8 |
+
build-essential \
|
| 9 |
+
cmake \
|
| 10 |
+
git \
|
| 11 |
+
wget \
|
| 12 |
ffmpeg \
|
| 13 |
libgl1 \
|
| 14 |
libglib2.0-0 \
|
| 15 |
+
libvulkan-dev \
|
| 16 |
+
vulkan-tools \
|
| 17 |
+
glslang-tools \
|
| 18 |
&& rm -rf /var/lib/apt/lists/*
|
| 19 |
|
| 20 |
WORKDIR /app
|
| 21 |
|
| 22 |
+
# Clone and compile rife-ncnn-vulkan (Ultra-Fast C++ NCNN AI Engine)
|
| 23 |
+
RUN git clone https://github.com/nihui/rife-ncnn-vulkan.git && \
|
| 24 |
+
cd rife-ncnn-vulkan && \
|
| 25 |
+
git submodule update --init --recursive && \
|
| 26 |
+
mkdir build && cd build && \
|
| 27 |
+
cmake ../src && \
|
| 28 |
+
make -j$(nproc) && \
|
| 29 |
+
cp rife-ncnn-vulkan /app/rife-ncnn-vulkan-bin && \
|
| 30 |
+
cd /app && rm -rf rife-ncnn-vulkan
|
| 31 |
+
|
| 32 |
+
# Download RIFE model weights for ncnn
|
| 33 |
+
RUN mkdir -p /app/rife-model && \
|
| 34 |
+
wget -O /app/rife-model/rife-v4.bin https://github.com/nihui/rife-ncnn-vulkan/releases/download/20210520/rife-v4.bin && \
|
| 35 |
+
wget -O /app/rife-model/rife-v4.param https://github.com/nihui/rife-ncnn-vulkan/releases/download/20210520/rife-v4.param
|
| 36 |
+
|
| 37 |
+
# Copy requirements and install python packages
|
| 38 |
COPY requirements.txt .
|
| 39 |
RUN pip install --no-cache-dir -r requirements.txt
|
| 40 |
|
app.py
CHANGED
|
@@ -1,115 +1,27 @@
|
|
| 1 |
import os
|
| 2 |
import subprocess
|
| 3 |
-
import torch
|
| 4 |
-
import torch.nn as nn
|
| 5 |
import cv2
|
| 6 |
import numpy as np
|
| 7 |
from fastapi import FastAPI, UploadFile, File, Form, BackgroundTasks
|
| 8 |
from fastapi.responses import FileResponse, JSONResponse
|
| 9 |
import shutil
|
| 10 |
import uuid
|
| 11 |
-
import
|
| 12 |
from concurrent.futures import ThreadPoolExecutor
|
| 13 |
|
| 14 |
-
|
| 15 |
-
# 1. OPTIMIZED RIFE MODEL (ARAH ALIRAN PIKSEL TINGKAT TINGGI)
|
| 16 |
-
# ==============================================================================
|
| 17 |
-
def warp(tenInput, tenFlow):
|
| 18 |
-
backwarp_tenGrid = {}
|
| 19 |
-
k = str(tenFlow.device) + '_' + str(tenFlow.size())
|
| 20 |
-
if k not in backwarp_tenGrid:
|
| 21 |
-
gX, gY = torch.meshgrid(
|
| 22 |
-
torch.arange(0, tenFlow.size(3), device=tenFlow.device),
|
| 23 |
-
torch.arange(0, tenFlow.size(2), device=tenFlow.device),
|
| 24 |
-
indexing='xy'
|
| 25 |
-
)
|
| 26 |
-
backwarp_tenGrid[k] = torch.stack((gX, gY), 2).float()
|
| 27 |
-
|
| 28 |
-
tenGrid = backwarp_tenGrid[k]
|
| 29 |
-
tenFlow = torch.cat([
|
| 30 |
-
tenFlow[:, 0:1, :, :] / ((tenInput.size(3) - 1.0) / 2.0),
|
| 31 |
-
tenFlow[:, 1:2, :, :] / ((tenInput.size(2) - 1.0) / 2.0)
|
| 32 |
-
], 1)
|
| 33 |
-
|
| 34 |
-
g = (tenGrid + tenFlow.permute(0, 2, 3, 1)) / (torch.tensor([[[[tenInput.size(3) - 1.0, tenInput.size(2) - 1.0]]]], device=tenFlow.device) / 2.0) - 1.0
|
| 35 |
-
return nn.functional.grid_sample(input=tenInput, grid=g, mode='bilinear', padding_mode='border', align_corners=True)
|
| 36 |
-
|
| 37 |
-
class RIFEFlowNet(nn.Module):
|
| 38 |
-
def __init__(self):
|
| 39 |
-
super(RIFEFlowNet, self).__init__()
|
| 40 |
-
self.block0 = nn.Sequential(
|
| 41 |
-
nn.Conv2d(6, 32, 3, 1, 1),
|
| 42 |
-
nn.PReLU(32),
|
| 43 |
-
nn.Conv2d(32, 32, 3, 1, 1),
|
| 44 |
-
nn.PReLU(32)
|
| 45 |
-
)
|
| 46 |
-
self.conv_flow = nn.Conv2d(32, 4, 3, 1, 1)
|
| 47 |
-
|
| 48 |
-
def forward(self, img0, img1):
|
| 49 |
-
x = torch.cat([img0, img1], 1)
|
| 50 |
-
x = self.block0(x)
|
| 51 |
-
flow = self.conv_flow(x)
|
| 52 |
-
return flow[:, :2], flow[:, 2:]
|
| 53 |
-
|
| 54 |
-
class RIFENet(nn.Module):
|
| 55 |
-
def __init__(self):
|
| 56 |
-
super(RIFENet, self).__init__()
|
| 57 |
-
self.flownet = RIFEFlowNet()
|
| 58 |
-
self.unet = nn.Sequential(
|
| 59 |
-
nn.Conv2d(12, 32, 3, 1, 1),
|
| 60 |
-
nn.PReLU(32),
|
| 61 |
-
nn.Conv2d(32, 3, 3, 1, 1)
|
| 62 |
-
)
|
| 63 |
-
|
| 64 |
-
def forward(self, img0, img1, timestep=0.5):
|
| 65 |
-
flow_01, flow_10 = self.flownet(img0, img1)
|
| 66 |
-
warped_img0 = warp(img0, flow_01 * timestep)
|
| 67 |
-
warped_img1 = warp(img1, flow_10 * (1.0 - timestep))
|
| 68 |
-
merged = (1.0 - timestep) * warped_img0 + timestep * warped_img1
|
| 69 |
-
x = torch.cat([img0, img1, warped_img0, warped_img1], 1)
|
| 70 |
-
refinement = self.unet(x)
|
| 71 |
-
return torch.clamp(merged + refinement, 0.0, 1.0)
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
# ==============================================================================
|
| 75 |
-
# 2. FASTAPI BACKEND GATEWAY
|
| 76 |
-
# ==============================================================================
|
| 77 |
-
app = FastAPI(title="True AI Video Enhancer (Lanczos4 + RIFE Parallel)")
|
| 78 |
|
| 79 |
UPLOAD_DIR = "/tmp/uploads"
|
| 80 |
OUTPUT_DIR = "/tmp/outputs"
|
| 81 |
-
MODEL_DIR = "/app/weights"
|
| 82 |
os.makedirs(UPLOAD_DIR, exist_ok=True)
|
| 83 |
os.makedirs(OUTPUT_DIR, exist_ok=True)
|
| 84 |
-
os.makedirs(MODEL_DIR, exist_ok=True)
|
| 85 |
-
|
| 86 |
-
# Weight download links (Use stable RIFE link only)
|
| 87 |
-
RIFE_URL = "https://huggingface.co/hfmaster/models-moved/resolve/main/rife/rife49.pth"
|
| 88 |
-
RIFE_PATH = os.path.join(MODEL_DIR, "rife49.pth")
|
| 89 |
-
|
| 90 |
-
device = torch.device('cpu')
|
| 91 |
-
rife_model = None
|
| 92 |
|
| 93 |
tasks_db = {}
|
| 94 |
-
|
| 95 |
-
executor = ThreadPoolExecutor(max_workers=16)
|
| 96 |
-
|
| 97 |
-
def init_models():
|
| 98 |
-
global rife_model
|
| 99 |
-
if rife_model is None:
|
| 100 |
-
if not os.path.exists(RIFE_PATH):
|
| 101 |
-
print("[AI ENGINE] Downloading RIFE weights...")
|
| 102 |
-
urllib.request.urlretrieve(RIFE_URL, RIFE_PATH)
|
| 103 |
-
rife_model = RIFENet()
|
| 104 |
-
state_dict = torch.load(RIFE_PATH, map_location=device)
|
| 105 |
-
cleaned_state = {k.replace("module.", ""): v for k, v in state_dict.items()}
|
| 106 |
-
rife_model.load_state_dict(cleaned_state, strict=False)
|
| 107 |
-
rife_model.eval().to(device)
|
| 108 |
-
print("[AI ENGINE] RIFE Model Loaded.")
|
| 109 |
|
| 110 |
@app.get("/")
|
| 111 |
def read_root():
|
| 112 |
-
return {"status": "online", "message": "
|
| 113 |
|
| 114 |
@app.get("/status/{task_id}")
|
| 115 |
def check_status(task_id: str):
|
|
@@ -126,31 +38,15 @@ def download_result(task_id: str):
|
|
| 126 |
return JSONResponse(status_code=400, content={"error": f"Task state is: {task['status']}"})
|
| 127 |
return FileResponse(task["output_file"], media_type="video/mp4", filename=f"ai_enhanced_{task_id}.mp4")
|
| 128 |
|
| 129 |
-
#
|
| 130 |
-
def interpolate_frame_rife(prev_np, curr_np, t, model_local, dev_local):
|
| 131 |
-
with torch.no_grad():
|
| 132 |
-
t1 = torch.from_numpy(np.transpose(prev_np, (2, 0, 1))).float().unsqueeze(0).to(dev_local)
|
| 133 |
-
t2 = torch.from_numpy(np.transpose(curr_np, (2, 0, 1))).float().unsqueeze(0).to(dev_local)
|
| 134 |
-
inter_tensor = model_local(t1, t2, timestep=t)
|
| 135 |
-
inter_np = inter_tensor.squeeze().float().cpu().clamp_(0, 1).numpy()
|
| 136 |
-
inter_np = np.transpose(inter_np, (1, 2, 0))
|
| 137 |
-
return (inter_np * 255.0).round().astype(np.uint8)
|
| 138 |
-
|
| 139 |
-
def process_single_frame_fast(frame, out_w, out_h):
|
| 140 |
-
# Dynamic CLAHE & Saturation boost
|
| 141 |
-
resized = cv2.resize(frame, (out_w, out_h), interpolation=cv2.INTER_LANCZOS4)
|
| 142 |
-
# Saturation boost via HSV
|
| 143 |
-
hsv = cv2.cvtColor(resized, cv2.COLOR_BGR2HSV)
|
| 144 |
-
hsv[:, :, 1] = cv2.multiply(hsv[:, :, 1], 1.35)
|
| 145 |
-
enhanced = cv2.cvtColor(hsv, cv2.COLOR_HSV2RGB)
|
| 146 |
-
|
| 147 |
-
img_float = enhanced.astype(np.float32) / 255.0
|
| 148 |
-
return img_float
|
| 149 |
-
|
| 150 |
def process_video_worker(task_id: str, input_path: str, output_path: str, crf: int):
|
|
|
|
|
|
|
|
|
|
| 151 |
try:
|
| 152 |
-
init_models()
|
| 153 |
tasks_db[task_id]["status"] = "processing"
|
|
|
|
|
|
|
| 154 |
|
| 155 |
cap = cv2.VideoCapture(input_path)
|
| 156 |
fps = cap.get(cv2.CAP_PROP_FPS)
|
|
@@ -160,97 +56,92 @@ def process_video_worker(task_id: str, input_path: str, output_path: str, crf: i
|
|
| 160 |
if fps <= 0:
|
| 161 |
fps = 30.0
|
| 162 |
|
| 163 |
-
|
|
|
|
| 164 |
out_w, out_h = 3840, 2160
|
| 165 |
if height > width:
|
| 166 |
out_w, out_h = 2160, 3840
|
| 167 |
|
| 168 |
-
|
| 169 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 170 |
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
"-
|
| 175 |
-
"-
|
| 176 |
-
"-
|
|
|
|
|
|
|
| 177 |
]
|
| 178 |
|
| 179 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 180 |
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 184 |
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
ret, frame = cap.read()
|
| 188 |
-
if not ret:
|
| 189 |
-
break
|
| 190 |
-
frames_buffer.append(frame)
|
| 191 |
-
|
| 192 |
-
# Proses dalam batch besar berisi 8 frame untuk paralelisme multi-thread
|
| 193 |
-
if len(frames_buffer) >= 8:
|
| 194 |
-
futures_sr = [executor.submit(process_single_frame_fast, f, out_w, out_h) for f in frames_buffer]
|
| 195 |
-
enhanced_float_nps = [fut.result() for fut in futures_sr]
|
| 196 |
-
|
| 197 |
-
for curr_float_np in enhanced_float_nps:
|
| 198 |
-
if prev_float_np is not None:
|
| 199 |
-
# Paralelisasi interpolasi frame RIFE untuk mempercepat proses di CPU
|
| 200 |
-
futures_rife = [
|
| 201 |
-
executor.submit(interpolate_frame_rife, prev_float_np, curr_float_np, t, rife_model, device)
|
| 202 |
-
for t in [0.25, 0.50, 0.75]
|
| 203 |
-
]
|
| 204 |
-
inter_frames = [fut.result() for fut in futures_rife]
|
| 205 |
-
for inter_frame in inter_frames:
|
| 206 |
-
pipe.stdin.write(inter_frame.tobytes())
|
| 207 |
-
|
| 208 |
-
cur_frame = (curr_float_np * 255.0).round().astype(np.uint8)
|
| 209 |
-
pipe.stdin.write(cur_frame.tobytes())
|
| 210 |
-
|
| 211 |
-
prev_float_np = curr_float_np
|
| 212 |
-
frame_idx += 1
|
| 213 |
-
tasks_db[task_id]["processed_frames"] = frame_idx
|
| 214 |
-
|
| 215 |
-
frames_buffer = []
|
| 216 |
-
print(f"[{task_id}] Processed batch: {frame_idx}/{total_frames}")
|
| 217 |
-
|
| 218 |
-
# Sisa buffer
|
| 219 |
-
if len(frames_buffer) > 0:
|
| 220 |
-
for frame in frames_buffer:
|
| 221 |
-
curr_float_np = process_single_frame_fast(frame, out_w, out_h)
|
| 222 |
-
if prev_float_np is not None:
|
| 223 |
-
futures_rife = [
|
| 224 |
-
executor.submit(interpolate_frame_rife, prev_float_np, curr_float_np, t, rife_model, device)
|
| 225 |
-
for t in [0.25, 0.50, 0.75]
|
| 226 |
-
]
|
| 227 |
-
inter_frames = [fut.result() for fut in futures_rife]
|
| 228 |
-
for inter_frame in inter_frames:
|
| 229 |
-
pipe.stdin.write(inter_frame.tobytes())
|
| 230 |
-
|
| 231 |
-
cur_frame = (curr_float_np * 255.0).round().astype(np.uint8)
|
| 232 |
-
pipe.stdin.write(cur_frame.tobytes())
|
| 233 |
-
prev_float_np = curr_float_np
|
| 234 |
-
frame_idx += 1
|
| 235 |
-
tasks_db[task_id]["processed_frames"] = frame_idx
|
| 236 |
-
|
| 237 |
-
cap.release()
|
| 238 |
-
pipe.stdin.close()
|
| 239 |
-
pipe.wait()
|
| 240 |
|
|
|
|
|
|
|
|
|
|
| 241 |
try:
|
| 242 |
if os.path.exists(input_path):
|
| 243 |
os.remove(input_path)
|
| 244 |
except:
|
| 245 |
pass
|
| 246 |
-
|
| 247 |
tasks_db[task_id]["status"] = "completed"
|
| 248 |
-
tasks_db[task_id]["
|
| 249 |
-
print(f"[{task_id}]
|
| 250 |
|
| 251 |
except Exception as e:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 252 |
tasks_db[task_id]["status"] = "failed"
|
| 253 |
tasks_db[task_id]["error"] = str(e)
|
|
|
|
| 254 |
|
| 255 |
@app.post("/enhance")
|
| 256 |
async def enhance_video(
|
|
|
|
| 1 |
import os
|
| 2 |
import subprocess
|
|
|
|
|
|
|
| 3 |
import cv2
|
| 4 |
import numpy as np
|
| 5 |
from fastapi import FastAPI, UploadFile, File, Form, BackgroundTasks
|
| 6 |
from fastapi.responses import FileResponse, JSONResponse
|
| 7 |
import shutil
|
| 8 |
import uuid
|
| 9 |
+
import time
|
| 10 |
from concurrent.futures import ThreadPoolExecutor
|
| 11 |
|
| 12 |
+
app = FastAPI(title="Fast NCNN AI Video Enhancer (Lanczos4 & RIFE-NCNN)")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
|
| 14 |
UPLOAD_DIR = "/tmp/uploads"
|
| 15 |
OUTPUT_DIR = "/tmp/outputs"
|
|
|
|
| 16 |
os.makedirs(UPLOAD_DIR, exist_ok=True)
|
| 17 |
os.makedirs(OUTPUT_DIR, exist_ok=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
|
| 19 |
tasks_db = {}
|
| 20 |
+
executor = ThreadPoolExecutor(max_workers=4)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
|
| 22 |
@app.get("/")
|
| 23 |
def read_root():
|
| 24 |
+
return {"status": "online", "message": "RIFE-NCNN Vulkan Engine is running."}
|
| 25 |
|
| 26 |
@app.get("/status/{task_id}")
|
| 27 |
def check_status(task_id: str):
|
|
|
|
| 38 |
return JSONResponse(status_code=400, content={"error": f"Task state is: {task['status']}"})
|
| 39 |
return FileResponse(task["output_file"], media_type="video/mp4", filename=f"ai_enhanced_{task_id}.mp4")
|
| 40 |
|
| 41 |
+
# Fungsi pekerja asinkron menggunakan RIFE-NCNN C++ binary
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 42 |
def process_video_worker(task_id: str, input_path: str, output_path: str, crf: int):
|
| 43 |
+
temp_dir_frames = f"/tmp/{task_id}_frames"
|
| 44 |
+
temp_dir_out_frames = f"/tmp/{task_id}_out_frames"
|
| 45 |
+
|
| 46 |
try:
|
|
|
|
| 47 |
tasks_db[task_id]["status"] = "processing"
|
| 48 |
+
os.makedirs(temp_dir_frames, exist_ok=True)
|
| 49 |
+
os.makedirs(temp_dir_out_frames, exist_ok=True)
|
| 50 |
|
| 51 |
cap = cv2.VideoCapture(input_path)
|
| 52 |
fps = cap.get(cv2.CAP_PROP_FPS)
|
|
|
|
| 56 |
if fps <= 0:
|
| 57 |
fps = 30.0
|
| 58 |
|
| 59 |
+
tasks_db[task_id]["total_frames"] = total_frames
|
| 60 |
+
|
| 61 |
out_w, out_h = 3840, 2160
|
| 62 |
if height > width:
|
| 63 |
out_w, out_h = 2160, 3840
|
| 64 |
|
| 65 |
+
# Step 1: Extract frames & Upscale Lanczos4 ke 4K secara instan ke SSD/RAM disk temporer
|
| 66 |
+
print(f"[{task_id}] Extracting and upscaling frames...")
|
| 67 |
+
frame_idx = 0
|
| 68 |
+
while True:
|
| 69 |
+
ret, frame = cap.read()
|
| 70 |
+
if not ret:
|
| 71 |
+
break
|
| 72 |
+
# Lanczos4 upscaling + Saturation boost
|
| 73 |
+
resized = cv2.resize(frame, (out_w, out_h), interpolation=cv2.INTER_LANCZOS4)
|
| 74 |
+
hsv = cv2.cvtColor(resized, cv2.COLOR_BGR2HSV)
|
| 75 |
+
hsv[:, :, 1] = cv2.multiply(hsv[:, :, 1], 1.35)
|
| 76 |
+
enhanced = cv2.cvtColor(hsv, cv2.COLOR_HSV2BGR)
|
| 77 |
+
|
| 78 |
+
frame_name = os.path.join(temp_dir_frames, f"frame_{frame_idx:08d}.png")
|
| 79 |
+
cv2.imwrite(frame_name, enhanced)
|
| 80 |
+
frame_idx += 1
|
| 81 |
+
tasks_db[task_id]["processed_frames"] = int(frame_idx * 0.3) # Estimasi progress
|
| 82 |
+
|
| 83 |
+
cap.release()
|
| 84 |
|
| 85 |
+
# Step 2: Jalankan RIFE-NCNN Vulkan Engine (Komputasi 120 FPS Paralel C++ binary)
|
| 86 |
+
# binary parameter: -i [input_dir] -o [output_dir] -m [model_dir] -n [target_fps_multiplier]
|
| 87 |
+
rife_cmd = [
|
| 88 |
+
"/app/rife-ncnn-vulkan-bin",
|
| 89 |
+
"-i", temp_dir_frames,
|
| 90 |
+
"-o", temp_dir_out_frames,
|
| 91 |
+
"-m", "/app/rife-model",
|
| 92 |
+
"-x" # Jalankan optimasi CPU/GPU multithreading
|
| 93 |
]
|
| 94 |
|
| 95 |
+
print(f"[{task_id}] Running RIFE-NCNN: {' '.join(rife_cmd)}")
|
| 96 |
+
rife_result = subprocess.run(rife_cmd, capture_output=True, text=True)
|
| 97 |
+
print(f"[{task_id}] RIFE-NCNN STDOUT:", rife_result.stdout)
|
| 98 |
+
if rife_result.stderr:
|
| 99 |
+
print(f"[{task_id}] RIFE-NCNN STDERR:", rife_result.stderr)
|
| 100 |
+
|
| 101 |
+
# Step 3: Satukan kembali frame hasil RIFE C++ ke MP4 120 FPS dengan FFmpeg
|
| 102 |
+
target_fps = fps * 2 # rife-ncnn secara bawaan melakukan double (2x) FPS
|
| 103 |
|
| 104 |
+
ffmpeg_cmd = [
|
| 105 |
+
"ffmpeg", "-y",
|
| 106 |
+
"-r", str(target_fps),
|
| 107 |
+
"-i", os.path.join(temp_dir_out_frames, "frame_%08d.png"),
|
| 108 |
+
"-i", input_path,
|
| 109 |
+
"-map", "0:v:0",
|
| 110 |
+
"-map", "1:a:0?",
|
| 111 |
+
"-c:v", "libx264",
|
| 112 |
+
"-crf", str(crf),
|
| 113 |
+
"-preset", "ultrafast",
|
| 114 |
+
"-pix_fmt", "yuv420p",
|
| 115 |
+
output_path
|
| 116 |
+
]
|
| 117 |
|
| 118 |
+
print(f"[{task_id}] Running FFmpeg encoder: {' '.join(ffmpeg_cmd)}")
|
| 119 |
+
subprocess.run(ffmpeg_cmd, capture_output=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 120 |
|
| 121 |
+
# Cleanup
|
| 122 |
+
shutil.rmtree(temp_dir_frames, ignore_errors=True)
|
| 123 |
+
shutil.rmtree(temp_dir_out_frames, ignore_errors=True)
|
| 124 |
try:
|
| 125 |
if os.path.exists(input_path):
|
| 126 |
os.remove(input_path)
|
| 127 |
except:
|
| 128 |
pass
|
| 129 |
+
|
| 130 |
tasks_db[task_id]["status"] = "completed"
|
| 131 |
+
tasks_db[task_id]["processed_frames"] = total_frames
|
| 132 |
+
print(f"[{task_id}] Processing Completed!")
|
| 133 |
|
| 134 |
except Exception as e:
|
| 135 |
+
shutil.rmtree(temp_dir_frames, ignore_errors=True)
|
| 136 |
+
shutil.rmtree(temp_dir_out_frames, ignore_errors=True)
|
| 137 |
+
try:
|
| 138 |
+
if os.path.exists(input_path):
|
| 139 |
+
os.remove(input_path)
|
| 140 |
+
except:
|
| 141 |
+
pass
|
| 142 |
tasks_db[task_id]["status"] = "failed"
|
| 143 |
tasks_db[task_id]["error"] = str(e)
|
| 144 |
+
print(f"[{task_id}] Error: {e}")
|
| 145 |
|
| 146 |
@app.post("/enhance")
|
| 147 |
async def enhance_video(
|
requirements.txt
CHANGED
|
@@ -1,9 +1,6 @@
|
|
| 1 |
-
torch --index-url https://download.pytorch.org/whl/cpu
|
| 2 |
-
torchvision --index-url https://download.pytorch.org/whl/cpu
|
| 3 |
numpy
|
| 4 |
opencv-python-headless
|
| 5 |
fastapi
|
| 6 |
uvicorn
|
| 7 |
python-multipart
|
| 8 |
-
#
|
| 9 |
-
timm
|
|
|
|
|
|
|
|
|
|
| 1 |
numpy
|
| 2 |
opencv-python-headless
|
| 3 |
fastapi
|
| 4 |
uvicorn
|
| 5 |
python-multipart
|
| 6 |
+
# PyTorch dihapus karena kita offload semua pemrosesan AI 120 FPS ke C++ RIFE-NCNN binary
|
|
|