SOONIMATOR / app6.py
AlekseyCalvin's picture
Rename app.py to app6.py
4f7b48c verified
import gradio as gr
import subprocess
import tempfile
import os
import shutil
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
# Scene compatibility layer - handles method differences between scene types
SCENE_COMPATIBILITY = {
'ThreeDScene': {
'incompatible_methods': {
'fix_in_frame': 'Use `self.add_fixed_in_frame_mobject()` instead',
'move_to_and_align': 'Use `self.add_fixed_in_frame_mobject()` instead'
},
'required_imports': ['from manim import *'],
'camera_setup': True
},
'MovingCameraScene': {
'incompatible_methods': {},
'required_imports': ['from manim import *']
},
'ZoomedScene': {
'incompatible_methods': {},
'required_imports': ['from manim import *']
},
'VectorScene': {
'incompatible_methods': {},
'required_imports': ['from manim import *']
},
'LinearTransformationScene': {
'incompatible_methods': {},
'required_imports': ['from manim import *']
},
'Scene': {
'incompatible_methods': {},
'required_imports': ['from manim import *']
}
}
class LaTeXManager:
"""Comprehensive LaTeX handling with multiple fallback strategies"""
def __init__(self):
self.latex_available = self._check_latex_availability()
self.pytinytex_available = self._check_pytinytex()
def _check_latex_availability(self) -> bool:
"""Check if system LaTeX is available"""
try:
# Check for common LaTeX compilers
for compiler in ['pdflatex', 'xelatex', 'lualatex']:
result = subprocess.run(
[compiler, '--version'],
capture_output=True, text=True, timeout=5
)
if result.returncode == 0:
print(f"✅ Found LaTeX compiler: {compiler}")
return True
except:
pass
print("❌ No system LaTeX found")
return False
def _check_pytinytex(self) -> bool:
"""Check if PyTinyTeX is available"""
try:
import pytinytex
print("✅ PyTinyTeX is available")
return True
except:
print("❌ PyTinyTeX not found")
return False
def setup_latex_environment(self):
"""Configure LaTeX environment"""
if not self.latex_available and self.pytinytex_available:
try:
import pytinytex
# Install PyTinyTeX if needed
pytinytex.install()
print("✅ PyTinyTeX installed and configured")
self.latex_available = True
except Exception as e:
print(f"❌ Failed to setup PyTinyTeX: {e}")
def validate_and_fix_code(self, code: str) -> Tuple[str, List[str]]:
"""Validate code and fix LaTeX-related issues"""
warnings = []
fixed_code = code
# Check for MathTex usage
if 'MathTex' in code and not self.latex_available:
warnings.append("⚠️ MathTex detected but LaTeX not available. Converting to Text()")
# Replace MathTex with Text for simple expressions
fixed_code = self._convert_mathtex_to_text(fixed_code)
return fixed_code, warnings
def _convert_mathtex_to_text(self, code: str) -> str:
"""Convert MathTex to Text for simple expressions"""
# Replace MathTex with Text for basic formulas
replacements = [
(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 replacements:
fixed_code = re.sub(pattern, replacement, code)
# Generic MathTex to Text conversion for other cases
fixed_code = re.sub(r'MathTex\((.*?)\)', r'Text(\1)', fixed_code)
return fixed_code
class SceneCompatibilityFixer:
"""Handles compatibility issues between different scene types"""
def __init__(self):
self.known_issues = {
# Text methods that don't work in 3D scenes
'fix_in_frame': self._fix_fix_in_frame,
'move_to_and_align': self._fix_move_to_and_align,
}
def fix_code_compatibility(self, code: str, scene_type: str) -> str:
"""Fix compatibility issues based on scene type"""
fixed_code = code
if scene_type in ['ThreeDScene', 'SpecialThreeDScene', 'ThreeDSlide']:
# Fix 3D scene compatibility issues
for method_name, fixer_func in self.known_issues.items():
if method_name in fixed_code:
fixed_code = fixer_func(fixed_code)
print(f"🔧 Fixed {method_name} compatibility for {scene_type}")
return fixed_code
def _fix_fix_in_frame(self, code: str) -> str:
"""Fix Text.fix_in_frame() method calls"""
# Replace Text.fix_in_frame() with proper 3D scene method
# Pattern: object.fix_in_frame() -> self.add_fixed_in_frame_mobject(object)
fixed_code = re.sub(
r'(\w+)\.fix_in_frame\(\)',
r'self.add_fixed_in_frame_mobjects(\1)',
code
)
return fixed_code
def _fix_move_to_and_align(self, code: str) -> str:
"""Fix move_to_and_align method calls"""
# This method doesn't exist, replace with move_to
fixed_code = re.sub(
r'\.move_to_and_align\((.*?)\)',
r'.move_to(\1)',
code
)
return fixed_code
class EnhancedSceneDetector:
"""Enhanced scene detection with better error handling"""
@staticmethod
def extract_all_scenes(code: str) -> List[Dict[str, str]]:
"""Extract all scene classes with improved detection"""
scenes = []
lines = code.split('\n')
for i, line in enumerate(lines):
line = line.strip()
if line.startswith('class '):
# Extract class name and parent classes
class_match = re.match(r'class\s+(\w+)\s*\((.*?)\):', line)
if class_match:
class_name = class_match.group(1)
parent_classes = class_match.group(2)
# Check if it's a scene class
for scene_type in SUPPORTED_SCENE_TYPES:
if scene_type in parent_classes:
scenes.append({
'name': class_name,
'type': scene_type,
'line': i + 1,
'parent_classes': parent_classes
})
break
return scenes
@staticmethod
def validate_scene_methods(code: str, scene_type: str) -> List[Tuple[int, str, str]]:
"""Validate that scene methods are compatible with scene type"""
errors = []
lines = code.split('\n')
# Get incompatible methods for this scene type
incompatible = SCENE_COMPATIBILITY.get(scene_type, {}).get('incompatible_methods', {})
for i, line in enumerate(lines, 1):
for method, suggestion in incompatible.items():
if f'.{method}(' in line:
errors.append((i, method, suggestion))
return errors
class RobustManimRenderer:
"""Robust Manim renderer with comprehensive error handling"""
def __init__(self):
self.latex_manager = LaTeXManager()
self.compatibility_fixer = SceneCompatibilityFixer()
self.latex_manager.setup_latex_environment()
def prepare_code(self, code: str) -> Tuple[str, List[str]]:
"""Prepare code with fixes and validations"""
warnings = []
fixed_code = code
# 1. Extract scenes to understand context
scenes = EnhancedSceneDetector.extract_all_scenes(code)
# 2. Fix LaTeX issues
fixed_code, latex_warnings = self.latex_manager.validate_and_fix_code(fixed_code)
warnings.extend(latex_warnings)
# 3. Fix scene compatibility issues
for scene in scenes:
fixed_code = self.compatibility_fixer.fix_code_compatibility(
fixed_code, scene['type']
)
# Check for incompatible methods
method_errors = EnhancedSceneDetector.validate_scene_methods(
fixed_code, scene['type']
)
for line, method, suggestion in method_errors:
warnings.append(f"⚠️ Line {line}: {method} may not work. {suggestion}")
return fixed_code, warnings
def render_scene_with_fallbacks(self, code: str, scene_name: str, quality: str,
progress_callback=None) -> Tuple[Optional[str], str]:
"""Render with multiple fallback strategies"""
# First, prepare the code with fixes
prepared_code, warnings = self.prepare_code(code)
if warnings:
warning_msg = "\n".join(warnings)
if progress_callback:
progress_callback(0.05, f"Warnings:\n{warning_msg}")
# Create temp directory
temp_dir = tempfile.mkdtemp(prefix=f"manim_robust_{scene_name}_")
try:
if progress_callback:
progress_callback(0.1, "Saving prepared code...")
# Save prepared code
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"Starting render of {scene_name}...")
# Quality mapping
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")
# Build command with error handling
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 {scene_name}...")
# Execute with comprehensive error capture
result = subprocess.run(
cmd, capture_output=True, text=True,
timeout=600, cwd=temp_dir
)
if progress_callback:
progress_callback(0.8, "Processing output...")
# Handle results
output_text = result.stdout + "\n" + result.stderr
if result.returncode != 0:
# Try fallback rendering with lower quality
if quality_flag != "l":
if progress_callback:
progress_callback(0.85, "Render failed, trying low quality...")
cmd[6] = "l" # Change to low quality
result = subprocess.run(
cmd, capture_output=True, text=True,
timeout=600, cwd=temp_dir
)
if result.returncode == 0:
output_text = result.stdout + "\n" + result.stderr
output_text += "\n⚠️ Render succeeded with low quality fallback"
if result.returncode != 0:
return None, f"❌ Scene {scene_name} failed:\n{output_text}"
# Find output file
output_file = self._find_output_file(output_dir, scene_name)
if not output_file:
return None, f"❌ No output found for {scene_name}"
if progress_callback:
progress_callback(1.0, f"✅ {scene_name} complete!")
file_size = os.path.getsize(output_file) / (1024 * 1024)
message = f"✅ {scene_name} rendered successfully!\n📁 Size: {file_size:.2f} MB"
if warnings:
message += f"\n\n⚠️ Warnings:\n" + "\n".join(warnings[:3])
return output_file, message
except subprocess.TimeoutExpired:
return None, f"❌ Scene {scene_name} timed out (>10 minutes)"
except Exception as e:
return None, f"❌ Error in {scene_name}: {str(e)}"
def render_all_scenes_robust(self, code: str, quality: str,
progress=gr.Progress()) -> Tuple[List[str], str]:
"""Render all scenes with robust error handling"""
# Extract all scenes
scenes = EnhancedSceneDetector.extract_all_scenes(code)
if not scenes:
return [], "❌ No valid 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)")
# Check LaTeX status
if not self.latex_manager.latex_available:
progress(0.08, desc="⚠️ LaTeX not available - MathTex will be converted to Text")
output_files = []
all_messages = []
# Render scenes with individual error handling
for i, scene_info in enumerate(scenes):
scene_name = scene_info['name']
scene_type = scene_info['type']
progress(
0.2 + (i / len(scenes)) * 0.7,
desc=f"Rendering {scene_name} ({scene_type})..."
)
video_file, message = self.render_scene_with_fallbacks(
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)
all_messages.append(f"✅ {scene_name}: Success")
else:
all_messages.append(f"❌ {scene_name}: {message}")
# Combine messages
final_message = "\n".join(all_messages)
final_message += f"\n\n📊 Total scenes rendered: {len(output_files)}/{len(scenes)}"
return output_files, final_message
def _find_output_file(self, output_dir: str, scene_name: str) -> Optional[str]:
"""Find generated output file"""
# Check videos directory
videos_dir = os.path.join(output_dir, "videos", "animation")
if os.path.exists(videos_dir):
for root, dirs, files in os.walk(videos_dir):
for file in files:
if file.endswith('.mp4') and scene_name.lower() in file.lower():
return os.path.join(root, file)
# Check images directory
images_dir = os.path.join(output_dir, "images", "animation")
if os.path.exists(images_dir):
for root, dirs, files in os.walk(images_dir):
for file in files:
if file.endswith(('.png', '.jpg')):
return os.path.join(root, file)
return None
# Robust examples that work with the compatibility layer
ROBUST_EXAMPLES = {
"Simple Animation": '''from manim import *
class SimpleAnimation(Scene):
def construct(self):
title = Text("Robust Manim Editor!", font_size=48, color=BLUE)
subtitle = Text("Works with all scene types", 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 (Fixed)": '''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 - use add_fixed_in_frame_mobjects for 3D scenes
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)
''',
"Moving Camera Scene": '''from manim import *
class CameraMovementDemo(MovingCameraScene):
def construct(self):
# Create a large scene
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 on specific area
self.play(
self.camera.frame.animate.scale(0.5).move_to(text_group[12]),
run_time=2
)
self.wait(1)
# Pan to another area
self.play(
self.camera.frame.animate.move_to(text_group[20]),
run_time=2
)
self.wait(1)
# Zoom out
self.play(
self.camera.frame.animate.scale(2),
run_time=2
)
self.wait(1)
''',
"Math Formulas (Text Fallback)": '''from manim import *
class MathFormulasDemo(Scene):
def construct(self):
title = Text("Mathematics with Text", font_size=48, color=BLUE)
title.to_edge(UP)
# Use Text instead of MathTex for compatibility
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 Example (Fixed)": '''from manim import *
class IntroScene(Scene):
def construct(self):
title = Text("Multi-Scene Demo", font_size=48, color=BLUE)
subtitle = Text("This is the first scene", 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)
self.add_fixed_in_frame_mobjects(text) # Fixed for 3D
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("Thank You!", font_size=60, color=GREEN)
self.play(Write(thanks))
self.play(thanks.animate.scale(1.5))
self.wait(2)
''',
"Zoomed Scene (Fixed)": '''from manim import *
class ZoomedSceneDemo(ZoomedScene):
def construct(self):
# Create the main content
equation = Text("∫ f(x)dx = F(x) + C", font_size=60)
# Add to scene
self.play(Write(equation))
self.wait(1)
# Activate zoom
self.activate_zooming()
self.play(self.zoomed_camera.frame.animate.scale(0.5))
self.play(self.zoomed_camera.frame.animate.move_to(equation))
self.wait(2)
# Deactivate zoom
self.play(self.zoomed_camera.frame.animate.scale(2))
self.wait(1)
''',
}
# Initialize robust renderer
robust_renderer = RobustManimRenderer()
def render_manim_robust(code: str, quality: str, progress=gr.Progress()):
"""Robust rendering function with comprehensive error handling"""
if not code.strip():
return [], "❌ Error: Please enter some code!"
try:
output_files, message = robust_renderer.render_all_scenes_robust(
code, quality, progress
)
return output_files, message
except Exception as e:
return [], f"❌ Critical error: {str(e)}"
def load_example_robust(example_name: str) -> str:
"""Load robust example"""
return ROBUST_EXAMPLES.get(example_name, list(ROBUST_EXAMPLES.values())[0])
def clear_all_robust():
"""Clear all fields"""
return "", [], "Ready to render!"
def validate_code_robust(code: str) -> str:
"""Enhanced code validation"""
if not code.strip():
return "Please enter some code"
# Check syntax
try:
compile(code, '<string>', 'exec')
except SyntaxError as e:
return f"❌ Syntax Error: Line {e.lineno}, {e.msg}"
# Check for scenes
scenes = EnhancedSceneDetector.extract_all_scenes(code)
if not scenes:
return "❌ No scene classes found"
# Check LaTeX
if not robust_renderer.latex_manager.latex_available:
return f"⚠️ LaTeX not available - {len(scenes)} scene(s) found (MathTex will use Text fallback)"
return f"✅ Found {len(scenes)} scene(s) - Ready to render!"
# Create enhanced interface
with gr.Blocks(title="SOON MANIMALTOR") as demo:
gr.Markdown("""
# 🛡️🎯 SOON MANIMALTOR 🔄✅
**Production-Ready Mathematical Animation Studio**
""")
with gr.Row():
with gr.Column(scale=3):
# Code editor
code_input = gr.Code(
value=ROBUST_EXAMPLES["Simple Animation"],
language="python",
label="📝 Manim Code Editor",
lines=30,
interactive=True
)
# Live validation
validation_status = gr.Textbox(
label="🔍 Code Validation",
value="Ready to validate...",
lines=2,
interactive=False
)
with gr.Row():
quality_input = gr.Radio(
choices=["Low (Fast)", "Medium", "High", "Production"],
value="Low (Fast)",
label="🎥 Render Quality",
info="Higher quality = slower rendering"
)
with gr.Row():
render_btn = gr.Button("▶️ Render All Scenes", variant="primary", scale=2)
clear_btn = gr.Button("🗑️ Clear All", scale=1)
validate_btn = gr.Button("✅ Validate Code", scale=1)
with gr.Column(scale=2):
# Video gallery
video_gallery = gr.Gallery(
label="🎬 Rendered Animations",
columns=1,
rows=3,
height=500,
object_fit="contain"
)
# Console output
console_output = gr.Textbox(
label="📋 Console Output",
value="Ready to render!",
lines=20,
max_lines=20,
interactive=False
)
# Examples section
with gr.Row():
with gr.Column():
gr.Markdown("### 📚 Robust Examples")
example_dropdown = gr.Dropdown(
choices=list(ROBUST_EXAMPLES.keys()),
label="Load Example",
value="Simple Animation"
)
load_example_btn = gr.Button("Load Example")
# Documentation
with gr.Accordion("📖 Documentation & Troubleshooting", open=False):
gr.Markdown("""
### Robust Features
**✅ Auto-Fixes:**
- Text.fix_in_frame() → self.add_fixed_in_frame_mobjects() for 3D scenes
- MathTex → Text fallback when LaTeX unavailable
- Scene-specific compatibility fixes
**🎯 Smart LaTeX Handling:**
- Detects system LaTeX and PyTinyTeX
- Auto-installs PyTinyTeX if needed
- Converts MathTex to Text for simple formulas
- Provides clear warnings for LaTeX issues
**🔄 Error Recovery:**
- Fallback to lower quality on render failure
- Individual scene error handling
- Comprehensive error messages
**📐 Scene Compatibility:**
- ThreeDScene: Uses proper fixed-in-frame methods
- MovingCameraScene: Full camera control support
- ZoomedScene: Magnification effects
- Scene: Standard 2D animations
**🛠️ Troubleshooting:**
- "No attribute 'fix_in_frame'" → Fixed automatically
- "No such file or directory: 'latex'" → Uses Text fallback
- Render timeouts → Try lower quality
- Scene not found → Validates class names
### Requirements
- manim: `pip install manim>=0.19.0`
- PyTinyTeX: `pip install PyTinyTeX` (optional, for LaTeX)
- ffmpeg: For video processing
""")
# Event handlers
def on_code_change(code):
return validate_code_robust(code)
code_input.change(on_code_change, inputs=[code_input], outputs=[validation_status])
render_btn.click(
fn=render_manim_robust,
inputs=[code_input, quality_input],
outputs=[video_gallery, console_output]
)
validate_btn.click(
fn=validate_code_robust,
inputs=[code_input],
outputs=[validation_status]
)
load_example_btn.click(
fn=load_example_robust,
inputs=[example_dropdown],
outputs=[code_input]
)
clear_btn.click(
fn=clear_all_robust,
outputs=[code_input, video_gallery, console_output]
)
if __name__ == "__main__":
demo.launch(theme=gr.themes.Soft(), ssr_mode=False, mcp_server=True)