Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -138,7 +138,259 @@
|
|
| 138 |
|
| 139 |
|
| 140 |
|
| 141 |
-
# app.py
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 142 |
import os
|
| 143 |
import uuid
|
| 144 |
import shutil
|
|
@@ -149,23 +401,16 @@ from fastapi import FastAPI, File, UploadFile, Form, HTTPException
|
|
| 149 |
from fastapi.responses import FileResponse, JSONResponse
|
| 150 |
from fastapi.middleware.cors import CORSMiddleware
|
| 151 |
import subprocess
|
|
|
|
|
|
|
|
|
|
| 152 |
# local imports
|
| 153 |
from task_queue import TaskQueue, TaskStatus
|
| 154 |
-
from core.pipeline import process_image_pipeline
|
| 155 |
-
from fastapi.responses import FileResponse, JSONResponse, Response
|
| 156 |
-
from fastapi.responses import JSONResponse, Response, FileResponse
|
| 157 |
-
from fastapi import HTTPException
|
| 158 |
-
from pathlib import Path
|
| 159 |
-
import base64
|
| 160 |
-
import base64
|
| 161 |
-
from pathlib import Path
|
| 162 |
-
from moviepy.video.io.VideoFileClip import VideoFileClip
|
| 163 |
|
| 164 |
# --------------------------------------------------
|
| 165 |
# Logging Setup
|
| 166 |
# --------------------------------------------------
|
| 167 |
-
import logging
|
| 168 |
-
|
| 169 |
logging.basicConfig(
|
| 170 |
level=logging.DEBUG,
|
| 171 |
format="π [%(asctime)s] [%(levelname)s] %(message)s",
|
|
@@ -177,7 +422,7 @@ logger = logging.getLogger("manim_render_service")
|
|
| 177 |
# App config
|
| 178 |
# --------------------------------------------------
|
| 179 |
APP_NAME = "manim_render_service"
|
| 180 |
-
TMP_ROOT = Path("tmp")/ APP_NAME
|
| 181 |
TASKS_DIR = TMP_ROOT / "tasks"
|
| 182 |
OUTPUTS_DIR = TMP_ROOT / "outputs"
|
| 183 |
TASKS_DIR.mkdir(parents=True, exist_ok=True)
|
|
@@ -298,167 +543,69 @@ async def status(task_id: str):
|
|
| 298 |
})
|
| 299 |
|
| 300 |
|
| 301 |
-
# async def result(task_id: str):
|
| 302 |
-
# logger.debug(f"π¦ Fetching result for task: {task_id}")
|
| 303 |
-
# info = queue.get_task_info(task_id)
|
| 304 |
-
# if info is None:
|
| 305 |
-
# logger.warning(f"β οΈ Task info not found for {task_id}")
|
| 306 |
-
# raise HTTPException(status_code=404, detail="Task not found")
|
| 307 |
-
|
| 308 |
-
# status = queue.get_status(task_id)
|
| 309 |
-
# logger.debug(f"π Task {task_id} current status: {status.name}")
|
| 310 |
-
|
| 311 |
-
# if status != TaskStatus.COMPLETED:
|
| 312 |
-
# logger.info(f"β³ Task {task_id} still in progress ({status.name})")
|
| 313 |
-
# return JSONResponse({"task_id": task_id, "status": status.name})
|
| 314 |
-
|
| 315 |
-
# output_path = Path(info.get("output_path", ""))
|
| 316 |
-
# logger.debug(f"π§© Checking output path: {output_path}")
|
| 317 |
-
# # if not output_path.exists():
|
| 318 |
-
# # logger.error(f"β Output file missing for task {task_id}")
|
| 319 |
-
# # raise HTTPException(status_code=404, detail="Output not found on disk")
|
| 320 |
-
|
| 321 |
-
# info = queue.get_task_info(task_id)
|
| 322 |
-
# output_bytes = info.get("output_bytes")
|
| 323 |
-
|
| 324 |
-
# if output_bytes:
|
| 325 |
-
# logger.info(f"π¬ Returning in-memory video for task {task_id}")
|
| 326 |
-
# return Response(content=output_bytes, media_type="video")
|
| 327 |
-
|
| 328 |
-
# # fallback to disk if memory missing
|
| 329 |
-
# if not output_path.exists():
|
| 330 |
-
# logger.error(f"β Output file missing for task {task_id}")
|
| 331 |
-
# raise HTTPException(status_code=404, detail="Output not found on disk")
|
| 332 |
-
|
| 333 |
-
# logger.info(f"π¬ Returning result video from disk for task {task_id}")
|
| 334 |
-
# return FileResponse(
|
| 335 |
-
# path=str(output_path), filename=output_path.name, media_type="video"
|
| 336 |
-
# )
|
| 337 |
@app.get("/result/{task_id}")
|
| 338 |
-
# async def result(task_id: str):
|
| 339 |
-
# logger.debug(f"π¦ Fetching result for task: {task_id}")
|
| 340 |
-
# info = queue.get_task_info(task_id)
|
| 341 |
-
# if info is None:
|
| 342 |
-
# logger.warning(f"β οΈ Task info not found for {task_id}")
|
| 343 |
-
# raise HTTPException(status_code=404, detail="Task not found")
|
| 344 |
-
|
| 345 |
-
# status = queue.get_status(task_id)
|
| 346 |
-
# logger.debug(f"π Task {task_id} current status: {status.name}")
|
| 347 |
-
|
| 348 |
-
# if status != TaskStatus.COMPLETED:
|
| 349 |
-
# logger.info(f"β³ Task {task_id} still in progress ({status.name})")
|
| 350 |
-
# return JSONResponse({"task_id": task_id, "status": status.name})
|
| 351 |
-
|
| 352 |
-
# info = queue.get_task_info(task_id)
|
| 353 |
-
# output_path = Path(info.get("output_path", "")) # MOV path
|
| 354 |
-
|
| 355 |
-
# if not output_path.exists():
|
| 356 |
-
# logger.error(f"β Output file missing for task {task_id}")
|
| 357 |
-
# raise HTTPException(status_code=404, detail="Output not found")
|
| 358 |
-
|
| 359 |
-
# # Convert MOV to WEBM with alpha if needed
|
| 360 |
-
# webm_path = output_path.with_suffix(".webm")
|
| 361 |
-
# if output_path.suffix.lower() == ".mov" and not webm_path.exists():
|
| 362 |
-
# try:
|
| 363 |
-
# logger.info(f"ποΈ Converting .mov β .webm (keeping transparency)...")
|
| 364 |
-
# cmd = [
|
| 365 |
-
# "ffmpeg",
|
| 366 |
-
# "-y",
|
| 367 |
-
# "-i", str(output_path),
|
| 368 |
-
# "-c:v", "libvpx-vp9",
|
| 369 |
-
# "-pix_fmt", "yuva420p", # keep alpha channel
|
| 370 |
-
# "-b:v", "4M",
|
| 371 |
-
# "-auto-alt-ref", "0",
|
| 372 |
-
# str(webm_path)
|
| 373 |
-
# ]
|
| 374 |
-
# subprocess.run(cmd, check=True, capture_output=True)
|
| 375 |
-
# logger.info(f"β
Converted successfully β {webm_path}")
|
| 376 |
-
# except Exception as e:
|
| 377 |
-
# logger.error(f"β οΈ MOVβWEBM conversion failed: {e}")
|
| 378 |
-
# raise HTTPException(status_code=500, detail=f"Conversion failed: {e}")
|
| 379 |
-
|
| 380 |
-
# # Read both MOV and WEBM as bytes
|
| 381 |
-
# mov_bytes = output_path.read_bytes()
|
| 382 |
-
# webm_bytes = webm_path.read_bytes()
|
| 383 |
-
|
| 384 |
-
# logger.info(f"β
Returning both MOV + WEBM for task {task_id}")
|
| 385 |
-
# return JSONResponse({
|
| 386 |
-
# "task_id": task_id,
|
| 387 |
-
# "status": "COMPLETED",
|
| 388 |
-
# "results": [
|
| 389 |
-
# {
|
| 390 |
-
# "format": "mov",
|
| 391 |
-
# "data": base64.b64encode(mov_bytes).decode("utf-8"),
|
| 392 |
-
# },
|
| 393 |
-
# {
|
| 394 |
-
# "format": "webm",
|
| 395 |
-
# "data": base64.b64encode(webm_bytes).decode("utf-8"),
|
| 396 |
-
# },
|
| 397 |
-
# ],
|
| 398 |
-
# })
|
| 399 |
async def result(task_id: str):
|
| 400 |
logger.debug(f"π¦ Fetching result for task: {task_id}")
|
| 401 |
info = queue.get_task_info(task_id)
|
| 402 |
if info is None:
|
| 403 |
logger.warning(f"β οΈ Task info not found for {task_id}")
|
| 404 |
raise HTTPException(status_code=404, detail="Task not found")
|
|
|
|
| 405 |
status = queue.get_status(task_id)
|
| 406 |
logger.debug(f"π Task {task_id} current status: {status.name}")
|
| 407 |
if status != TaskStatus.COMPLETED:
|
| 408 |
logger.info(f"β³ Task {task_id} still in progress ({status.name})")
|
| 409 |
return JSONResponse({"task_id": task_id, "status": status.name})
|
| 410 |
-
|
| 411 |
-
output_path = Path(info.get("output_path", ""))
|
| 412 |
if not output_path.exists():
|
| 413 |
logger.error(f"β Output file missing for task {task_id}")
|
| 414 |
raise HTTPException(status_code=404, detail="Output not found")
|
| 415 |
|
| 416 |
-
# Convert MOV to optimized WEBM
|
| 417 |
webm_path = output_path.with_suffix(".webm")
|
| 418 |
if output_path.suffix.lower() == ".mov" and not webm_path.exists():
|
| 419 |
try:
|
| 420 |
-
logger.info(f"ποΈ Converting .mov β .webm (optimized
|
| 421 |
cmd = [
|
| 422 |
"ffmpeg",
|
| 423 |
"-y",
|
| 424 |
"-i", str(output_path),
|
| 425 |
"-c:v", "libvpx-vp9",
|
| 426 |
-
"-pix_fmt", "yuva420p",
|
| 427 |
-
"-b:v", "2M",
|
| 428 |
"-maxrate", "2.5M",
|
| 429 |
"-bufsize", "5M",
|
| 430 |
"-auto-alt-ref", "0",
|
| 431 |
-
"-cpu-used", "4",
|
| 432 |
-
"-tile-columns", "2",
|
| 433 |
"-tile-rows", "2",
|
| 434 |
str(webm_path)
|
| 435 |
]
|
| 436 |
subprocess.run(cmd, check=True, capture_output=True)
|
| 437 |
|
| 438 |
-
# Log file sizes
|
| 439 |
mov_size = output_path.stat().st_size / (1024 * 1024)
|
| 440 |
webm_size = webm_path.stat().st_size / (1024 * 1024)
|
| 441 |
-
logger.info(f"β
Converted
|
| 442 |
-
logger.info(f"π File sizes - MOV: {mov_size:.2f}MB β WEBM: {webm_size:.2f}MB (reduction: {((1 - webm_size/mov_size) * 100):.1f}%)")
|
| 443 |
except Exception as e:
|
| 444 |
-
logger.error(f"β οΈ
|
| 445 |
raise HTTPException(status_code=500, detail=f"Conversion failed: {e}")
|
| 446 |
|
| 447 |
-
|
| 448 |
-
|
| 449 |
-
|
| 450 |
-
logger.info(f"β
Sending WEBM only ({webm_size:.2f}MB) for task {task_id}")
|
| 451 |
|
| 452 |
-
|
| 453 |
-
|
| 454 |
-
|
| 455 |
-
|
| 456 |
-
|
| 457 |
-
|
| 458 |
-
|
| 459 |
-
|
| 460 |
-
|
| 461 |
-
|
|
|
|
| 462 |
@app.delete("/task/{task_id}")
|
| 463 |
async def delete_task(task_id: str):
|
| 464 |
logger.info(f"π Request to delete task: {task_id}")
|
|
@@ -476,10 +623,6 @@ async def delete_task(task_id: str):
|
|
| 476 |
raise HTTPException(status_code=404, detail="Task not found")
|
| 477 |
|
| 478 |
|
| 479 |
-
|
| 480 |
-
|
| 481 |
-
|
| 482 |
@app.get("/")
|
| 483 |
def home():
|
| 484 |
-
return {"status": "Your Manim backend is running!"}
|
| 485 |
-
|
|
|
|
| 138 |
|
| 139 |
|
| 140 |
|
| 141 |
+
# # app.py
|
| 142 |
+
# import os
|
| 143 |
+
# import uuid
|
| 144 |
+
# import shutil
|
| 145 |
+
# import tempfile
|
| 146 |
+
# import asyncio
|
| 147 |
+
# from pathlib import Path
|
| 148 |
+
# from fastapi import FastAPI, File, UploadFile, Form, HTTPException
|
| 149 |
+
# from fastapi.responses import FileResponse, JSONResponse
|
| 150 |
+
# from fastapi.middleware.cors import CORSMiddleware
|
| 151 |
+
# import subprocess
|
| 152 |
+
# # local imports
|
| 153 |
+
# from task_queue import TaskQueue, TaskStatus
|
| 154 |
+
# from core.pipeline import process_image_pipeline # your real pipeline
|
| 155 |
+
# from fastapi.responses import FileResponse, JSONResponse, Response
|
| 156 |
+
# from fastapi.responses import JSONResponse, Response, FileResponse
|
| 157 |
+
# from fastapi import HTTPException
|
| 158 |
+
# from pathlib import Path
|
| 159 |
+
# import base64
|
| 160 |
+
# import base64
|
| 161 |
+
# from pathlib import Path
|
| 162 |
+
# from moviepy.video.io.VideoFileClip import VideoFileClip
|
| 163 |
+
|
| 164 |
+
# # --------------------------------------------------
|
| 165 |
+
# # Logging Setup
|
| 166 |
+
# # --------------------------------------------------
|
| 167 |
+
# import logging
|
| 168 |
+
|
| 169 |
+
# logging.basicConfig(
|
| 170 |
+
# level=logging.DEBUG,
|
| 171 |
+
# format="π [%(asctime)s] [%(levelname)s] %(message)s",
|
| 172 |
+
# datefmt="%H:%M:%S",
|
| 173 |
+
# )
|
| 174 |
+
# logger = logging.getLogger("manim_render_service")
|
| 175 |
+
|
| 176 |
+
# # --------------------------------------------------
|
| 177 |
+
# # App config
|
| 178 |
+
# # --------------------------------------------------
|
| 179 |
+
# APP_NAME = "manim_render_service"
|
| 180 |
+
# TMP_ROOT = Path("tmp")/ APP_NAME
|
| 181 |
+
# TASKS_DIR = TMP_ROOT / "tasks"
|
| 182 |
+
# OUTPUTS_DIR = TMP_ROOT / "outputs"
|
| 183 |
+
# TASKS_DIR.mkdir(parents=True, exist_ok=True)
|
| 184 |
+
# OUTPUTS_DIR.mkdir(parents=True, exist_ok=True)
|
| 185 |
+
|
| 186 |
+
# queue = TaskQueue(base_dir=TMP_ROOT, max_workers=os.cpu_count() or 2)
|
| 187 |
+
|
| 188 |
+
# app = FastAPI(title="Manim Render Service")
|
| 189 |
+
# app.add_middleware(
|
| 190 |
+
# CORSMiddleware,
|
| 191 |
+
# allow_origins=["*"],
|
| 192 |
+
# allow_credentials=True,
|
| 193 |
+
# allow_methods=["*"],
|
| 194 |
+
# allow_headers=["*"],
|
| 195 |
+
# )
|
| 196 |
+
|
| 197 |
+
# # --------------------------------------------------
|
| 198 |
+
# # Lifecycle Events
|
| 199 |
+
# # --------------------------------------------------
|
| 200 |
+
# @app.on_event("startup")
|
| 201 |
+
# async def startup_event():
|
| 202 |
+
# logger.info("π Starting up backend...")
|
| 203 |
+
# logger.debug(f"Temporary root: {TMP_ROOT}")
|
| 204 |
+
# await queue.start(processor=process_image_pipeline)
|
| 205 |
+
# logger.info("β
Queue system initialized and worker started.")
|
| 206 |
+
|
| 207 |
+
|
| 208 |
+
# @app.on_event("shutdown")
|
| 209 |
+
# async def shutdown_event():
|
| 210 |
+
# logger.info("π§Ή Shutting down backend...")
|
| 211 |
+
# await queue.stop()
|
| 212 |
+
# logger.info("π Queue stopped gracefully.")
|
| 213 |
+
|
| 214 |
+
|
| 215 |
+
# # --------------------------------------------------
|
| 216 |
+
# # Helpers
|
| 217 |
+
# # --------------------------------------------------
|
| 218 |
+
# def _make_task_dir(task_id: str) -> Path:
|
| 219 |
+
# p = TASKS_DIR / task_id
|
| 220 |
+
# p.mkdir(parents=True, exist_ok=True)
|
| 221 |
+
# logger.debug(f"π Created task directory: {p}")
|
| 222 |
+
# return p
|
| 223 |
+
|
| 224 |
+
|
| 225 |
+
# def _secure_filename(filename: str) -> str:
|
| 226 |
+
# safe = "".join(c for c in filename if c.isalnum() or c in "._-").strip("_")
|
| 227 |
+
# logger.debug(f"π Secured filename: {filename} β {safe}")
|
| 228 |
+
# return safe
|
| 229 |
+
|
| 230 |
+
|
| 231 |
+
# # --------------------------------------------------
|
| 232 |
+
# # Routes
|
| 233 |
+
# # --------------------------------------------------
|
| 234 |
+
# @app.post("/render", status_code=202)
|
| 235 |
+
# async def submit_render(
|
| 236 |
+
# image: UploadFile = File(...),
|
| 237 |
+
# style: str = Form("fade-in"),
|
| 238 |
+
# quality: str = Form("final"),
|
| 239 |
+
# ):
|
| 240 |
+
# logger.info(f"π¨ Received new render request | style={style}, quality={quality}")
|
| 241 |
+
# logger.debug(f"Uploaded file info: {image.filename}, type={image.content_type}")
|
| 242 |
+
|
| 243 |
+
# if image.content_type.split("/")[0] != "image":
|
| 244 |
+
# logger.error("β Invalid file type, not an image.")
|
| 245 |
+
# raise HTTPException(status_code=400, detail="Uploaded file must be an image.")
|
| 246 |
+
|
| 247 |
+
# task_id = uuid.uuid4().hex
|
| 248 |
+
# task_dir = _make_task_dir(task_id)
|
| 249 |
+
# logger.info(f"π Generated Task ID: {task_id}")
|
| 250 |
+
|
| 251 |
+
# safe_name = _secure_filename(image.filename or f"{task_id}.png")
|
| 252 |
+
# uploaded_path = task_dir / safe_name
|
| 253 |
+
# logger.debug(f"π Saving upload to {uploaded_path}")
|
| 254 |
+
|
| 255 |
+
# try:
|
| 256 |
+
# with uploaded_path.open("wb") as f:
|
| 257 |
+
# content = await image.read()
|
| 258 |
+
# logger.debug(f"π¦ File size: {len(content)/1024:.2f} KB")
|
| 259 |
+
# if len(content) > 25 * 1024 * 1024:
|
| 260 |
+
# logger.warning("β οΈ Upload too large (>25MB). Rejecting.")
|
| 261 |
+
# raise HTTPException(status_code=413, detail="File too large (max 25MB).")
|
| 262 |
+
# f.write(content)
|
| 263 |
+
# finally:
|
| 264 |
+
# await image.close()
|
| 265 |
+
# logger.debug("π Image file closed after writing.")
|
| 266 |
+
|
| 267 |
+
# meta = {
|
| 268 |
+
# "task_id": task_id,
|
| 269 |
+
# "input_image": str(uploaded_path),
|
| 270 |
+
# "style": style,
|
| 271 |
+
# "quality": quality,
|
| 272 |
+
# "task_dir": str(task_dir),
|
| 273 |
+
# }
|
| 274 |
+
|
| 275 |
+
# logger.debug(f"π§Ύ Task metadata: {meta}")
|
| 276 |
+
# queue.enqueue(meta)
|
| 277 |
+
# logger.info(f"π€ Task {task_id} successfully enqueued.")
|
| 278 |
+
|
| 279 |
+
# return JSONResponse({"task_id": task_id, "status": "queued"})
|
| 280 |
+
|
| 281 |
+
|
| 282 |
+
# @app.get("/status/{task_id}")
|
| 283 |
+
# async def status(task_id: str):
|
| 284 |
+
# logger.debug(f"Status check for task: {task_id}")
|
| 285 |
+
# st = queue.get_status(task_id)
|
| 286 |
+
# if st is None:
|
| 287 |
+
# logger.warning(f"Task {task_id} not found")
|
| 288 |
+
# raise HTTPException(status_code=404, detail="Task not found")
|
| 289 |
+
|
| 290 |
+
# # Get additional info
|
| 291 |
+
# task_info = queue.get_task_info(task_id)
|
| 292 |
+
# logger.info(f"Task {task_id} status: {st.name} | info: {task_info}")
|
| 293 |
+
|
| 294 |
+
# return JSONResponse({
|
| 295 |
+
# "task_id": task_id,
|
| 296 |
+
# "status": st.name,
|
| 297 |
+
# "details": task_info
|
| 298 |
+
# })
|
| 299 |
+
|
| 300 |
+
|
| 301 |
+
|
| 302 |
+
# @app.get("/result/{task_id}")
|
| 303 |
+
# async def result(task_id: str):
|
| 304 |
+
# logger.debug(f"π¦ Fetching result for task: {task_id}")
|
| 305 |
+
# info = queue.get_task_info(task_id)
|
| 306 |
+
# if info is None:
|
| 307 |
+
# logger.warning(f"β οΈ Task info not found for {task_id}")
|
| 308 |
+
# raise HTTPException(status_code=404, detail="Task not found")
|
| 309 |
+
# status = queue.get_status(task_id)
|
| 310 |
+
# logger.debug(f"π Task {task_id} current status: {status.name}")
|
| 311 |
+
# if status != TaskStatus.COMPLETED:
|
| 312 |
+
# logger.info(f"β³ Task {task_id} still in progress ({status.name})")
|
| 313 |
+
# return JSONResponse({"task_id": task_id, "status": status.name})
|
| 314 |
+
# info = queue.get_task_info(task_id)
|
| 315 |
+
# output_path = Path(info.get("output_path", "")) # MOV path
|
| 316 |
+
# if not output_path.exists():
|
| 317 |
+
# logger.error(f"β Output file missing for task {task_id}")
|
| 318 |
+
# raise HTTPException(status_code=404, detail="Output not found")
|
| 319 |
+
|
| 320 |
+
# # Convert MOV to optimized WEBM with reduced file size
|
| 321 |
+
# webm_path = output_path.with_suffix(".webm")
|
| 322 |
+
# if output_path.suffix.lower() == ".mov" and not webm_path.exists():
|
| 323 |
+
# try:
|
| 324 |
+
# logger.info(f"ποΈ Converting .mov β .webm (optimized for size)...")
|
| 325 |
+
# cmd = [
|
| 326 |
+
# "ffmpeg",
|
| 327 |
+
# "-y",
|
| 328 |
+
# "-i", str(output_path),
|
| 329 |
+
# "-c:v", "libvpx-vp9",
|
| 330 |
+
# "-pix_fmt", "yuva420p", # keep alpha channel
|
| 331 |
+
# "-b:v", "2M", # Reduced bitrate (from 4M to 2M) - keeps quality but smaller file
|
| 332 |
+
# "-maxrate", "2.5M",
|
| 333 |
+
# "-bufsize", "5M",
|
| 334 |
+
# "-auto-alt-ref", "0",
|
| 335 |
+
# "-cpu-used", "4", # Speed up encoding
|
| 336 |
+
# "-tile-columns", "2", # Enable parallelization
|
| 337 |
+
# "-tile-rows", "2",
|
| 338 |
+
# str(webm_path)
|
| 339 |
+
# ]
|
| 340 |
+
# subprocess.run(cmd, check=True, capture_output=True)
|
| 341 |
+
|
| 342 |
+
# # Log file sizes
|
| 343 |
+
# mov_size = output_path.stat().st_size / (1024 * 1024)
|
| 344 |
+
# webm_size = webm_path.stat().st_size / (1024 * 1024)
|
| 345 |
+
# logger.info(f"β
Converted successfully β {webm_path}")
|
| 346 |
+
# logger.info(f"π File sizes - MOV: {mov_size:.2f}MB β WEBM: {webm_size:.2f}MB (reduction: {((1 - webm_size/mov_size) * 100):.1f}%)")
|
| 347 |
+
# except Exception as e:
|
| 348 |
+
# logger.error(f"β οΈ MOVβWEBM conversion failed: {e}")
|
| 349 |
+
# raise HTTPException(status_code=500, detail=f"Conversion failed: {e}")
|
| 350 |
+
|
| 351 |
+
# # Read only WEBM as bytes
|
| 352 |
+
# webm_bytes = webm_path.read_bytes()
|
| 353 |
+
# webm_size = len(webm_bytes) / (1024 * 1024)
|
| 354 |
+
# logger.info(f"β
Sending WEBM only ({webm_size:.2f}MB) for task {task_id}")
|
| 355 |
+
|
| 356 |
+
# return JSONResponse({
|
| 357 |
+
# "task_id": task_id,
|
| 358 |
+
# "status": "COMPLETED",
|
| 359 |
+
# "results": [
|
| 360 |
+
# {
|
| 361 |
+
# "format": "webm",
|
| 362 |
+
# "data": base64.b64encode(webm_bytes).decode("utf-8"),
|
| 363 |
+
# },
|
| 364 |
+
# ],
|
| 365 |
+
# })
|
| 366 |
+
# @app.delete("/task/{task_id}")
|
| 367 |
+
# async def delete_task(task_id: str):
|
| 368 |
+
# logger.info(f"π Request to delete task: {task_id}")
|
| 369 |
+
# info = queue.get_task_info(task_id)
|
| 370 |
+
# if info:
|
| 371 |
+
# task_dir = Path(info.get("task_dir", ""))
|
| 372 |
+
# if task_dir.exists():
|
| 373 |
+
# logger.debug(f"π§Ή Removing directory: {task_dir}")
|
| 374 |
+
# shutil.rmtree(task_dir, ignore_errors=True)
|
| 375 |
+
# queue.remove_task(task_id)
|
| 376 |
+
# logger.info(f"β
Task {task_id} removed successfully.")
|
| 377 |
+
# return JSONResponse({"task_id": task_id, "status": "removed"})
|
| 378 |
+
# else:
|
| 379 |
+
# logger.warning(f"β οΈ Task {task_id} not found for deletion.")
|
| 380 |
+
# raise HTTPException(status_code=404, detail="Task not found")
|
| 381 |
+
|
| 382 |
+
|
| 383 |
+
|
| 384 |
+
|
| 385 |
+
|
| 386 |
+
# @app.get("/")
|
| 387 |
+
# def home():
|
| 388 |
+
# return {"status": "Your Manim backend is running!"}
|
| 389 |
+
|
| 390 |
+
|
| 391 |
+
|
| 392 |
+
|
| 393 |
+
|
| 394 |
import os
|
| 395 |
import uuid
|
| 396 |
import shutil
|
|
|
|
| 401 |
from fastapi.responses import FileResponse, JSONResponse
|
| 402 |
from fastapi.middleware.cors import CORSMiddleware
|
| 403 |
import subprocess
|
| 404 |
+
import logging
|
| 405 |
+
import base64
|
| 406 |
+
|
| 407 |
# local imports
|
| 408 |
from task_queue import TaskQueue, TaskStatus
|
| 409 |
+
from core.pipeline import process_image_pipeline
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 410 |
|
| 411 |
# --------------------------------------------------
|
| 412 |
# Logging Setup
|
| 413 |
# --------------------------------------------------
|
|
|
|
|
|
|
| 414 |
logging.basicConfig(
|
| 415 |
level=logging.DEBUG,
|
| 416 |
format="π [%(asctime)s] [%(levelname)s] %(message)s",
|
|
|
|
| 422 |
# App config
|
| 423 |
# --------------------------------------------------
|
| 424 |
APP_NAME = "manim_render_service"
|
| 425 |
+
TMP_ROOT = Path("tmp") / APP_NAME
|
| 426 |
TASKS_DIR = TMP_ROOT / "tasks"
|
| 427 |
OUTPUTS_DIR = TMP_ROOT / "outputs"
|
| 428 |
TASKS_DIR.mkdir(parents=True, exist_ok=True)
|
|
|
|
| 543 |
})
|
| 544 |
|
| 545 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 546 |
@app.get("/result/{task_id}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 547 |
async def result(task_id: str):
|
| 548 |
logger.debug(f"π¦ Fetching result for task: {task_id}")
|
| 549 |
info = queue.get_task_info(task_id)
|
| 550 |
if info is None:
|
| 551 |
logger.warning(f"β οΈ Task info not found for {task_id}")
|
| 552 |
raise HTTPException(status_code=404, detail="Task not found")
|
| 553 |
+
|
| 554 |
status = queue.get_status(task_id)
|
| 555 |
logger.debug(f"π Task {task_id} current status: {status.name}")
|
| 556 |
if status != TaskStatus.COMPLETED:
|
| 557 |
logger.info(f"β³ Task {task_id} still in progress ({status.name})")
|
| 558 |
return JSONResponse({"task_id": task_id, "status": status.name})
|
| 559 |
+
|
| 560 |
+
output_path = Path(info.get("output_path", ""))
|
| 561 |
if not output_path.exists():
|
| 562 |
logger.error(f"β Output file missing for task {task_id}")
|
| 563 |
raise HTTPException(status_code=404, detail="Output not found")
|
| 564 |
|
| 565 |
+
# Convert MOV to optimized WEBM
|
| 566 |
webm_path = output_path.with_suffix(".webm")
|
| 567 |
if output_path.suffix.lower() == ".mov" and not webm_path.exists():
|
| 568 |
try:
|
| 569 |
+
logger.info(f"ποΈ Converting .mov β .webm (optimized)...")
|
| 570 |
cmd = [
|
| 571 |
"ffmpeg",
|
| 572 |
"-y",
|
| 573 |
"-i", str(output_path),
|
| 574 |
"-c:v", "libvpx-vp9",
|
| 575 |
+
"-pix_fmt", "yuva420p",
|
| 576 |
+
"-b:v", "2M",
|
| 577 |
"-maxrate", "2.5M",
|
| 578 |
"-bufsize", "5M",
|
| 579 |
"-auto-alt-ref", "0",
|
| 580 |
+
"-cpu-used", "4",
|
| 581 |
+
"-tile-columns", "2",
|
| 582 |
"-tile-rows", "2",
|
| 583 |
str(webm_path)
|
| 584 |
]
|
| 585 |
subprocess.run(cmd, check=True, capture_output=True)
|
| 586 |
|
|
|
|
| 587 |
mov_size = output_path.stat().st_size / (1024 * 1024)
|
| 588 |
webm_size = webm_path.stat().st_size / (1024 * 1024)
|
| 589 |
+
logger.info(f"β
Converted: MOV {mov_size:.2f}MB β WEBM {webm_size:.2f}MB ({((1 - webm_size/mov_size) * 100):.1f}% smaller)")
|
|
|
|
| 590 |
except Exception as e:
|
| 591 |
+
logger.error(f"β οΈ Conversion failed: {e}")
|
| 592 |
raise HTTPException(status_code=500, detail=f"Conversion failed: {e}")
|
| 593 |
|
| 594 |
+
if not webm_path.exists():
|
| 595 |
+
logger.error(f"β WEBM file not found after conversion: {webm_path}")
|
| 596 |
+
raise HTTPException(status_code=500, detail="WEBM file generation failed")
|
|
|
|
| 597 |
|
| 598 |
+
webm_size = webm_path.stat().st_size / (1024 * 1024)
|
| 599 |
+
logger.info(f"β
Streaming WEBM ({webm_size:.2f}MB) for task {task_id}")
|
| 600 |
+
|
| 601 |
+
# Stream the file directly instead of base64 encoding
|
| 602 |
+
return FileResponse(
|
| 603 |
+
path=webm_path,
|
| 604 |
+
media_type="video/webm",
|
| 605 |
+
filename=f"{task_id}.webm"
|
| 606 |
+
)
|
| 607 |
+
|
| 608 |
+
|
| 609 |
@app.delete("/task/{task_id}")
|
| 610 |
async def delete_task(task_id: str):
|
| 611 |
logger.info(f"π Request to delete task: {task_id}")
|
|
|
|
| 623 |
raise HTTPException(status_code=404, detail="Task not found")
|
| 624 |
|
| 625 |
|
|
|
|
|
|
|
|
|
|
| 626 |
@app.get("/")
|
| 627 |
def home():
|
| 628 |
+
return {"status": "Your Manim backend is running!"}
|
|
|