Spaces:
Sleeping
Sleeping
| 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""" | |
| 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 | |
| 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) |