programmersd's picture
Upload app.py
246b32e verified
"""
PDF → Manim Animation Pipeline
Hugging Face Spaces — Gradio 6.x
"""
import asyncio
import atexit
import queue
import threading
import uuid
import gradio as gr
from queue_manager import JobQueue, State
from pipeline import run_pipeline
def _cleanup_event_loop():
try:
loop = asyncio.get_event_loop()
if not loop.is_closed():
loop.close()
except Exception:
pass
atexit.register(_cleanup_event_loop)
job_queue = JobQueue(max_workers=8, max_jobs=100)
def submit_and_stream(pdf_file, api_key: str):
"""
Generator yielding exactly 4 values per tick:
status_md (str)
code_box (gr.update)
video_player (gr.update)
zip_download (gr.update)
"""
def _err(msg):
return (
msg,
gr.update(visible=False),
gr.update(visible=False),
gr.update(visible=False),
)
if pdf_file is None:
yield _err("❌ Please upload a PDF file.")
return
if not api_key or len(api_key) < 10:
yield _err("❌ Please enter a valid Gemini API key.")
return
if job_queue.is_full():
yield _err("⚠️ Queue is full (max 100 jobs). Please try again shortly.")
return
job_id = uuid.uuid4().hex
pdf_path = pdf_file.name
job_queue.register(job_id)
update_q: queue.Queue = queue.Queue()
def status_cb(state: State, message: str = "", code: str | None = None):
update_q.put((state, message, code))
result_holder: dict = {}
def _run():
try:
result = run_pipeline(
job_id=job_id,
pdf_path=pdf_path,
gemini_api_key=api_key,
status_cb=status_cb,
)
result_holder.update(result)
update_q.put((State.DONE, "✅ Render complete!", result.get("code")))
except Exception as exc:
update_q.put((State.FAILED, f"❌ {exc}", None))
finally:
update_q.put(None)
threading.Thread(target=_run, daemon=True).start()
icons = {State.QUEUED: "⏳", State.RUNNING: "⚙️", State.DONE: "✅", State.FAILED: "❌"}
code_so_far = ""
# Initial tick
yield (
f"⏳ **Queued** — Starting…\n\n*Job `{job_id}`*",
gr.update(visible=False),
gr.update(visible=False),
gr.update(visible=False),
)
while True:
item = update_q.get()
if item is None:
break
state, message, code = item
if code:
code_so_far = code
status_text = f"{icons.get(state,'❓')} **{state.value.title()}** — {message}\n\n*Job `{job_id}`*"
is_done = state == State.DONE
yield (
status_text,
gr.update(value=code_so_far, visible=bool(code_so_far)),
gr.update(value=result_holder.get("video_path") if is_done else None, visible=is_done),
gr.update(value=result_holder.get("zip_path") if is_done else None, visible=is_done),
)
# ── UI ────────────────────────────────────────────────────────────────────────
with gr.Blocks(title="PDF → Manim Video") as demo:
gr.Markdown("# 🎬 PDF → Manim Animation Pipeline\nUpload a PDF and get a downloadable Manim animation.")
saved_api_key = gr.BrowserState("")
with gr.Row():
with gr.Column(scale=1):
pdf_input = gr.File(label="📄 Upload PDF", file_types=[".pdf"])
api_key_input = gr.Textbox(
label="🔑 Gemini API Key",
placeholder="AIza…",
type="password",
info="Saved in your browser — enter once.",
)
submit_btn = gr.Button("🚀 Generate Video", variant="primary")
with gr.Column(scale=1):
status_md = gr.Markdown("*Submit a job to see live status here.*")
code_box = gr.Code(label="📝 Generated Manim Code", language="python", visible=False, interactive=False)
video_player = gr.Video(label="🎬 Rendered Animation", visible=False, interactive=False)
zip_download = gr.File(label="⬇️ Download Artifacts (.py + .mp4)", visible=False, interactive=False)
gr.Markdown("---\n**Notes:** Processing takes 2–5 minutes. The artifacts ZIP contains the `.py` source and rendered `.mp4`. Your API key is never stored server-side.")
demo.load(fn=lambda k: k, inputs=[saved_api_key], outputs=[api_key_input])
api_key_input.change(fn=lambda v: v, inputs=[api_key_input], outputs=[saved_api_key])
submit_btn.click(
fn=submit_and_stream,
inputs=[pdf_input, api_key_input],
outputs=[status_md, code_box, video_player, zip_download],
)
if __name__ == "__main__":
demo.launch(
theme=gr.themes.Soft(),
ssr_mode=False,
server_name="0.0.0.0",
server_port=7860,
)