Verdiola commited on
Commit
cb5e24d
·
verified ·
1 Parent(s): 6ca83b6
Files changed (1) hide show
  1. 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 {"ok": True, "model": MODEL, "has_gemini": bool(API_KEY)}
 
 
 
 
 
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
- mp4 = refine_loop(inp.prompt, settings=inp.settings, max_error_refines=3, do_visual_refine=False)
 
 
 
 
 
 
 
 
 
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
- mp4_bytes, _ = _run_manim(inp.code, run_id="manual", quality=quality)
1265
- return Response(
1266
- content=mp4_bytes,
1267
- media_type="video/mp4",
1268
- headers={"Content-Disposition": 'inline; filename="result.mp4"'}
1269
- )
1270
- except RenderError as exc:
1271
- log = exc.log or ""
1272
- if not inp.auto_fix:
1273
- raise HTTPException(
1274
- status_code=400,
1275
- detail={
1276
- "error": "Render failed",
1277
- "message": "Render failed. Attempting automatic fix...",
1278
- },
1279
- )
1280
- fixed_code, fixed_video, final_log = _auto_fix_render(
1281
- user_prompt=inp.prompt or "User-edited Manim code",
1282
- code=inp.code,
1283
- settings=inp.settings,
1284
- initial_log=log,
1285
- )
1286
- if fixed_code and fixed_video:
1287
- payload = {
1288
- "auto_fixed": True,
1289
- "message": "Your code triggered a Manim error, so I applied the smallest possible fix (keeping your edits) and reran the render.",
1290
- "code": fixed_code,
1291
- "video_base64": base64.b64encode(fixed_video).decode("utf-8"),
1292
- "video_mime_type": "video/mp4",
1293
- "files": [
1294
- {"filename": "scene.py", "contents": fixed_code}
1295
- ],
1296
- "meta": {"resolution": inp.settings.get("resolution") if inp.settings else None},
1297
- "log_tail": (log or "")[-600:]
1298
- }
1299
- return Response(
1300
- content=json.dumps(payload),
1301
- media_type="application/json",
1302
- )
1303
- detail_log = (final_log or log)[-6000:]
 
 
 
 
 
 
 
1304
  raise HTTPException(
1305
- status_code=400,
1306
- detail={"error": "Render failed", "log": detail_log, "code": inp.code},
 
 
 
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)})