Spaces:
Running on Zero
Running on Zero
| """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: | |
| 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 '<div class="status-pill ready">MODEL CORE: READY<br>ZEROGPU LOADS ON DEMAND</div>' | |
| 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 | |
| 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""" | |
| <main id="sci-shell"> | |
| <section class="cyber-hero"> | |
| <div class="brand-row"> | |
| <div> | |
| <div class="kicker">Physics & Mathematics Virtual Animation Lab</div> | |
| <h1 class="cyber-title">SciVisual-Agent</h1> | |
| <p class="cyber-subtitle"> | |
| Turn maths and physics ideas into <b>accurate</b> animated Manim videos. | |
| </p> | |
| </div> | |
| </div> | |
| </section> | |
| """ | |
| ) | |
| 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('<div class="examples-row">Status bus: prompt -> model adapter code -> temp_scene.py -> Manim render -> MP4 viewport</div>') | |
| with gr.Column(scale=7, elem_classes=["panel-right"]): | |
| gr.HTML('<div class="panel-title">Output Observatory</div>') | |
| 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("</main>") | |
| 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() | |