Spaces:
Running
Running
File size: 5,081 Bytes
246b32e | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 | """
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,
)
|