AnimateMyIdea / app.py
devendergarg14's picture
Update app.py
45c3201 verified
import gradio as gr
import os
import subprocess
import re
import time
import glob
import shutil
# ---------------------------------------------------------
# 1. Helper Functions (Dependencies for Rendering)
# ---------------------------------------------------------
def cleanup_media_directory():
"""Wipes the media directory to prevent caching issues."""
media_dir = 'media'
if os.path.exists(media_dir):
try:
shutil.rmtree(media_dir)
except OSError as e:
print(f"⚠️ Warning during cleanup: {e}", flush=True)
def make_even(n):
n = int(n)
return n if n % 2 == 0 else n + 1
def get_resolution_flags(orientation, quality):
qual_map = {
"Preview (360p)": 360, "480p": 480, "720p": 720, "1080p": 1080, "4k": 2160
}
base_h = qual_map.get(quality, 1080)
if orientation == "Landscape (16:9)":
width = make_even(base_h * (16/9)); height = make_even(base_h)
else: # Portrait (9:16)
width = make_even(base_h); height = make_even(base_h * (16/9))
return f"{width},{height}"
def scale_animation_times(code, factor):
"""
Scales run_time and wait() calls, enforcing a minimum run_time to prevent render errors.
"""
print(f"⚑ Scaling animation times by a factor of {factor} for preview.", flush=True)
MIN_RUN_TIME = 0.1 # A safe minimum duration for any animation clip.
def scale_runtime(m):
new_time = max(float(m.group(2)) * factor, MIN_RUN_TIME)
return f"{m.group(1)}{new_time}"
def scale_wait(m):
new_time = float(m.group(2)) * factor
return f"{m.group(1)}{new_time}"
code = re.sub(r"(run_time\s*=\s*)(\d+\.?\d*)", scale_runtime, code)
code = re.sub(r"(self\.wait\s*\(\s*)(\d+\.?\d*)", scale_wait, code)
return code
def run_manim(code_str, orientation, quality, timeout):
timeout_sec = float(timeout) if timeout and float(timeout) > 0 else None
print(f"🎬 Starting Render: {orientation} @ {quality} (Timeout: {timeout_sec}s)...", flush=True)
# Write code to file
with open("scene.py", "w", encoding="utf-8") as f:
f.write(code_str)
timestamp = int(time.time())
output_filename = f"video_{timestamp}.mp4"
res_str = get_resolution_flags(orientation, quality)
# Manim command
frame_rate_flags = ["--frame_rate", "15"] if quality == "Preview (360p)" else []
cmd = [
"manim",
"--resolution", res_str,
*frame_rate_flags,
"--disable_caching",
"--progress_bar", "none",
"scene.py", "GenScene",
"-o", output_filename
]
print(f"βš™οΈ Running command: {' '.join(cmd)}", flush=True)
try:
process = subprocess.run(cmd, capture_output=True, timeout=timeout_sec, 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.\n{stderr_log}", flush=True)
return None, f"⚠️ ERROR: Manim failed to render.\n{full_logs}", False
except subprocess.TimeoutExpired as e:
print(f"βŒ› Render timed out after {timeout_sec} seconds.", flush=True)
stdout_log = e.stdout.decode('utf-8', 'ignore') if e.stdout else ""
stderr_log = e.stderr.decode('utf-8', 'ignore') if e.stderr else ""
timeout_logs = f"--- MANIM STDOUT ---\n{stdout_log}\n\n--- MANIM STDERR ---\n{stderr_log}"
return None, f"❌ ERROR: Timed out.\n{timeout_logs}", False
# ---------------------------------------------------------
# 2. LOCATE OUTPUT FILE
# ---------------------------------------------------------
# Strategy A: Look for the expected Video File (.mp4)
media_video_base = os.path.join("media", "videos", "scene")
found_video_path = None
if os.path.exists(media_video_base):
for root, _, files in os.walk(media_video_base):
if output_filename in files:
found_video_path = os.path.join(root, output_filename)
break
if found_video_path:
print(f"βœ… Video Render Success: {found_video_path}", flush=True)
return found_video_path, f"βœ… Rendering Successful\n\n{full_logs}", True
# Strategy B: Look for a Static Image (.png) and convert to Video
media_image_base = os.path.join("media", "images", "scene")
expected_image_name = output_filename + ".png"
found_image_path = None
if os.path.exists(media_image_base):
for root, _, files in os.walk(media_image_base):
if expected_image_name in files:
found_image_path = os.path.join(root, expected_image_name)
break
if found_image_path:
print(f"πŸ–ΌοΈ Static scene detected (0 animations). converting image to video: {found_image_path}", flush=True)
converted_video_path = os.path.join(os.path.dirname(found_image_path), output_filename)
ffmpeg_cmd = [
"ffmpeg", "-y", "-loop", "1",
"-i", found_image_path,
"-t", "1",
"-c:v", "libx264",
"-pix_fmt", "yuv420p",
converted_video_path
]
ffmpeg_proc = subprocess.run(ffmpeg_cmd, capture_output=True, check=False)
if ffmpeg_proc.returncode == 0 and os.path.exists(converted_video_path):
return converted_video_path, f"βœ… Static Scene Rendered (Converted to 1s Video)\n\n{full_logs}", True
else:
print(f"❌ FFMPEG Conversion Failed: {ffmpeg_proc.stderr.decode()}", flush=True)
print(f"❌ Final output file '{output_filename}' not found.", flush=True)
return None, f"Video file not created despite success code. Check logs:\n{full_logs}", False
# ---------------------------------------------------------
# 3. Main API Function
# ---------------------------------------------------------
def render_video_from_code(code, orientation, quality, timeout, preview_factor):
"""Renders a video from a given Manim code string."""
try:
cleanup_media_directory()
if not code or "from manim import" not in code:
return None, "Error: No valid code to render.", gr.Button(visible=False)
code_to_render = code
if quality == "Preview (360p)":
try:
factor = float(preview_factor)
if factor <= 0: factor = 0.5
except (ValueError, TypeError):
factor = 0.5
code_to_render = scale_animation_times(code, factor)
video_path, logs, success = run_manim(code_to_render, orientation, quality, timeout)
return video_path, logs, gr.Button(visible=not success)
except Exception as e:
return None, f"Rendering failed: {str(e)}", gr.Button(visible=True)
# ---------------------------------------------------------
# 4. Gradio Interface (API Definition)
# ---------------------------------------------------------
DEFAULT_CODE = """from manim import *
class GenScene(Scene):
def construct(self):
c = Circle(color=BLUE, fill_opacity=0.5)
self.play(Create(c)) # Animation adds runtime
self.wait(1) # Pause adds runtime
"""
with gr.Blocks(title="Manim Render API") as demo:
# Hidden components to define the API signature
code_input = gr.Code(label="Python Code", language="python", value=DEFAULT_CODE,visible=False)
orientation_opt = gr.Radio(choices=["Landscape (16:9)", "Portrait (9:16)"], value="Portrait (9:16)", label="Orientation",visible=False)
quality_opt = gr.Dropdown(choices=["Preview (360p)", "480p", "720p", "1080p", "4k"], value="Preview (360p)", label="Quality", visible=False)
timeout_input = gr.Number(label="Render Timeout (seconds)", value=60, visible=False)
preview_speed_factor_input = gr.Number(label="Preview Speed Factor", value=0.5,visible=False)
video_output = gr.Video(label="Result")
status_output = gr.Textbox(label="Status/Logs")
fix_btn_output = gr.Button("Fix Error & Re-render", variant="stop",visible=False)
render_btn = gr.Button("Render")
render_btn.click(
fn=render_video_from_code,
inputs=[code_input, orientation_opt, quality_opt, timeout_input, preview_speed_factor_input],
outputs=[video_output, status_output, fix_btn_output],
api_name="render"
)
if __name__ == "__main__":
demo.launch(server_name="0.0.0.0", server_port=7860)