"""SciVisual-Agent Hugging Face Space entrypoint.""" from __future__ import annotations import html import traceback from typing import Tuple import gradio as gr from generator import build_default_agent try: import spaces except ImportError: # Local development fallback when not running on HF ZeroGPU. class _SpacesFallback: @staticmethod def GPU(*args, **kwargs): def decorator(func): return func return decorator spaces = _SpacesFallback() agent = None custom_css = """ @import url('https://fonts.googleapis.com/css2?family=Share+Tech+Mono&display=swap'); :root { --cyber-bg: #0b0f19; --cyber-panel: rgba(15, 24, 42, 0.72); --cyber-panel-strong: rgba(4, 10, 20, 0.86); --cyber-cyan: #00ffcc; --cyber-pink: #ff0055; --cyber-blue: #3b82f6; --cyber-text: #e6fff9; --cyber-muted: #8aa8b3; --cyber-line: rgba(0, 255, 204, 0.35); } * { box-sizing: border-box; letter-spacing: 0; } body, .gradio-container { min-height: 100vh; margin: 0; color: var(--cyber-text) !important; font-family: 'Share Tech Mono', 'Courier New', monospace !important; background: linear-gradient(rgba(0, 255, 204, 0.035) 1px, transparent 1px), linear-gradient(90deg, rgba(0, 255, 204, 0.035) 1px, transparent 1px), radial-gradient(circle at 18% 18%, rgba(0, 255, 204, 0.16), transparent 28%), radial-gradient(circle at 82% 10%, rgba(255, 0, 85, 0.11), transparent 30%), #0b0f19 !important; background-size: 34px 34px, 34px 34px, auto, auto, auto !important; } .gradio-container { max-width: none !important; padding: 0 !important; } footer, .footer, .built-with { display: none !important; } #sci-shell { margin: 0 auto; padding: 18px 0 26px; } .cyber-hero { position: relative; min-height: 128px; padding: 18px 22px; border: 1px solid var(--cyber-line); background: linear-gradient(135deg, rgba(0, 255, 204, 0.12), rgba(255, 0, 85, 0.06) 52%, rgba(59, 130, 246, 0.1)); box-shadow: 0 0 34px rgba(0, 255, 204, 0.12), inset 0 0 24px rgba(0, 255, 204, 0.08); overflow: hidden; } .cyber-hero::before { content: ""; position: absolute; inset: 0; background: repeating-linear-gradient(0deg, transparent 0 9px, rgba(0, 255, 204, 0.045) 10px); pointer-events: none; animation: scan 6s linear infinite; } @keyframes scan { from { transform: translateY(-18px); } to { transform: translateY(18px); } } .brand-row { position: relative; display: flex; justify-content: space-between; gap: 18px; align-items: flex-start; } .kicker { color: var(--cyber-cyan); font-size: 13px; text-transform: uppercase; } .cyber-title { margin: 4px 0 8px; color: #ffffff; font-size: clamp(34px, 5vw, 74px); line-height: 0.92; text-shadow: 0 0 16px rgba(0, 255, 204, 0.75), 2px 0 0 rgba(255, 0, 85, 0.55); } .cyber-subtitle { max-width: 900px; margin: 0; color: var(--cyber-muted); font-size: 16px; } .status-pill { flex: 0 0 auto; min-width: 210px; padding: 10px 12px; border: 1px solid rgba(255, 0, 85, 0.55); background: rgba(255, 0, 85, 0.08); color: var(--cyber-text); box-shadow: 0 0 18px rgba(255, 0, 85, 0.18); } .status-pill.ready { border-color: rgba(0, 255, 204, 0.6); background: rgba(0, 255, 204, 0.08); box-shadow: 0 0 18px rgba(0, 255, 204, 0.18); } .panel-title { margin: 0 0 12px; color: var(--cyber-cyan); font-size: 14px; text-transform: uppercase; border-bottom: 1px solid var(--cyber-line); padding-bottom: 8px; } .dashboard-grid { gap: 18px !important; margin-top: 18px; } .panel-left, .panel-right { position: relative; padding: 16px !important; border: 1px solid var(--cyber-line); background: var(--cyber-panel); backdrop-filter: blur(18px); box-shadow: 0 0 30px rgba(0, 255, 204, 0.10), inset 0 0 24px rgba(255, 255, 255, 0.025); } .panel-right { min-height: 710px; } .panel-left::after, .panel-right::after { content: ""; position: absolute; top: -1px; right: 16px; width: 70px; height: 2px; background: var(--cyber-pink); box-shadow: 0 0 12px var(--cyber-pink); } .module-strip { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; margin-bottom: 12px; } .module-strip span { display: block; padding: 8px; border: 1px solid rgba(0, 255, 204, 0.22); background: rgba(0, 0, 0, 0.22); color: var(--cyber-muted); font-size: 12px; text-align: center; } .viewport-frame { position: relative; margin-bottom: 12px; padding: 10px; border: 1px solid rgba(0, 255, 204, 0.45); background: linear-gradient(180deg, rgba(0, 255, 204, 0.05), rgba(255, 0, 85, 0.035)); box-shadow: inset 0 0 22px rgba(0, 255, 204, 0.08); } .viewport-frame::after { content: ""; position: absolute; inset: 10px; pointer-events: none; border: 1px solid rgba(255, 0, 85, 0.26); } .gr-form, .block, .form, .wrap, .contain { background: transparent !important; border: 0 !important; box-shadow: none !important; } label, .label-wrap span { color: var(--cyber-cyan) !important; font-family: 'Share Tech Mono', 'Courier New', monospace !important; text-transform: uppercase; } textarea, input, select, .input, .gr-text-input, .gradio-dropdown, .token, .wrap textarea { color: var(--cyber-text) !important; background: rgba(2, 6, 16, 0.88) !important; border: 1px solid rgba(0, 255, 204, 0.48) !important; border-radius: 0 !important; box-shadow: inset 0 0 14px rgba(0, 255, 204, 0.06), 0 0 12px rgba(0, 255, 204, 0.08) !important; font-family: 'Share Tech Mono', 'Courier New', monospace !important; } textarea:focus, input:focus { border-color: var(--cyber-pink) !important; box-shadow: 0 0 18px rgba(255, 0, 85, 0.25), inset 0 0 16px rgba(0, 255, 204, 0.08) !important; } button, .gr-button { min-height: 44px !important; border: 1px solid var(--cyber-cyan) !important; border-radius: 0 !important; color: #001713 !important; background: linear-gradient(90deg, var(--cyber-cyan), #78ffe7) !important; box-shadow: 0 0 18px rgba(0, 255, 204, 0.35) !important; font-family: 'Share Tech Mono', 'Courier New', monospace !important; font-weight: 700 !important; text-transform: uppercase !important; transition: transform 140ms ease, box-shadow 140ms ease, filter 140ms ease !important; } button:hover, .gr-button:hover { transform: translateY(-1px); filter: brightness(1.08); box-shadow: 0 0 26px rgba(0, 255, 204, 0.55) !important; } #clear-btn button { color: var(--cyber-text) !important; border-color: var(--cyber-pink) !important; background: rgba(255, 0, 85, 0.12) !important; box-shadow: 0 0 16px rgba(255, 0, 85, 0.22) !important; } #terminal textarea { min-height: 220px !important; color: #9fffe9 !important; background: linear-gradient(rgba(0, 255, 204, 0.03) 50%, rgba(255, 0, 85, 0.025) 50%), rgba(0, 0, 0, 0.72) !important; background-size: 100% 8px !important; font-size: 13px !important; } #source-code textarea { min-height: 430px !important; color: #d9fff8 !important; font-size: 13px !important; overflow-y: scroll !important; } .tabs, .tab-nav, .tabitem { background: transparent !important; border-color: rgba(0, 255, 204, 0.25) !important; } .tab-nav button { color: var(--cyber-text) !important; background: rgba(3, 10, 20, 0.78) !important; border: 1px solid rgba(0, 255, 204, 0.26) !important; } .tab-nav button.selected { color: #001713 !important; background: var(--cyber-cyan) !important; } #render-video { border: 0 !important; background: #020610 !important; } #render-video video { width: 100% !important; max-height: 560px !important; object-fit: contain !important; background: #020610 !important; } .examples-row { margin-top: 12px; color: var(--cyber-muted); font-size: 12px; } #quick-examples .wrap { gap: 8px !important; } #quick-examples .wrap label { position: relative !important; min-height: 38px !important; padding: 9px 40px 9px 12px !important; border: 1px solid rgba(0, 255, 204, 0.22) !important; background: rgba(2, 6, 16, 0.58) !important; color: var(--cyber-muted) !important; transition: border-color 140ms ease, box-shadow 140ms ease, color 140ms ease, background 140ms ease !important; } #quick-examples .wrap label:hover { border-color: rgba(0, 255, 204, 0.55) !important; color: var(--cyber-text) !important; } #quick-examples .wrap label:has(input[type="radio"]:checked) { border-color: var(--cyber-cyan) !important; background: linear-gradient(90deg, rgba(0, 255, 204, 0.16), rgba(255, 0, 85, 0.08)) !important; color: #ffffff !important; box-shadow: 0 0 18px rgba(0, 255, 204, 0.20), inset 0 0 18px rgba(0, 255, 204, 0.06) !important; } #quick-examples .wrap label:has(input[type="radio"]:checked)::after { content: "✓"; position: absolute; right: 12px; top: 50%; transform: translateY(-50%); color: var(--cyber-cyan); font-size: 18px; text-shadow: 0 0 10px rgba(0, 255, 204, 0.75); } @media (max-width: 900px) { #sci-shell { width: calc(100vw - 16px); padding-top: 8px; } .brand-row { flex-direction: column; } .status-pill { width: 100%; } .module-strip { grid-template-columns: 1fr; } .panel-right { min-height: auto; } } """ def startup_status_html() -> str: return '
MODEL CORE: READY
ZEROGPU LOADS ON DEMAND
' def initial_terminal_log() -> str: return ( "[boot] SciVisual-Agent interface online\n" "[model] Adapter loads on demand inside the ZeroGPU render callback\n" "[render] Waiting for prompt" ) def get_agent(): global agent if agent is None: agent = build_default_agent() return agent @spaces.GPU(duration=300) def run_visual_lab(prompt: str, retries: int) -> Tuple[str | None, str, str]: try: visual_agent = get_agent() success, video_path, code, log = visual_agent.generate_and_fix( user_prompt=prompt, domain="Mathematics", max_retries=int(retries), ) status = "[complete] Video render available" if success else "[failed] Render unavailable" return video_path if success else None, code, f"{log}\n{status}" except Exception: error_log = traceback.format_exc() return None, "", f"[fatal] Application exception\n{error_log}" def load_example(example: str) -> str: examples = { "Spring Mass": """Create a professional 2D physics simulation of a horizontal spring-mass system with a real-time synchronized position graph using ManimCE. Fix the velocity easing issue. Requirements: 1. Layout & Axes: - Shift the entire Spring-Mass system to the upper half (around y = 1.5). - In the lower half (centered at y = -2), create a coordinate system `Axes` using `Axes(x_range=[0, 7, 1], y_range=[-2.5, 2.5, 1], x_length=7, y_length=3)`. Color the axes WHITE and add labels "Time (t)" and "Position (x)". 2. Physical Objects: - Vertical wall at x = -4 (from y = 0.5 to y = 2.5), color = WHITE. - Horizontal track from x = -4 to x = 4 at y = 1.2, color = WHITE. - Mass block using `Square(side_length=0.6, color=PURPLE, fill_opacity=0.8)`. - Use a `ValueTracker(0)` for time `t`. - Update the block's center position via `add_updater` to be exactly at `np.array([2 * np.cos(3 * t.get_value()), 1.5, 0])`. - Create the spring using `always_redraw`. Calculate `L = mass.get_left()[0] - (-4)`. Use `ParametricFunction` with `t_range=[0, 1]` where: * x_func(u) = -4 + u * L * y_func(u) = 1.5 + 0.3 * np.sin(2 * np.pi * 8 * u) Color the spring WHITE. 3. Graph Synchronization: - Create an orange dynamic dot on the graph tracker using `add_updater` at `axes.c2p(t.get_value(), 2 * np.cos(3 * t.get_value()), 0)`. - Create the graph line using `always_redraw` plotting `axes.plot(lambda time: 2 * np.cos(3 * time), x_range=[0, max(0.001, t.get_value())], color=ORANGE)`. 4. Strict Animation Speed (CRITICAL SPEED FIX): - Animate the tracker `t` from 0 to 2*PI using `self.play(t.animate.set_value(2 * PI), run_time=6, rate_func=linear)`. - YOU MUST EXPLICITLY INCLUDE `rate_func=linear` to eliminate the default smooth easing effect. This ensures that time 't' updates at a perfectly constant velocity, making the physical oscillation and graph rendering completely uniform from start to finish.""", "Orbit": """Create a beautiful, scientifically accurate 2D physics animation of a satellite in an elliptical orbit around Earth using ManimCE. Requirements: 1. Scaling & Geometry (CRITICAL to avoid overlapping): - Represent Earth as a clear sphere or circle in the center, but keep its radius small (e.g., radius=0.6) so it doesn't swallow the orbit. - Create an explicit, highly elongated elliptical orbit path (`Ellipse(width=6.0, height=3.5)`) shifted slightly so that Earth sits exactly at one of the focal points (Foci) of the ellipse, NOT at the geometric center (Kepler's First Law). 2. Dynamic Vectors & Updaters: - Represent the satellite as a distinct, colored Dot (e.g., Cyber Cyan) moving along the elliptical path using a custom tracker or `ValueTracker` for the orbital angle. - Create two dynamic arrows (`Vector` or `Arrow`) attached to the satellite dot: * Gravitational Force Vector (Color: Neon Red): Must always point directly from the satellite's current position to Earth's center. Its length must dynamically increase when close to Earth and decrease when far away (Inverse-square law). * Velocity Vector (Color: Bright Green): Must always be perfectly tangent to the elliptical orbit path in the direction of motion. Its length must dynamically represent orbital speed (faster at perigee, slower at apogee). - Use `add_updater` on both vectors so their positions, directions, and lengths update smoothly at every single frame based on the satellite's position. 3. Labels & Overlay: - Add text labels for "Velocity (v)" and "Gravity (Fg)" matching the vector colors. - In the top-right corner, add a LaTeX mathematical text displaying Newton's Law of Universal Gravitation: "F_g = G * (M_1 * M_2) / r²". 4. Animation flow: Run the animation for 2 complete orbital periods (about 10-12 seconds) so the viewer can clearly observe the dramatic speeding up at the close approach and slowing down at the far end. Wrap everything inside a single clean Scene class.""", "Simple Pendulum": """Create a high-quality Physics simulation of a Damped Simple Pendulum using ManimCE. Requirements: 1. Physics Setup: Define explicit variables for gravitational acceleration (g=9.81), rod length (L=3.0), initial angle (theta = pi/4), and a damping coefficient (b=0.15) to simulate real-world air resistance. Use a clear numerical integration method (like Euler-Cromer) inside an object updater (`add_updater`) to dynamically recalculate the angular velocity and displacement at every frame (`dt`). 2. Visual Elements (Strict Coordinate Updates): - A fixed ceiling line or pivot dot at the top center. - A colored dot (e.g., Cyber Cyan or Neon Pink) representing the heavy bob. - A clean line representing the pendulum rod that attaches the pivot to the bob. CRITICAL: You must attach an updater to this rod using `add_updater` so that its end position dynamically calls `rod.put_start_and_end_on(pivot.get_center(), bob.get_center())` at every single frame. The rod must always move dynamically with the bob. - Add an elegant fading trace/trail (`TracedPath`) attached to the bob to visually map its decaying sinusoidal path across the screen over time. 3. Mathematical Overlay: In the upper left corner, display the differential equation governing the motion: "d²θ/dt² + (b/m)dθ/dt + (g/L)sin(θ) = 0" rendered beautifully via LaTeX. 4. Animation flow: Let the simulation run smoothly for 12 seconds to clearly demonstrate the kinetic energy converting to thermal energy as the oscillation slowly dampens to a complete halt. Ensure the code is self-contained and wrapped inside a single executable Scene class.""", "Visual proof (a+b)² = a² + 2ab + b²": """Create a perfect 2D geometric animation proving the identity (a+b)² = a² + 2ab + b² using ManimCE based on strict quadrant alignment. Fix the overlapping bug. Requirements: 1. Geometry & Scale Setup: - Define lengths: a = 2.0 and b = 1.0. - Define a central intersection origin point: `origin = np.array([0, 0, 0])`. All blocks must be strictly positioned relative to this point by alignment. 2. Precise Corner Alignment (CRITICAL OVERLAP FIX): - Block 1 (Square a²): Create a Square with side_length=a, color=BLUE. Use `.next_to(origin, UL, buff=0)` or explicitly align its bottom-right corner to `origin` so it sits entirely in the Upper-Left quadrant. - Block 2 (Square b²): Create a Square with side_length=b, color=PINK. Use `.next_to(origin, DR, buff=0)` or explicitly align its top-left corner to `origin` so it sits entirely in the Lower-Right quadrant. - Block 3 (Rectangle ab - Top Right): Create a Rectangle with width=b, height=a, color=GREEN. Use `.next_to(origin, UR, buff=0)` or align its bottom-left corner to `origin` so it sits entirely in the Upper-Right quadrant. - Block 4 (Rectangle ab - Bottom Left): Create a Rectangle with width=a, height=b, color=GREEN. Use `.next_to(origin, DL, buff=0)` or align its top-right corner to `origin` so it sits entirely in the Lower-Left quadrant. - Set fill_opacity=0.5 and stroke_color=WHITE for all 4 blocks. 3. Main Outer Square Framework: - Create a large outer Square with side_length=(a+b) to represent the final boundary. Center it precisely at the combined visual center of the 4 blocks, which is `np.array([(-a+b)/2, (a-b)/2, 0])`. Color it WHITE with a thin stroke width, without fill. 4. Text Labels & Math Mapping: - Place MathTex text labels ("a²", "b²", "ab", "ab") centered inside each corresponding block using `.move_to(block.get_center())`. Note: Labels must be created and positioned AFTER the blocks have been moved to their correct quadrants. - Place the formula "(a + b)² = a² + 2ab + b²" at the top edge of the screen. 5. Animation Sequence: - Step 1: Write the formula at the top and create the thin outer main square framework first (1.5s). - Step 2: Draw Block 1 (a²) and Block 2 (b²) simultaneously using `FadeIn` (1s). - Step 3: Animate the two Rectangle blocks (ab) creating themselves using `Create` to fill the remaining empty corners (1.5s). - Step 4: Display the labels inside each block using `Write`. 6. Constraint: Keep the code clean, modular, and under 5 seconds total run_time. Do not use random pixel offsets; strictly use `next_to` with `buff=0` for perfect grid locking.""", "Fourier Series" : """Animate the geometric construction of a square wave using its first 7 Fourier components (odd harmonics: n=1,3,5,7,9,11,13) in ManimCE. Fix the always_redraw compilation conflict. Requirements: 1. Math Equations: - Base square wave graph: `axes.plot(lambda x: np.sign(np.sin(x)), color=RED)`. (Do NOT wrap this static red wave in always_redraw). - Fourier mathematical sum formula inside the loop: `sum((4 / (n * PI)) * np.sin(n * x) for n in range(1, 2 * int(N_tracker.get_value()), 2))`. 2. Correct Animation Sequence (CRITICAL GRAPHICS FIX): - Step 1: Add the Axes and the static red square wave directly to the scene using `self.add(axes, square_wave)`. - Step 2: Create a STATIC fundamental sine wave curve (where N=1 hardcoded) using `fourier_base = axes.plot(lambda x: (4 / PI) * np.sin(x), color=YELLOW)`. Animate this static line with `self.play(Create(fourier_base), run_time=1.0)`. This guarantees a beautiful, bug-free drawing effect from left to right in the first second. - Step 3: Initialize `N_tracker = ValueTracker(1)`. Now, introduce the dynamic morphing curve using `always_redraw`. Inside its lambda, it must calculate the Fourier sum based on `N_tracker.get_value()`. - Step 4: Use `self.add(fourier_dynamic)` to overlay it, remove the static `fourier_base`, and immediately animate `N_tracker` from 1 to 7 using `self.play(N_tracker.animate.set_value(7), run_time=4.5, rate_func=linear)`. 3. Ensure no while-loops are used. The transition between the static line and the morphing line must be seamless.""", } return examples.get(example, "") with gr.Blocks(css=custom_css, title="SciVisual-Agent") as demo: gr.HTML( f"""
Physics & Mathematics Virtual Animation Lab

