Spaces:
Running
Running
Update app.py
Browse files
app.py
CHANGED
|
@@ -84,23 +84,19 @@ def run_manim(code_str, orientation, quality, timeout):
|
|
| 84 |
stderr_log = process.stderr.decode('utf-8', 'ignore')
|
| 85 |
full_logs = f"--- MANIM STDOUT ---\n{stdout_log}\n\n--- MANIM STDERR ---\n{stderr_log}"
|
| 86 |
|
| 87 |
-
# 1. Check if Manim explicitly failed (non-zero exit code)
|
| 88 |
if process.returncode != 0:
|
| 89 |
print(f"β Render Failed.\n{stderr_log}", flush=True)
|
| 90 |
return None, f"β οΈ ERROR: Manim failed to render.\n{full_logs}", False
|
| 91 |
|
| 92 |
except subprocess.TimeoutExpired as e:
|
| 93 |
-
|
| 94 |
-
print(f"β Render timed out after {timeout_sec} seconds. Attempting recovery.", flush=True)
|
| 95 |
stdout_log = e.stdout.decode('utf-8', 'ignore') if e.stdout else ""
|
| 96 |
stderr_log = e.stderr.decode('utf-8', 'ignore') if e.stderr else ""
|
| 97 |
timeout_logs = f"--- MANIM STDOUT ---\n{stdout_log}\n\n--- MANIM STDERR ---\n{stderr_log}"
|
| 98 |
-
|
| 99 |
-
# ... [Recovery Logic Omitted for Brevity, but essentially same as previous version] ...
|
| 100 |
return None, f"β ERROR: Timed out.\n{timeout_logs}", False
|
| 101 |
|
| 102 |
# ---------------------------------------------------------
|
| 103 |
-
# 2. LOCATE OUTPUT FILE
|
| 104 |
# ---------------------------------------------------------
|
| 105 |
|
| 106 |
# Strategy A: Look for the expected Video File (.mp4)
|
|
@@ -118,45 +114,33 @@ def run_manim(code_str, orientation, quality, timeout):
|
|
| 118 |
return found_video_path, f"β
Rendering Successful\n\n{full_logs}", True
|
| 119 |
|
| 120 |
# Strategy B: Look for a Static Image (.png) and convert to Video
|
| 121 |
-
# Manim saves images to media/images/scene/ if no animations are played.
|
| 122 |
media_image_base = os.path.join("media", "images", "scene")
|
| 123 |
-
expected_image_name = output_filename + ".png"
|
| 124 |
found_image_path = None
|
| 125 |
|
| 126 |
if os.path.exists(media_image_base):
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
else:
|
| 132 |
-
# Recursive check in case of subfolders
|
| 133 |
-
for root, _, files in os.walk(media_image_base):
|
| 134 |
-
if expected_image_name in files:
|
| 135 |
-
found_image_path = os.path.join(root, expected_image_name)
|
| 136 |
-
break
|
| 137 |
|
| 138 |
if found_image_path:
|
| 139 |
print(f"πΌοΈ Static scene detected (0 animations). converting image to video: {found_image_path}", flush=True)
|
| 140 |
-
|
| 141 |
-
# Convert PNG to MP4 using FFMPEG (1 second duration)
|
| 142 |
converted_video_path = os.path.join(os.path.dirname(found_image_path), output_filename)
|
| 143 |
ffmpeg_cmd = [
|
| 144 |
"ffmpeg", "-y", "-loop", "1",
|
| 145 |
"-i", found_image_path,
|
| 146 |
"-t", "1",
|
| 147 |
"-c:v", "libx264",
|
| 148 |
-
"-pix_fmt", "yuv420p",
|
| 149 |
converted_video_path
|
| 150 |
]
|
| 151 |
-
|
| 152 |
ffmpeg_proc = subprocess.run(ffmpeg_cmd, capture_output=True, check=False)
|
| 153 |
if ffmpeg_proc.returncode == 0 and os.path.exists(converted_video_path):
|
| 154 |
-
print(f"β
Conversion Success: {converted_video_path}", flush=True)
|
| 155 |
return converted_video_path, f"β
Static Scene Rendered (Converted to 1s Video)\n\n{full_logs}", True
|
| 156 |
else:
|
| 157 |
print(f"β FFMPEG Conversion Failed: {ffmpeg_proc.stderr.decode()}", flush=True)
|
| 158 |
|
| 159 |
-
# If neither video nor image found
|
| 160 |
print(f"β Final output file '{output_filename}' not found.", flush=True)
|
| 161 |
return None, f"Video file not created despite success code. Check logs:\n{full_logs}", False
|
| 162 |
|
|
@@ -190,9 +174,18 @@ def render_video_from_code(code, orientation, quality, timeout, preview_factor):
|
|
| 190 |
# 4. Gradio Interface (API Definition)
|
| 191 |
# ---------------------------------------------------------
|
| 192 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 193 |
with gr.Blocks(title="Manim Render API") as demo:
|
| 194 |
# Hidden components to define the API signature
|
| 195 |
-
code_input = gr.Code(label="Python Code", language="python", value=
|
| 196 |
orientation_opt = gr.Radio(choices=["Landscape (16:9)", "Portrait (9:16)"], value="Portrait (9:16)", label="Orientation")
|
| 197 |
quality_opt = gr.Dropdown(choices=["Preview (360p)", "480p", "720p", "1080p", "4k"], value="Preview (360p)", label="Quality")
|
| 198 |
timeout_input = gr.Number(label="Render Timeout (seconds)", value=60)
|
|
@@ -202,7 +195,6 @@ with gr.Blocks(title="Manim Render API") as demo:
|
|
| 202 |
status_output = gr.Textbox(label="Status/Logs")
|
| 203 |
fix_btn_output = gr.Button("Fix Error & Re-render", variant="stop")
|
| 204 |
|
| 205 |
-
# The trigger
|
| 206 |
render_btn = gr.Button("Render")
|
| 207 |
|
| 208 |
render_btn.click(
|
|
|
|
| 84 |
stderr_log = process.stderr.decode('utf-8', 'ignore')
|
| 85 |
full_logs = f"--- MANIM STDOUT ---\n{stdout_log}\n\n--- MANIM STDERR ---\n{stderr_log}"
|
| 86 |
|
|
|
|
| 87 |
if process.returncode != 0:
|
| 88 |
print(f"β Render Failed.\n{stderr_log}", flush=True)
|
| 89 |
return None, f"β οΈ ERROR: Manim failed to render.\n{full_logs}", False
|
| 90 |
|
| 91 |
except subprocess.TimeoutExpired as e:
|
| 92 |
+
print(f"β Render timed out after {timeout_sec} seconds.", flush=True)
|
|
|
|
| 93 |
stdout_log = e.stdout.decode('utf-8', 'ignore') if e.stdout else ""
|
| 94 |
stderr_log = e.stderr.decode('utf-8', 'ignore') if e.stderr else ""
|
| 95 |
timeout_logs = f"--- MANIM STDOUT ---\n{stdout_log}\n\n--- MANIM STDERR ---\n{stderr_log}"
|
|
|
|
|
|
|
| 96 |
return None, f"β ERROR: Timed out.\n{timeout_logs}", False
|
| 97 |
|
| 98 |
# ---------------------------------------------------------
|
| 99 |
+
# 2. LOCATE OUTPUT FILE
|
| 100 |
# ---------------------------------------------------------
|
| 101 |
|
| 102 |
# Strategy A: Look for the expected Video File (.mp4)
|
|
|
|
| 114 |
return found_video_path, f"β
Rendering Successful\n\n{full_logs}", True
|
| 115 |
|
| 116 |
# Strategy B: Look for a Static Image (.png) and convert to Video
|
|
|
|
| 117 |
media_image_base = os.path.join("media", "images", "scene")
|
| 118 |
+
expected_image_name = output_filename + ".png"
|
| 119 |
found_image_path = None
|
| 120 |
|
| 121 |
if os.path.exists(media_image_base):
|
| 122 |
+
for root, _, files in os.walk(media_image_base):
|
| 123 |
+
if expected_image_name in files:
|
| 124 |
+
found_image_path = os.path.join(root, expected_image_name)
|
| 125 |
+
break
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 126 |
|
| 127 |
if found_image_path:
|
| 128 |
print(f"πΌοΈ Static scene detected (0 animations). converting image to video: {found_image_path}", flush=True)
|
|
|
|
|
|
|
| 129 |
converted_video_path = os.path.join(os.path.dirname(found_image_path), output_filename)
|
| 130 |
ffmpeg_cmd = [
|
| 131 |
"ffmpeg", "-y", "-loop", "1",
|
| 132 |
"-i", found_image_path,
|
| 133 |
"-t", "1",
|
| 134 |
"-c:v", "libx264",
|
| 135 |
+
"-pix_fmt", "yuv420p",
|
| 136 |
converted_video_path
|
| 137 |
]
|
|
|
|
| 138 |
ffmpeg_proc = subprocess.run(ffmpeg_cmd, capture_output=True, check=False)
|
| 139 |
if ffmpeg_proc.returncode == 0 and os.path.exists(converted_video_path):
|
|
|
|
| 140 |
return converted_video_path, f"β
Static Scene Rendered (Converted to 1s Video)\n\n{full_logs}", True
|
| 141 |
else:
|
| 142 |
print(f"β FFMPEG Conversion Failed: {ffmpeg_proc.stderr.decode()}", flush=True)
|
| 143 |
|
|
|
|
| 144 |
print(f"β Final output file '{output_filename}' not found.", flush=True)
|
| 145 |
return None, f"Video file not created despite success code. Check logs:\n{full_logs}", False
|
| 146 |
|
|
|
|
| 174 |
# 4. Gradio Interface (API Definition)
|
| 175 |
# ---------------------------------------------------------
|
| 176 |
|
| 177 |
+
DEFAULT_CODE = """from manim import *
|
| 178 |
+
|
| 179 |
+
class GenScene(Scene):
|
| 180 |
+
def construct(self):
|
| 181 |
+
c = Circle(color=BLUE, fill_opacity=0.5)
|
| 182 |
+
self.play(Create(c)) # Animation adds runtime
|
| 183 |
+
self.wait(1) # Pause adds runtime
|
| 184 |
+
"""
|
| 185 |
+
|
| 186 |
with gr.Blocks(title="Manim Render API") as demo:
|
| 187 |
# Hidden components to define the API signature
|
| 188 |
+
code_input = gr.Code(label="Python Code", language="python", value=DEFAULT_CODE)
|
| 189 |
orientation_opt = gr.Radio(choices=["Landscape (16:9)", "Portrait (9:16)"], value="Portrait (9:16)", label="Orientation")
|
| 190 |
quality_opt = gr.Dropdown(choices=["Preview (360p)", "480p", "720p", "1080p", "4k"], value="Preview (360p)", label="Quality")
|
| 191 |
timeout_input = gr.Number(label="Render Timeout (seconds)", value=60)
|
|
|
|
| 195 |
status_output = gr.Textbox(label="Status/Logs")
|
| 196 |
fix_btn_output = gr.Button("Fix Error & Re-render", variant="stop")
|
| 197 |
|
|
|
|
| 198 |
render_btn = gr.Button("Render")
|
| 199 |
|
| 200 |
render_btn.click(
|