Spaces:
Sleeping
Sleeping
add queue
Browse files- app/main.py +88 -44
app/main.py
CHANGED
|
@@ -4,6 +4,7 @@ from collections import deque
|
|
| 4 |
from pathlib import Path
|
| 5 |
from typing import Optional, Tuple, List, Dict, Any
|
| 6 |
from dataclasses import dataclass, field
|
|
|
|
| 7 |
|
| 8 |
from fastapi import FastAPI, HTTPException, Response
|
| 9 |
from fastapi.middleware.cors import CORSMiddleware
|
|
@@ -88,6 +89,25 @@ class RateLimiter:
|
|
| 88 |
|
| 89 |
limiter = RateLimiter(10)
|
| 90 |
storyboard_limiter = RateLimiter(30)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 91 |
|
| 92 |
def _to_chat_content_item(item: Any) -> Any:
|
| 93 |
if isinstance(item, str):
|
|
@@ -1236,7 +1256,12 @@ class EmailIn(BaseModel):
|
|
| 1236 |
|
| 1237 |
@app.get("/")
|
| 1238 |
def health():
|
| 1239 |
-
return {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1240 |
|
| 1241 |
@app.post("/generate-code")
|
| 1242 |
def generate_code(inp: GenerateCodeIn):
|
|
@@ -1247,7 +1272,16 @@ def generate_code(inp: GenerateCodeIn):
|
|
| 1247 |
@app.post("/generate-and-render")
|
| 1248 |
def generate_and_render(inp: PromptIn):
|
| 1249 |
try:
|
| 1250 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1251 |
except Exception:
|
| 1252 |
raise HTTPException(500, "Failed to produce video after refinement")
|
| 1253 |
return Response(
|
|
@@ -1261,49 +1295,59 @@ def generate_and_render(inp: PromptIn):
|
|
| 1261 |
def render_code(inp: RenderCodeIn):
|
| 1262 |
quality = _quality_from_settings(inp.settings)
|
| 1263 |
try:
|
| 1264 |
-
|
| 1265 |
-
|
| 1266 |
-
|
| 1267 |
-
|
| 1268 |
-
|
| 1269 |
-
|
| 1270 |
-
|
| 1271 |
-
|
| 1272 |
-
|
| 1273 |
-
|
| 1274 |
-
|
| 1275 |
-
|
| 1276 |
-
|
| 1277 |
-
|
| 1278 |
-
|
| 1279 |
-
|
| 1280 |
-
|
| 1281 |
-
|
| 1282 |
-
|
| 1283 |
-
|
| 1284 |
-
|
| 1285 |
-
|
| 1286 |
-
|
| 1287 |
-
|
| 1288 |
-
|
| 1289 |
-
|
| 1290 |
-
|
| 1291 |
-
|
| 1292 |
-
|
| 1293 |
-
|
| 1294 |
-
|
| 1295 |
-
|
| 1296 |
-
|
| 1297 |
-
|
| 1298 |
-
|
| 1299 |
-
|
| 1300 |
-
|
| 1301 |
-
|
| 1302 |
-
|
| 1303 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1304 |
raise HTTPException(
|
| 1305 |
-
status_code=
|
| 1306 |
-
detail={
|
|
|
|
|
|
|
|
|
|
| 1307 |
)
|
| 1308 |
except Exception as exc:
|
| 1309 |
raise HTTPException(status_code=500, detail={"error": "Unexpected render failure", "log": str(exc)})
|
|
|
|
| 4 |
from pathlib import Path
|
| 5 |
from typing import Optional, Tuple, List, Dict, Any
|
| 6 |
from dataclasses import dataclass, field
|
| 7 |
+
from contextlib import contextmanager
|
| 8 |
|
| 9 |
from fastapi import FastAPI, HTTPException, Response
|
| 10 |
from fastapi.middleware.cors import CORSMiddleware
|
|
|
|
| 89 |
|
| 90 |
limiter = RateLimiter(10)
|
| 91 |
storyboard_limiter = RateLimiter(30)
|
| 92 |
+
RENDER_LOCK = threading.Lock()
|
| 93 |
+
|
| 94 |
+
|
| 95 |
+
@contextmanager
|
| 96 |
+
def acquire_render_slot(timeout: Optional[float] = None):
|
| 97 |
+
"""
|
| 98 |
+
Global render queue: only one Manim render runs at a time.
|
| 99 |
+
Blocks until the lock is available (optional timeout).
|
| 100 |
+
"""
|
| 101 |
+
if timeout is None:
|
| 102 |
+
acquired = RENDER_LOCK.acquire()
|
| 103 |
+
else:
|
| 104 |
+
acquired = RENDER_LOCK.acquire(timeout=timeout)
|
| 105 |
+
if not acquired:
|
| 106 |
+
raise RuntimeError("Render queue is busy; try again shortly.")
|
| 107 |
+
try:
|
| 108 |
+
yield
|
| 109 |
+
finally:
|
| 110 |
+
RENDER_LOCK.release()
|
| 111 |
|
| 112 |
def _to_chat_content_item(item: Any) -> Any:
|
| 113 |
if isinstance(item, str):
|
|
|
|
| 1256 |
|
| 1257 |
@app.get("/")
|
| 1258 |
def health():
|
| 1259 |
+
return {
|
| 1260 |
+
"ok": True,
|
| 1261 |
+
"model": MODEL,
|
| 1262 |
+
"has_gemini": bool(gemini_client),
|
| 1263 |
+
"has_gpt": bool(gpt_client),
|
| 1264 |
+
}
|
| 1265 |
|
| 1266 |
@app.post("/generate-code")
|
| 1267 |
def generate_code(inp: GenerateCodeIn):
|
|
|
|
| 1272 |
@app.post("/generate-and-render")
|
| 1273 |
def generate_and_render(inp: PromptIn):
|
| 1274 |
try:
|
| 1275 |
+
with acquire_render_slot():
|
| 1276 |
+
mp4 = refine_loop(inp.prompt, settings=inp.settings, max_error_refines=3, do_visual_refine=False)
|
| 1277 |
+
except RuntimeError:
|
| 1278 |
+
raise HTTPException(
|
| 1279 |
+
status_code=503,
|
| 1280 |
+
detail={
|
| 1281 |
+
"error": "queue_busy",
|
| 1282 |
+
"message": "Another render is already running. Please wait a moment and try again.",
|
| 1283 |
+
},
|
| 1284 |
+
)
|
| 1285 |
except Exception:
|
| 1286 |
raise HTTPException(500, "Failed to produce video after refinement")
|
| 1287 |
return Response(
|
|
|
|
| 1295 |
def render_code(inp: RenderCodeIn):
|
| 1296 |
quality = _quality_from_settings(inp.settings)
|
| 1297 |
try:
|
| 1298 |
+
with acquire_render_slot():
|
| 1299 |
+
try:
|
| 1300 |
+
mp4_bytes, _ = _run_manim(inp.code, run_id="manual", quality=quality)
|
| 1301 |
+
return Response(
|
| 1302 |
+
content=mp4_bytes,
|
| 1303 |
+
media_type="video/mp4",
|
| 1304 |
+
headers={"Content-Disposition": 'inline; filename="result.mp4"'}
|
| 1305 |
+
)
|
| 1306 |
+
except RenderError as exc:
|
| 1307 |
+
log = exc.log or ""
|
| 1308 |
+
if not inp.auto_fix:
|
| 1309 |
+
raise HTTPException(
|
| 1310 |
+
status_code=400,
|
| 1311 |
+
detail={
|
| 1312 |
+
"error": "Render failed",
|
| 1313 |
+
"message": "Render failed. Attempting automatic fix...",
|
| 1314 |
+
},
|
| 1315 |
+
)
|
| 1316 |
+
fixed_code, fixed_video, final_log = _auto_fix_render(
|
| 1317 |
+
user_prompt=inp.prompt or "User-edited Manim code",
|
| 1318 |
+
code=inp.code,
|
| 1319 |
+
settings=inp.settings,
|
| 1320 |
+
initial_log=log,
|
| 1321 |
+
)
|
| 1322 |
+
if fixed_code and fixed_video:
|
| 1323 |
+
payload = {
|
| 1324 |
+
"auto_fixed": True,
|
| 1325 |
+
"message": "Your code triggered a Manim error, so I applied the smallest possible fix (keeping your edits) and reran the render.",
|
| 1326 |
+
"code": fixed_code,
|
| 1327 |
+
"video_base64": base64.b64encode(fixed_video).decode("utf-8"),
|
| 1328 |
+
"video_mime_type": "video/mp4",
|
| 1329 |
+
"files": [
|
| 1330 |
+
{"filename": "scene.py", "contents": fixed_code}
|
| 1331 |
+
],
|
| 1332 |
+
"meta": {"resolution": inp.settings.get("resolution") if inp.settings else None},
|
| 1333 |
+
"log_tail": (log or "")[-600:]
|
| 1334 |
+
}
|
| 1335 |
+
return Response(
|
| 1336 |
+
content=json.dumps(payload),
|
| 1337 |
+
media_type="application/json",
|
| 1338 |
+
)
|
| 1339 |
+
detail_log = (final_log or log)[-6000:]
|
| 1340 |
+
raise HTTPException(
|
| 1341 |
+
status_code=400,
|
| 1342 |
+
detail={"error": "Render failed", "log": detail_log, "code": inp.code},
|
| 1343 |
+
)
|
| 1344 |
+
except RuntimeError:
|
| 1345 |
raise HTTPException(
|
| 1346 |
+
status_code=503,
|
| 1347 |
+
detail={
|
| 1348 |
+
"error": "queue_busy",
|
| 1349 |
+
"message": "Another render is already running. Please wait a moment and try again.",
|
| 1350 |
+
},
|
| 1351 |
)
|
| 1352 |
except Exception as exc:
|
| 1353 |
raise HTTPException(status_code=500, detail={"error": "Unexpected render failure", "log": str(exc)})
|