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