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,
    )