Spaces:
Running
Running
| import gradio as gr | |
| import os | |
| import subprocess | |
| import time | |
| import shutil | |
| import glob | |
| import re | |
| # --------------------------------------------------------- | |
| # 1. Helper Functions | |
| # --------------------------------------------------------- | |
| def cleanup_media_directory(): | |
| """Removes the old media directory to prevent clutter.""" | |
| media_dir = 'media' | |
| if os.path.exists(media_dir): | |
| try: | |
| shutil.rmtree(media_dir) | |
| except OSError: | |
| pass | |
| def make_even(n): | |
| return int(n) if int(n) % 2 == 0 else int(n) + 1 | |
| def get_resolution_flags(orientation, quality): | |
| """Calculates exact width and height based on orientation and quality.""" | |
| if orientation == "Carousel Portrait (4:5)": | |
| res_map = { | |
| "Preview (360p)": (288, 360), | |
| "480p": (384, 480), | |
| "720p": (576, 720), | |
| "1080p": (1080, 1350), # Exactly 1080x1350 for 4:5 carousels | |
| "4k": (2160, 2700) | |
| } | |
| elif orientation == "Portrait (9:16)": | |
| res_map = { | |
| "Preview (360p)": (360, 640), | |
| "480p": (480, 854), | |
| "720p": (720, 1280), | |
| "1080p": (1080, 1920), | |
| "4k": (2160, 3840) | |
| } | |
| else: # Landscape (16:9) | |
| res_map = { | |
| "Preview (360p)": (640, 360), | |
| "480p": (854, 480), | |
| "720p": (1280, 720), | |
| "1080p": (1920, 1080), | |
| "4k": (3840, 2160) | |
| } | |
| w, h = res_map.get(quality, (1080, 1350)) | |
| return f"{make_even(w)},{make_even(h)}" | |
| def get_scene_names_in_order(code_str): | |
| """Parses the code to find the order of Scene classes defined.""" | |
| pattern = r"class\s+([A-Za-z0-9_]+)\s*\([^)]*\):" | |
| return re.findall(pattern, code_str) | |
| def run_manim(code_str, orientation, quality): | |
| """ | |
| Executes Manim to render all Scene classes as separate PNG images. | |
| """ | |
| print(f"🎨 Starting Carousel Render: {orientation} @ {quality}...", flush=True) | |
| with open("scene.py", "w", encoding="utf-8") as f: | |
| f.write(code_str) | |
| res_str = get_resolution_flags(orientation, quality) | |
| # -a renders all scenes in the file | |
| # -s instructs Manim to save the last frame of each scene as an image | |
| cmd = [ | |
| "manim", | |
| "--resolution", res_str, | |
| "--disable_caching", | |
| "--progress_bar", "none", | |
| "scene.py", | |
| "-a", "-s" | |
| ] | |
| full_logs = "" | |
| try: | |
| process = subprocess.run(cmd, capture_output=True, timeout=60, check=False) | |
| stdout_log = process.stdout.decode('utf-8', 'ignore') | |
| stderr_log = process.stderr.decode('utf-8', 'ignore') | |
| full_logs = f"--- MANIM STDOUT ---\n{stdout_log}\n\n--- MANIM STDERR ---\n{stderr_log}" | |
| if process.returncode != 0: | |
| print(f"❌ Render Failed (Process Error). Return Code: {process.returncode}", flush=True) | |
| except subprocess.TimeoutExpired: | |
| print("⌛ Render timed out.", flush=True) | |
| return [], "❌ Failure: Render Timed Out.", False | |
| # Manim saves images into the media/images/scene folder | |
| media_image_base = os.path.join("media", "images", "scene") | |
| generated_images = [] | |
| if os.path.exists(media_image_base): | |
| # Extract scene names in the exact order they were written in the code | |
| expected_scenes = get_scene_names_in_order(code_str) | |
| for scene_name in expected_scenes: | |
| expected_img_path = os.path.join(media_image_base, f"{scene_name}.png") | |
| if os.path.exists(expected_img_path): | |
| generated_images.append(expected_img_path) | |
| # Fallback: if regex missed anything, grab whatever is left in the folder | |
| all_pngs = glob.glob(os.path.join(media_image_base, "*.png")) | |
| for png in all_pngs: | |
| if png not in generated_images: | |
| generated_images.append(png) | |
| if generated_images: | |
| print(f"✅ Generated {len(generated_images)} images.", flush=True) | |
| return generated_images, f"✅ Rendering Successful. Generated {len(generated_images)} slides.\n\n{full_logs}", True | |
| return [], f"❌ Failure: No images were created.\n\n{full_logs}", False | |
| # --------------------------------------------------------- | |
| # 2. Main API Function | |
| # --------------------------------------------------------- | |
| def render_carousel_from_code(code, orientation, quality): | |
| try: | |
| cleanup_media_directory() | |
| if not code or "from manim import" not in code: | |
| return [], "Error: No valid Manim code provided." | |
| images, logs, success = run_manim(code, orientation, quality) | |
| return images, logs | |
| except Exception as e: | |
| return [], f"Rendering failed: {str(e)}" | |
| # --------------------------------------------------------- | |
| # 3. Gradio Interface | |
| # --------------------------------------------------------- | |
| # Carousel Example Code specifically tailored to look good on 4:5 1080x1350 | |
| DEFAULT_CODE = """from manim import * | |
| class Slide1_Title(Scene): | |
| def construct(self): | |
| title = Text("Manim Carousel", font_size=64, color=YELLOW, weight=BOLD) | |
| subtitle = Text("Swipe to see more ->", font_size=32).next_to(title, DOWN, buff=0.5) | |
| # Self.add puts it on the screen immediately for the static PNG export | |
| self.add(title, subtitle) | |
| class Slide2_Shapes(Scene): | |
| def construct(self): | |
| circle = Circle(color=BLUE, fill_opacity=0.5, radius=2) | |
| text = Text("High Quality Shapes", font_size=48).next_to(circle, DOWN, buff=1) | |
| self.add(circle, text) | |
| class Slide3_Conclusion(Scene): | |
| def construct(self): | |
| square = Square(color=RED, fill_opacity=0.5, side_length=4) | |
| text = Text("Perfect 1080x1350 Output", font_size=48).next_to(square, DOWN, buff=1) | |
| self.add(square, text) | |
| """ | |
| with gr.Blocks(title="Manim Carousel Generator") as demo: | |
| gr.Markdown("## 🎠 Manim Carousel Image Generator") | |
| gr.Markdown("Define **multiple `Scene` classes** in your code. Manim will render the final frame of *each* class and display them sequentially. Select `Carousel Portrait (4:5)` and `1080p` for exact **1080x1350** dimensions.") | |
| with gr.Row(): | |
| with gr.Column(scale=1): | |
| code_input = gr.Code(label="Python Code", language="python", value=DEFAULT_CODE) | |
| orientation_opt = gr.Radio( | |
| choices=["Landscape (16:9)", "Portrait (9:16)", "Carousel Portrait (4:5)"], | |
| value="Carousel Portrait (4:5)", # Defaulted to Carousel 4:5 | |
| label="Orientation" | |
| ) | |
| quality_opt = gr.Dropdown( | |
| choices=["Preview (360p)", "480p", "720p", "1080p", "4k"], | |
| value="1080p", # Defaulted to 1080p for 1080x1350 output | |
| label="Quality" | |
| ) | |
| render_btn = gr.Button("Generate Carousel", variant="primary") | |
| with gr.Column(scale=1): | |
| gallery_output = gr.Gallery(label="Carousel Output", columns=2, object_fit="contain", height="auto") | |
| status_output = gr.Textbox(label="Status/Logs", lines=10) | |
| render_btn.click( | |
| fn=render_carousel_from_code, | |
| inputs=[code_input, orientation_opt, quality_opt], | |
| outputs=[gallery_output, status_output], | |
| api_name="render_carousel" | |
| ) | |
| if __name__ == "__main__": | |
| demo.launch(server_name="0.0.0.0", server_port=7860) |