SOONIMATOR / app.py
AlekseyCalvin's picture
Update app.py
79c13b7 verified
import gradio as gr
import subprocess
import tempfile
import os
import re
import sys
from pathlib import Path
import time
import json
from typing import List, Tuple, Optional, Dict
import concurrent.futures
from datetime import datetime
import shutil
# ============================================
# CRITICAL: Persistent output directory
# ============================================
PERSISTENT_OUTPUT_DIR = Path("/tmp/manim_outputs")
PERSISTENT_OUTPUT_DIR.mkdir(exist_ok=True)
SUPPORTED_SCENE_TYPES = [
'Scene', 'ThreeDScene', 'MovingCameraScene', 'ZoomedScene',
'LinearTransformationScene', 'VectorScene', 'ComplexPlane',
'SpecialThreeDScene', 'ThreeDSlide', 'Slide'
]
class LaTeXManager:
"""Handle LaTeX properly with PyTinyTeX and fallbacks"""
def __init__(self):
self.latex_available = self._check_latex_availability()
self.pytinytex_available = self._check_pytinytex()
self.setup_complete = False
if self.pytinytex_available and not self.latex_available:
self._setup_pytinytex()
def _check_latex_availability(self) -> bool:
"""Check if ANY LaTeX is available"""
try:
for compiler in ['pdflatex', 'xelatex', "texlive", 'lualatex', 'latex']:
result = subprocess.run(
[compiler, '--version'],
capture_output=True, text=True, timeout=5
)
if result.returncode == 0:
print(f"✅ Found LaTeX: {compiler}")
return True
except:
pass
try:
import pytinytex
tinytex_dir = Path(pytinytex.TINYTEX_DIR)
if tinytex_dir.exists():
latex_bin = tinytex_dir / "bin" / "x86_64-linux" / "pdflatex"
if latex_bin.exists():
print("✅ PyTinyTeX binaries found")
return True
except:
pass
print("❌ No LaTeX available")
return False
def _check_pytinytex(self) -> bool:
"""Check PyTinyTeX module"""
try:
import pytinytex
print("✅ PyTinyTeX module available")
return True
except ImportError:
print("❌ PyTinyTeX module not found")
return False
def _setup_pytinytex(self):
"""Setup PyTinyTeX correctly"""
try:
import pytinytex
if not Path(pytinytex.TINYTEX_DIR).exists():
print("📥 Downloading PyTinyTeX...")
pytinytex.download()
tinytex_bin = Path(pytinytex.TINYTEX_DIR) / "bin" / "x86_64-linux"
if tinytex_bin.exists():
os.environ["PATH"] = str(tinytex_bin) + os.pathsep + os.environ.get("PATH", "")
print(f"✅ PyTinyTeX added to PATH: {tinytex_bin}")
self.latex_available = self._check_latex_availability()
self.setup_complete = True
else:
print("❌ PyTinyTeX bin directory not found")
except Exception as e:
print(f"❌ PyTinyTeX setup failed: {e}")
def validate_and_fix_code(self, code: str) -> Tuple[str, List[str]]:
"""Fix LaTeX issues in code"""
warnings = []
fixed_code = code
if not self.latex_available:
warnings.append("⚠️ No LaTeX found. Converting MathTex to Text()")
fixed_code = self._convert_mathtex_to_text(fixed_code)
fixed_code = self._convert_tex_to_text(fixed_code)
return fixed_code, warnings
def _convert_mathtex_to_text(self, code: str) -> str:
"""Convert MathTex to Text for SIMPLE expressions only"""
simple_formulas = {
r'MathTex\("E = mc\^2"\)': 'Text("E = mc²")',
r'MathTex\("a\^2 \+ b\^2 = c\^2"\)': 'Text("a² + b² = c²")',
r'MathTex\("e\^\{i\\\\pi\} \+ 1 = 0"\)': 'Text("e^(iπ) + 1 = 0")',
r'MathTex\("\\\\int_\{-\}\^\{\\\\infty\} e\^\{-x\^2\} dx = \\\\sqrt\{\\\\pi\}"\)':
'Text("∫_{-∞}^{∞} e^(-x²) dx = √π")',
}
for pattern, replacement in simple_formulas.items():
code = re.sub(pattern, replacement, code)
return code
def _convert_tex_to_text(self, code: str) -> str:
"""Convert Tex to Text"""
return re.sub(r'Tex\((.*?)\)', r'Text(\1)', code)
# ============================================
# Scene Compatibility FIXER
# ============================================
class SceneCompatibilityFixer:
"""Fix compatibility issues for different scene types"""
def __init__(self):
self.scene_fixes = {
'ThreeDScene': self._fix_three_d_scene_issues,
'SpecialThreeDScene': self._fix_three_d_scene_issues,
'ThreeDSlide': self._fix_three_d_scene_issues,
}
def fix_code_compatibility(self, code: str, scene_type: str) -> str:
"""Apply scene-specific fixes"""
if scene_type in self.scene_fixes:
print(f"🔧 Applying fixes for {scene_type}")
return self.scene_fixes[scene_type](code)
return code
def _fix_three_d_scene_issues(self, code: str) -> str:
"""Fix known ThreeDScene issues"""
fixed_code = code
# CRITICAL FIX: Replace fix_in_frame() calls
fixed_code = re.sub(
r'(\w+)\.fix_in_frame\(\)',
r'self.add_fixed_in_frame_mobjects(\1)',
fixed_code
)
# Fix remaining instances line by line
lines = fixed_code.split('\n')
for i, line in enumerate(lines):
if '.fix_in_frame()' in line and 'self.add_fixed_in_frame_mobjects' not in line:
match = re.search(r'(\w+)\.fix_in_frame\(\)', line)
if match:
obj_name = match.group(1)
indent = len(line) - len(line.lstrip())
lines[i] = ' ' * indent + f'self.add_fixed_in_frame_mobjects({obj_name})'
fixed_code = '\n'.join(lines)
# Add missing imports
if 'from manim import' in fixed_code and 'ThreeDScene' not in fixed_code:
fixed_code = re.sub(
r'from manim import \*',
'from manim import *\nfrom manim import ThreeDScene, Cube, Sphere, Torus',
fixed_code
)
return fixed_code
# ============================================
# Scene Detector
# ============================================
class EnhancedSceneDetector:
"""Detect scenes properly"""
@staticmethod
def extract_all_scenes(code: str) -> List[Dict[str, str]]:
"""Extract all scene classes"""
scenes = []
lines = code.split('\n')
for i, line in enumerate(lines):
line = line.strip()
if line.startswith('class '):
match = re.match(r'class\s+(\w+)\s*\(([^)]+)\):', line)
if match:
class_name = match.group(1)
parent_str = match.group(2)
parents = [p.strip() for p in parent_str.split(',')]
for parent in parents:
for scene_type in SUPPORTED_SCENE_TYPES:
if scene_type in parent:
scenes.append({
'name': class_name,
'type': scene_type,
'line': i + 1
})
break
return scenes
# ============================================
# Main Renderer - CRITICAL FIX: Persistent file storage
# ============================================
class RobustManimRenderer:
"""Ultimate robust renderer with persistent output"""
def __init__(self):
self.latex_manager = LaTeXManager()
self.compatibility_fixer = SceneCompatibilityFixer()
# Ensure persistent directory exists
PERSISTENT_OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
print(f"📁 Persistent output directory: {PERSISTENT_OUTPUT_DIR}")
def prepare_code(self, code: str) -> Tuple[str, List[str]]:
"""Comprehensive code preparation"""
warnings = []
prepared_code = code
scenes = EnhancedSceneDetector.extract_all_scenes(prepared_code)
print(f"📊 Found {len(scenes)} scenes: {[s['name'] for s in scenes]}")
prepared_code, latex_warnings = self.latex_manager.validate_and_fix_code(prepared_code)
warnings.extend(latex_warnings)
for scene_info in scenes:
prepared_code = self.compatibility_fixer.fix_code_compatibility(
prepared_code, scene_info['type']
)
return prepared_code, warnings
def render_scene(self, code: str, scene_name: str, quality: str,
progress_callback=None) -> Tuple[Optional[str], str]:
"""Render a single scene with PERSISTENT file storage"""
prepared_code, warnings = self.prepare_code(code)
# Create temp directory for rendering
temp_dir = tempfile.mkdtemp(prefix=f"manim_{scene_name}_")
try:
if progress_callback:
progress_callback(0.1, f"Saving {scene_name}...")
temp_file = os.path.join(temp_dir, "animation.py")
with open(temp_file, 'w', encoding='utf-8') as f:
f.write(prepared_code)
if progress_callback:
progress_callback(0.2, f"Rendering {scene_name} with {quality} quality...")
quality_map = {
"Low (Fast)": "l", "Medium": "m",
"High": "h", "Production": "p"
}
quality_flag = quality_map.get(quality, "l")
output_dir = os.path.join(temp_dir, "media")
cmd = [
sys.executable, "-m", "manim", "render",
temp_file, scene_name,
"-q", quality_flag,
"--media_dir", output_dir,
"--disable_caching"
]
if quality_flag == "l":
cmd.extend(["--fps", "15"])
if progress_callback:
progress_callback(0.3, f"Executing manim...")
# CRITICAL: Print command for debugging
print(f"🚀 Executing: {' '.join(cmd)}")
result = subprocess.run(
cmd, capture_output=True, text=True,
timeout=600, cwd=temp_dir
)
output_text = result.stdout + "\n" + result.stderr
# CRITICAL: Print output directory contents for debugging
print(f"📂 Output directory contents:")
if os.path.exists(output_dir):
for root, dirs, files in os.walk(output_dir):
for file in files:
print(f" - {os.path.join(root, file)}")
if progress_callback:
progress_callback(0.8, "Finding and saving output...")
if result.returncode != 0:
# Try fallback to low quality
if quality_flag != "l":
progress_callback(0.85, "❌ Failed, trying low quality...")
cmd[6] = "l"
result = subprocess.run(cmd, capture_output=True, text=True, timeout=600)
if result.returncode == 0:
output_text = result.stdout + "\n" + result.stderr
output_text += "\n⚠️ Rendered with low quality fallback"
if result.returncode != 0:
return None, f"❌ Render failed:\n{output_text}"
# CRITICAL FIX: Copy file to persistent location
temp_output_file = self._find_output_file(output_dir, scene_name)
if not temp_output_file:
return None, f"❌ No output file found for {scene_name} in {output_dir}"
# Copy to persistent directory
persistent_file = PERSISTENT_OUTPUT_DIR / f"{scene_name}_{int(time.time())}.mp4"
shutil.copy2(temp_output_file, persistent_file)
print(f"💾 Saved to persistent location: {persistent_file}")
if progress_callback:
progress_callback(1.0, "✅ Complete!")
file_size = os.path.getsize(persistent_file) / (1024 * 1024)
message = f"✅ {scene_name} rendered!\n📁 Size: {file_size:.2f} MB\n💾 Saved to: {persistent_file}"
if warnings:
message += f"\n\n⚠️ Warnings:\n" + "\n".join(warnings[:2])
# Return the PERSISTENT path, not temp path
return str(persistent_file), message
except Exception as e:
import traceback
error_detail = traceback.format_exc()
return None, f"❌ Error: {str(e)}\n\nDetails:\n{error_detail}"
finally:
# Clean up temp directory but NOT the persistent file
try:
shutil.rmtree(temp_dir, ignore_errors=True)
print(f"🗑️ Cleaned temp dir: {temp_dir}")
except:
pass
def render_all_scenes(self, code: str, quality: str,
progress=gr.Progress()) -> Tuple[List[str], str]:
"""Render all scenes with persistent storage"""
scenes = EnhancedSceneDetector.extract_all_scenes(code)
if not scenes:
return [], "❌ No scene classes found!"
# Validate syntax
try:
compile(code, '<string>', 'exec')
except SyntaxError as e:
return [], f"❌ Syntax Error Line {e.lineno}: {e.msg}"
progress(0.05, desc=f"Found {len(scenes)} scene(s)")
output_files = []
messages = []
for i, scene_info in enumerate(scenes):
scene_name = scene_info['name']
progress(
0.2 + (i / len(scenes)) * 0.7,
desc=f"Rendering {scene_name}..."
)
video_file, message = self.render_scene(
code, scene_name, quality,
lambda p, m: progress(
0.2 + (i + p * 0.9) / len(scenes) * 0.7,
desc=f"{scene_name}: {m}"
)
)
if video_file:
output_files.append(video_file)
messages.append(f"✅ {scene_name}")
else:
messages.append(f"❌ {scene_name}: {message}")
final_message = "\n".join(messages)
final_message += f"\n\n📊 Success: {len(output_files)}/{len(scenes)}"
# CRITICAL: Print final file list for debugging
print(f"🎬 Final output files: {output_files}")
return output_files, final_message
def _find_output_file(self, output_dir: str, scene_name: str) -> Optional[str]:
"""Find output file in temp directory"""
for root, dirs, files in os.walk(output_dir):
for file in files:
if file.endswith('.mp4') and scene_name.lower() in file.lower():
return os.path.join(root, file)
# Check for any video file
for root, dirs, files in os.walk(output_dir):
for file in files:
if file.endswith('.mp4'):
return os.path.join(root, file)
return None
# ============================================
# Examples - FIXED versions
# ============================================
FIXED_EXAMPLES = {
"Simple Animation": '''from manim import *
class SimpleAnimation(Scene):
def construct(self):
title = Text("SOONIMATOR Manim Editor!", font_size=48, color=BLUE)
subtitle = Text("All Bugs Fixed Version", font_size=24)
subtitle.next_to(title, DOWN)
# Create shapes
circle = Circle(radius=1, color=RED)
square = Square(side_length=2, color=GREEN)
triangle = Triangle(color=YELLOW)
# Position shapes
circle.shift(LEFT * 3)
square.shift(RIGHT * 3)
triangle.shift(DOWN * 2)
# Animate!
self.play(Write(title), run_time=2)
self.wait(0.5)
self.play(Write(subtitle), run_time=1.5)
self.wait(1)
# Show shapes
self.play(
Create(circle),
Create(square),
Create(triangle),
run_time=2
)
self.wait(1)
# Transform shapes
self.play(
circle.animate.set_color(PURPLE),
square.animate.rotate(PI/4),
triangle.animate.scale(1.5),
run_time=2
)
self.wait(1)
''',
"3D Scene": '''from manim import *
class ThreeDRotationDemo(ThreeDScene):
def construct(self):
# Set up 3D camera
self.set_camera_orientation(phi=75 * DEGREES, theta=45 * DEGREES)
# Create 3D objects
cube = Cube(side_length=2, color=BLUE)
sphere = Sphere(radius=1.2, color=RED).shift(RIGHT * 3)
torus = Torus(color=GREEN).shift(LEFT * 3)
# Title - FIXED: use add_fixed_in_frame_mobjects
title = Text("3D Scene Demo", font_size=36)
title.to_edge(UP)
self.add_fixed_in_frame_mobjects(title)
self.play(Write(title))
self.wait(0.5)
# Create 3D objects
self.play(
Create(cube),
Create(sphere),
Create(torus),
run_time=3
)
self.wait(1)
# Rotate objects
self.play(
Rotate(cube, angle=PI/2, axis=UP, run_time=2),
Rotate(sphere, angle=2*PI, axis=RIGHT, run_time=2),
Rotate(torus, angle=PI, axis=OUT, run_time=2)
)
self.wait(2)
# Move camera around
self.move_camera(phi=60*DEGREES, theta=90*DEGREES, run_time=3)
self.wait(1)
''',
"Math Formulas": '''from manim import *
class MathFormulasDemo(Scene):
def construct(self):
title = Text("Mathematics", font_size=48, color=BLUE)
title.to_edge(UP)
# These will work even without LaTeX
einstein = Text("E = mc²", font_size=72, color=YELLOW)
pythagoras = Text("a² + b² = c²", font_size=60, color=GREEN)
pythagoras.next_to(einstein, DOWN, buff=1)
euler = Text("e^(iπ) + 1 = 0", font_size=60, color=RED)
euler.next_to(pythagoras, DOWN, buff=1)
integral = Text("∫_{-∞}^{∞} e^(-x²) dx = √π", font_size=50, color=PURPLE)
integral.next_to(euler, DOWN, buff=1)
# Animate
self.play(Write(title))
self.wait(0.5)
formulas = [einstein, pythagoras, euler, integral]
for formula in formulas:
self.play(Write(formula), run_time=2)
self.wait(0.5)
self.wait(2)
''',
"Multi-Scene": '''from manim import *
class IntroScene(Scene):
def construct(self):
title = Text("Multi-Scene Demo", font_size=48, color=BLUE)
subtitle = Text("Scene 1 of 3", font_size=24)
subtitle.next_to(title, DOWN)
self.play(Write(title))
self.play(Write(subtitle))
self.wait(2)
class MainAnimation(ThreeDScene):
def construct(self):
self.set_camera_orientation(phi=75 * DEGREES, theta=45 * DEGREES)
cube = Cube(color=BLUE)
text = Text("3D Scene", font_size=36)
text.to_edge(UP)
# FIXED: Correct method for 3D scenes
self.add_fixed_in_frame_mobjects(text)
self.play(Write(text))
self.play(Create(cube))
self.play(Rotate(cube, angle=2*PI, axis=UP, run_time=3))
self.wait(1)
class OutroScene(Scene):
def construct(self):
thanks = Text("All Scenes Working!", font_size=60, color=GREEN)
self.play(Write(thanks))
self.play(thanks.animate.scale(1.5))
self.wait(2)
''',
"Moving Camera": '''from manim import *
class CameraMovementDemo(MovingCameraScene):
def construct(self):
# Create a large grid
text_group = VGroup()
for i in range(5):
for j in range(5):
text = Text(f"({i},{j})", font_size=24)
text.move_to(np.array([i*2-4, j*2-4, 0]))
text_group.add(text)
# Add all at once
self.play(FadeIn(text_group), run_time=2)
self.wait(1)
# Zoom in
self.play(
self.camera.frame.animate.scale(0.5).move_to(text_group[12]),
run_time=2
)
self.wait(1)
# Zoom out
self.play(
self.camera.frame.animate.scale(2),
run_time=2
)
self.wait(1)
''',
}
# ============================================
# Gradio Interface
# ============================================
renderer = RobustManimRenderer()
def render_manim_ultimate(code: str, quality: str, progress=gr.Progress()):
"""Ultimate rendering function"""
if not code.strip():
return [], "❌ Please enter code!"
try:
# CRITICAL: Clear old files periodically to prevent disk fill
cleanup_old_files()
output_files, message = renderer.render_all_scenes(code, quality, progress)
return output_files, message
except Exception as e:
import traceback
error_detail = traceback.format_exc()
return [], f"❌ Critical Error: {str(e)}\n\nDetails:\n{error_detail}"
def cleanup_old_files():
"""Clean up files older than 1 hour to prevent disk overflow"""
try:
current_time = time.time()
for file in PERSISTENT_OUTPUT_DIR.glob("*.mp4"):
if current_time - file.stat().st_mtime > 3600: # 1 hour
file.unlink()
print(f"🗑️ Deleted old file: {file}")
except Exception as e:
print(f"⚠️ Cleanup warning: {e}")
def load_example_ultimate(example_name: str) -> str:
"""Load fixed example"""
return FIXED_EXAMPLES.get(example_name, list(FIXED_EXAMPLES.values())[0])
def clear_all():
return "", [], "Ready!"
def validate_code(code: str) -> str:
"""Validate code"""
if not code.strip():
return "Please enter code"
try:
compile(code, '<string>', 'exec')
except SyntaxError as e:
return f"❌ Syntax Error Line {e.lineno}: {e.msg}"
scenes = EnhancedSceneDetector.extract_all_scenes(code)
if not scenes:
return "❌ No scene classes found"
return f"✅ {len(scenes)} scene(s) ready!"
css = """
@font-face {
font-family: "K.O. Activista* Bold";
src: url("https://st.1001fonts.net/download/font/k-o-activista.bold.ttf") format('truetype');
}
@font-face {
font-family: "Cadman Regular";
src: url("https://st.1001fonts.net/download/font/cadman.regular.otf") format('opentype');
}
body, .gradio-container {
font-family: "Cadman Regular", sans-serif !important;
background-color: #132015 !important;
color: #e0eecd;
}
h1, h2, h3, h4, h5, h6 {
font-family: "K.O. Activista* Bold", sans-serif !important;
margin-top: 5px;
margin-bottom: 5px;
}
/* Header Text Colors matching Soon Merger */
.gradio-container h1 {
font-size: 2.2rem;
text-align: center !important;
color: #FF312A;
text-transform: uppercase;
text-shadow: 2px 3px 1px #000;
line-height: 1.1;
}
.gradio-container h2 {
font-size: 1.3rem;
color: #FFF450;
text-align: center !important;
font-weight: bold;
font-variant: small-caps;
text-shadow: 2px 2px 1px #000;
}
.gradio-container h3 {
font-size: 1.0rem;
text-align: center !important;
color: #ABBEAB;
}
.block {
background: linear-gradient(to right, #10200E 0%, #6D1515 50%, #1F3C1F 100%) !important;
border: 1px solid #1a2f1a;
border-radius: 4px;
box-shadow: 1px 1px 2px 1px #1D3A1B;
padding: 10px !important; /* Reduced padding like Merger */
}
/* Reduce spacing in rows and columns */
.row, .column {
gap: 10px !important;
}
/* General Inputs */
input[type="text"], input[type="password"], input[type="number"], textarea, select, .wrap {
background-color: #111B11 !important;
border: 1px solid #2b4532 !important;
color: #e0eecd !important;
border-radius: 2px;
padding: 8px 12px !important;
font-family: "Cadman Regular", sans-serif !important;
}
input:focus, textarea:focus, select:focus {
border-color: #FF6262 !important;
outline: none;
box-shadow: 0 0 3px rgba(255, 98, 98, 0.3);
}
/* Code Editor Specifics */
#component-0 textarea, /* Adjust index based on layout, but targeting generally */
.cm-editor {
background-color: #050805 !important;
border: 1px solid #1a2f1a;
color: #d4d4d4;
font-family: "Consolas", "Monaco", monospace !important;
font-size: 13px !important;
}
#component-6, /* Adjust based on actual component ID if needed, otherwise class-based */
.console-output {
background-color: #000 !important;
color: #0f0 !important;
font-family: "Consolas", monospace !important;
border: 1px solid #333;
font-size: 12px;
white-space: pre-wrap;
}
button.primary {
background: linear-gradient(135deg, #FF4646 0%, #C33939 100%) !important;
border: none !important;
color: #050805;
font-weight: 900;
font-family: "K.O. Activista* Bold", sans-serif;
text-transform: uppercase;
letter-spacing: 1px;
padding: 8px 16px;
border-radius: 4px;
width: 100%;
box-shadow: 0 3px 4px rgba(0,0,0,0.3);
}
button.primary:hover {
filter: brightness(1.2);
box-shadow: 0 0 10px rgba(255, 159, 102, 0.4);
}
button.secondary {
background: #1a2f1a !important;
color: #ABBEAB;
border: 1px solid #3d5c3d;
font-family: "Consolas", monospace !important;
text-transform: uppercase;
}
.label-wrap {
background-color: #193219 !important;
border: 1px solid #1a2f1a !important;
color: #ABBEAB !important;
padding: 10px 15px !important;
border-radius: 4px;
font-family: "K.O. Activista* Bold", sans-serif;
}
label {
color: #ABBEAB;
font-weight: 600;
font-size: 0.95rem;
}
.gallery-item {
border: 1px solid #1a2f1a;
background-color: #050805;
border-radius: 2px;
overflow: hidden;
}
/* Radio Items */
.radio-group label {
color: #e0eecd;
padding-right: 15px;
}
"""
# Create UI
with gr.Blocks(title="SOONIMATOR") as demo:
gr.Markdown("""
# 🛡️🎯 SOONIMATOR 🔄✅
## Animation Renderer & Editor Kit
### FOR [MANIM COMMUNITY EDITION](https://docs.manim.community/en/stable/index.html) SCRIPTS
""")
with gr.Row():
with gr.Column(scale=3):
code_input = gr.Code(
value=FIXED_EXAMPLES["Simple Animation"],
language="python",
label="📝 Code Editor",
lines=34,
interactive=True
)
validation_status = gr.Textbox(
label="🔍 Validation",
value="Ready...",
lines=2,
interactive=False
)
with gr.Row():
quality_input = gr.Radio(
choices=["Low (Fast)", "Medium", "High", "Production"],
value="Low (Fast)",
label="🎥 Quality"
)
with gr.Row():
render_btn = gr.Button("▶️ Render All", variant="primary", scale=2)
clear_btn = gr.Button("🗑️ Clear", scale=1)
validate_btn = gr.Button("✅ Validate", scale=1)
with gr.Column(scale=2):
# CRITICAL: Gallery with explicit video settings
video_gallery = gr.Gallery(
label="🎬 Output Videos",
columns=1,
rows=3,
height=550,
object_fit="contain",
# Explicitly tell Gradio these are video files
type="filepath"
)
console_output = gr.Textbox(
label="📋 Console",
value="Waiting...",
lines=16,
max_lines=16,
interactive=False
)
with gr.Row():
with gr.Column():
gr.Markdown("### 📚 Tested Examples")
example_dropdown = gr.Dropdown(
choices=list(FIXED_EXAMPLES.keys()),
label="Load Example",
value="Simple Animation"
)
load_btn = gr.Button("Load Example")
with gr.Accordion("📖 Documentation", open=False):
gr.Markdown("""
Old files (>1 hour) auto-deleted to prevent disk overflow
### ✅ What Works Now
- **Videos will appear in the gallery immediately**
- Simple 2D animations
- 3D scenes with camera control
- Multi-scene rendering
- Moving camera scenes
- Math formulas
""")
# Event handlers
code_input.change(validate_code, inputs=[code_input], outputs=[validation_status])
render_btn.click(render_manim_ultimate, inputs=[code_input, quality_input], outputs=[video_gallery, console_output])
validate_btn.click(validate_code, inputs=[code_input], outputs=[validation_status])
load_btn.click(load_example_ultimate, inputs=[example_dropdown], outputs=[code_input])
clear_btn.click(clear_all, outputs=[code_input, video_gallery, console_output])
if __name__ == "__main__":
print("🚀 ULTIMATE MANIM EDITOR STARTING...")
print(f"📁 Videos saved to: {PERSISTENT_OUTPUT_DIR}")
latex_mgr = LaTeXManager()
if latex_mgr.latex_available:
print("✅ LaTeX/PyTinyTeX ready")
else:
print("⚠️ LaTeX not available - using Text fallback")
demo.launch(css=css, ssr_mode=False, mcp_server=True, share=True)