Spaces:
Running
Running
| """ | |
| 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, | |
| ) | |