File size: 7,496 Bytes
3689207
 
 
 
 
12e6c81
e2f218a
3689207
 
05c2df9
3689207
 
 
e2f218a
3f1ca1c
 
e2f218a
 
 
 
3689207
 
12e6c81
3689207
 
990badf
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3689207
e2f218a
 
 
 
 
 
3f1ca1c
e2f218a
3f1ca1c
e2f218a
3f1ca1c
e2f218a
 
3f1ca1c
3689207
12e6c81
e2f218a
 
 
 
 
 
 
 
 
 
3f1ca1c
 
 
3689207
e2f218a
3f1ca1c
 
 
 
 
 
 
e2f218a
 
 
3f1ca1c
e2f218a
 
 
3f1ca1c
e2f218a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1b5bfb7
e2f218a
3689207
 
05c2df9
3689207
 
e2f218a
3689207
 
e2f218a
3f1ca1c
e2f218a
3f1ca1c
e2f218a
 
3689207
e2f218a
e6d9ed9
3689207
e2f218a
3689207
 
990badf
8bd2e7e
 
e2f218a
8bd2e7e
990badf
 
 
 
e2f218a
8bd2e7e
e2f218a
 
990badf
 
e2f218a
e6d9ed9
e2f218a
 
990badf
 
e2f218a
 
3f1ca1c
e2f218a
 
990badf
e2f218a
 
 
 
990badf
 
 
 
 
 
 
 
 
 
e2f218a
 
 
 
 
 
 
 
 
 
 
 
3689207
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
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)