SciVisual-Agent

Turn maths and physics ideas into accurate animated Manim videos.

""" ) with gr.Row(elem_classes=["dashboard-grid"]): with gr.Column(scale=5, elem_classes=["panel-left"]): prompt = gr.Textbox( label="Describe the animation", placeholder="Please enter your animation description or select one of the examples below.", lines=8, max_lines=14, interactive=True, ) retries = gr.Slider( minimum=0, maximum=5, value=3, step=1, label="Self-Correction Retries", interactive=True, ) with gr.Row(): generate_btn = gr.Button("Render", variant="primary") clear_btn = gr.Button("Clear", elem_id="clear-btn") example_choice = gr.Radio( choices=["Simple Pendulum", "Spring Mass", "Visual proof (a+b)² = a² + 2ab + b²", "Fourier Series"], label="Quick Examples", value=None, interactive=True, elem_id="quick-examples", ) terminal = gr.Textbox( label="System Terminal Log", value=initial_terminal_log(), lines=12, max_lines=18, interactive=False, elem_id="terminal", ) gr.HTML('
Status bus: prompt -> model adapter code -> temp_scene.py -> Manim render -> MP4 viewport
') with gr.Column(scale=7, elem_classes=["panel-right"]): gr.HTML('
Output Observatory
') with gr.Tabs(): with gr.Tab("Rendered Video"): video = gr.Video(label=None, elem_id="render-video", height=560, autoplay=True, loop=True) with gr.Tab("Generated Code"): source = gr.Textbox( label="Clean Python Source", lines=24, max_lines=34, interactive=False, elem_id="source-code", ) gr.HTML("
") generate_btn.click( fn=run_visual_lab, inputs=[prompt, retries], outputs=[video, source, terminal], show_progress=True, ) clear_btn.click( fn=lambda: ("", None, "", initial_terminal_log()), inputs=None, outputs=[prompt, example_choice, source, terminal], ) example_choice.change(fn=load_example, inputs=example_choice, outputs=prompt) if __name__ == "__main__": demo.launch()