""" utils.py — PDF extraction, Gemini LLM, email, Catbox upload helpers. """ from __future__ import annotations import os import re import smtplib import ssl import time from email.message import EmailMessage from pathlib import Path import requests from google import genai from google.genai import types # ── PDF Text Extraction ─────────────────────────────────────────────────────── def extract_pdf_text(pdf_path: str) -> str: """Extract plain text from a PDF using pypdf.""" from pypdf import PdfReader reader = PdfReader(pdf_path) pages = [] for page in reader.pages: text = page.extract_text() if text: pages.append(text) return "\n\n".join(pages) # ── Gemini LLM ──────────────────────────────────────────────────────────────── def generate_manim_code(prompt_text: str, api_key: str) -> str: """Stream Manim code from Gemini.""" client = genai.Client(api_key=api_key) model = "gemini-2.5-flash-preview-05-20" # latest stable flash model contents = [ types.Content(role="user", parts=[types.Part.from_text(prompt_text)]) ] config = types.GenerateContentConfig( thinking_config=types.ThinkingConfig(thinking_budget=8192) ) code = "" for chunk in client.models.generate_content_stream( model=model, contents=contents, config=config ): if chunk.text: code += chunk.text return code def sanitize_manim_code(raw: str, job_id: str) -> str: """ Strip markdown fences, ensure correct imports and class name, and add job-specific media dir hint. """ # Remove ```python ... ``` fences code = re.sub(r"^```(?:python)?\s*", "", raw.strip(), flags=re.MULTILINE) code = re.sub(r"\s*```$", "", code.strip(), flags=re.MULTILINE) # Ensure manim import is present if "from manim import" not in code and "import manim" not in code: code = "from manim import *\n\n" + code # Ensure class is named OutputVideo code = re.sub(r"class\s+\w+\s*\(\s*Scene\s*\)", "class OutputVideo(Scene)", code) return code # ── Email ───────────────────────────────────────────────────────────────────── def send_video_email( to_email: str, video_path: str | None = None, catbox_url: str | None = None, ) -> None: """Send email with video attachment or Catbox link.""" smtp_email = os.environ["SMTP_EMAIL"] smtp_password = os.environ["SMTP_PASSWORD"] msg = EmailMessage() msg["Subject"] = "🎬 Your Manim Animation is Ready!" msg["From"] = smtp_email msg["To"] = to_email if catbox_url: body = ( "Your animated video has been generated and uploaded.\n\n" f"Download it here (link valid for ~3 days):\n{catbox_url}\n\n" "Enjoy your animation!" ) msg.set_content(body) else: msg.set_content( "Your animated video is attached to this email.\n\nEnjoy your animation!" ) with open(video_path, "rb") as f: msg.add_attachment( f.read(), maintype="video", subtype="mp4", filename="animation.mp4", ) context = ssl.create_default_context() _smtp_send_with_retry(msg, smtp_email, smtp_password, context) # Delete video after successful send if video_path and Path(video_path).exists(): Path(video_path).unlink(missing_ok=True) def _smtp_send_with_retry( msg: EmailMessage, smtp_email: str, smtp_password: str, context: ssl.SSLContext, max_retries: int = 3, ) -> None: backoff = 2 for attempt in range(max_retries): try: with smtplib.SMTP_SSL("smtp.gmail.com", 465, context=context) as server: server.login(smtp_email, smtp_password) server.send_message(msg) return except smtplib.SMTPException as exc: if attempt == max_retries - 1: raise time.sleep(backoff) backoff *= 2 # ── Catbox Upload ───────────────────────────────────────────────────────────── API_URL = "https://catbox.moe/user/api.php" def upload_to_catbox(path: str, max_retries: int = 5) -> str: """Upload a file to Catbox.moe with exponential backoff.""" file_path = Path(path) backoff = 1 for attempt in range(max_retries): try: with file_path.open("rb") as f: r = requests.post( API_URL, data={"reqtype": "fileupload"}, files={"fileToUpload": f}, timeout=120, ) if r.status_code == 200 and r.text.startswith("https://files.catbox.moe/"): return r.text.strip() raise RuntimeError(f"Catbox returned unexpected response: {r.text[:200]}") except Exception: if attempt == max_retries - 1: raise time.sleep(backoff) backoff *= 2 raise RuntimeError("Catbox upload failed after all retries.")