""" pipeline.py — Main pipeline: PDF → Gemini → Manim → Upload/Email → Cleanup """ from __future__ import annotations import os import shutil import subprocess import textwrap import time from pathlib import Path from typing import Callable from queue_manager import State from utils import ( extract_pdf_text, generate_manim_code, send_video_email, upload_to_catbox, sanitize_manim_code, ) MEDIA_ROOT = Path("media/videos") JOBS_ROOT = Path("jobs") def run_pipeline( job_id: str, pdf_path: str, email: str, gemini_api_key: str, status_cb: Callable[[State, str, str | None], None], ) -> None: job_dir = JOBS_ROOT / job_id script_path = job_dir / f"{job_id}.py" video_path = MEDIA_ROOT / job_id / "720p30" / "OutputVideo.mp4" job_dir.mkdir(parents=True, exist_ok=True) try: # 1. Extract PDF status_cb(State.RUNNING, "📖 Extracting text from PDF…", None) pdf_text = extract_pdf_text(pdf_path) if not pdf_text.strip(): raise ValueError("PDF appears empty or has no selectable text.") # 2. Generate Manim code via Gemini status_cb(State.RUNNING, "🤖 Generating Manim code with Gemini…", None) prompt = _build_prompt(pdf_text) raw_code = generate_manim_code(prompt, gemini_api_key) manim_code = sanitize_manim_code(raw_code, job_id) # Surface generated code to UI status_cb(State.RUNNING, "✏️ Manim code generated — starting render…", manim_code) script_path.write_text(manim_code, encoding="utf-8") # 3. Render status_cb(State.RUNNING, "🎬 Rendering animation (this may take a few minutes)…", None) _render_manim(script_path, job_id) if not video_path.exists(): raise FileNotFoundError(f"Rendered video not found at {video_path}") # 4. Deliver status_cb(State.SENDING, "📧 Sending video to your inbox…", None) _deliver_video(str(video_path), email, status_cb) finally: _cleanup(job_dir, video_path) # ── Helpers ─────────────────────────────────────────────────────────────────── def _build_prompt(pdf_text: str) -> str: truncated = pdf_text[:12_000] return textwrap.dedent(f""" You are an expert Manim animator. Given the following document content, create a concise, visually engaging Manim animation that summarises the key ideas. Use ONLY the class name `OutputVideo` extending `Scene`. Requirements: - Class name MUST be exactly `OutputVideo(Scene)`. - Use only standard Manim Community v0.18+ API. - Output ONLY valid Python code. No explanations, no markdown fences. - Keep runtime under 90 seconds. - Avoid custom LaTeX; prefer Text() over MathTex() where possible. - Animations should be clear, readable, and professional. Document content: --- {truncated} --- """).strip() def _render_manim(script_path: Path, job_id: str, max_retries: int = 2) -> None: cmd = [ "manim", str(script_path), "OutputVideo", "-qm", "--media_dir", "media", "--disable_caching", ] for attempt in range(max_retries + 1): result = subprocess.run(cmd, capture_output=True, text=True, timeout=600) if result.returncode == 0: return if attempt < max_retries: time.sleep(5 * (attempt + 1)) raise RuntimeError( f"Manim render failed after {max_retries + 1} attempts.\n" f"STDERR: {result.stderr[-3000:]}" ) def _deliver_video( video_path: str, email: str, status_cb: Callable, ) -> None: file_size_mb = Path(video_path).stat().st_size / (1024 * 1024) if file_size_mb <= 24: try: send_video_email(email, video_path) return except Exception as exc: status_cb(State.UPLOADING, f"⚠️ Attachment failed ({exc}) — uploading to Catbox…", None) else: status_cb(State.UPLOADING, f"📦 {file_size_mb:.1f} MB video — uploading to Catbox…", None) url = upload_to_catbox(video_path) send_video_email(email, video_path=None, catbox_url=url) def _cleanup(*paths) -> None: for p in paths: if p is None: continue path = Path(p) try: if path.is_dir(): shutil.rmtree(path, ignore_errors=True) elif path.is_file(): path.unlink(missing_ok=True) except Exception: pass