karthikeya1212 commited on
Commit
eeba4b0
Β·
verified Β·
1 Parent(s): c2b0530

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +284 -141
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 # 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",
@@ -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
- info = queue.get_task_info(task_id)
411
- output_path = Path(info.get("output_path", "")) # MOV 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 with reduced file size
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 for size)...")
421
  cmd = [
422
  "ffmpeg",
423
  "-y",
424
  "-i", str(output_path),
425
  "-c:v", "libvpx-vp9",
426
- "-pix_fmt", "yuva420p", # keep alpha channel
427
- "-b:v", "2M", # Reduced bitrate (from 4M to 2M) - keeps quality but smaller file
428
  "-maxrate", "2.5M",
429
  "-bufsize", "5M",
430
  "-auto-alt-ref", "0",
431
- "-cpu-used", "4", # Speed up encoding
432
- "-tile-columns", "2", # Enable parallelization
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 successfully β†’ {webm_path}")
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"⚠️ MOVβ†’WEBM conversion failed: {e}")
445
  raise HTTPException(status_code=500, detail=f"Conversion failed: {e}")
446
 
447
- # Read only WEBM as bytes
448
- webm_bytes = webm_path.read_bytes()
449
- webm_size = len(webm_bytes) / (1024 * 1024)
450
- logger.info(f"βœ… Sending WEBM only ({webm_size:.2f}MB) for task {task_id}")
451
 
452
- return JSONResponse({
453
- "task_id": task_id,
454
- "status": "COMPLETED",
455
- "results": [
456
- {
457
- "format": "webm",
458
- "data": base64.b64encode(webm_bytes).decode("utf-8"),
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!"}