AnimateMyIdea1 / app.py
devendergarg14's picture
Update app.py
a1e136d verified
raw
history blame
8.36 kB
import gradio as gr
import os
import subprocess
import re
import time
import glob
import shutil
# ---------------------------------------------------------
# 1. Helper Functions (Dependencies for Rendering)
# ---------------------------------------------------------
def run_manim_pre_check(code_str: str) -> (bool, str):
"""
Runs Manim with the '-s' flag to perform a fast, reliable pre-check.
This executes the Python code without rendering the full video.
Returns (True, "Success") if the code is valid.
Returns (False, "Error Log") if any Python error (like NameError) occurs.
"""
print("🕵️ Running fast pre-check with 'manim -s'...", flush=True)
# Write the code to a temporary file for the pre-check
with open("scene_pre_check.py", "w", encoding="utf-8") as f:
f.write(code_str)
# Use low quality and '-s' flag for maximum speed. Output is discarded.
cmd = [
"manim",
"-ql",
"--progress_bar", "none",
"--disable_caching",
"scene_pre_check.py", "GenScene",
"-s", # This is the key: save last frame only.
"-o", "pre_check_output" # Output is temporary and will be overwritten
]
try:
# We don't need a long timeout for a pre-check
process = subprocess.run(cmd, capture_output=True, timeout=30, check=False)
if process.returncode == 0:
print("✅ Pre-check passed. Code is valid.", flush=True)
return True, "Pre-check successful."
else:
# An error occurred, capture the logs
stderr_log = process.stderr.decode('utf-8', 'ignore')
print(f"❌ Pre-check failed.\n{stderr_log}", flush=True)
# Format a clean error message for the user
error_message = f"⚠️ ERROR: Your code failed the pre-check.\n\n--- ERROR LOG ---\n{stderr_log}"
return False, error_message
except subprocess.TimeoutExpired:
print("⌛ Pre-check timed out.", flush=True)
return False, "❌ ERROR: The pre-check timed out. The code may contain an infinite loop."
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, height = make_even(base_h * (16/9)), make_even(base_h)
else:
width, height = make_even(base_h), make_even(base_h * (16/9))
return f"{width},{height}"
def scale_animation_times(code, factor):
"""Scales run_time and wait() calls."""
print(f"⚡ Scaling animation times by a factor of {factor} for preview.", flush=True)
MIN_RUN_TIME = 0.1
def scale(m, is_wait):
val = float(m.group(2)) * factor
return f"{m.group(1)}{val if is_wait else max(val, MIN_RUN_TIME)}"
code = re.sub(r"(run_time\s*=\s*)(\d+\.?\d*)", lambda m: scale(m, False), code)
code = re.sub(r"(self\.wait\s*\(\s*)(\d+\.?\d*)", lambda m: scale(m, True), 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 Full Render: {orientation} @ {quality} (Timeout: {timeout_sec}s)...", flush=True)
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)
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, stderr_log = process.stdout.decode('utf-8', 'ignore'), 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, stderr_log = (e.stdout.decode('utf-8', 'ignore') if e.stdout else ""), (e.stderr.decode('utf-8', 'ignore') if e.stderr else "")
return None, f"❌ ERROR: Timed out.\n--- MANIM STDOUT ---\n{stdout_log}\n\n--- MANIM STDERR ---\n{stderr_log}", False
media_video_base = os.path.join("media", "videos", "scene")
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)
print(f"✅ Video Render Success: {found_video_path}", flush=True)
return found_video_path, f"✅ Rendering Successful\n\n{full_logs}", 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:
# --- START: New '-s' Pre-Check ---
is_valid, logs = run_manim_pre_check(code)
if not is_valid:
return None, logs, gr.Button(visible=True)
# --- END: New '-s' Pre-Check ---
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)" else scale_animation_times(code, float(preview_factor) or 0.5)
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))
self.wait(1)
"""
with gr.Blocks(title="Manim Render API") as demo:
code_input = gr.Code(label="Python Code", language="python", value=DEFAULT_CODE, visible=True)
orientation_opt = gr.Radio(choices=["Landscape (16:9)", "Portrait (9:16)"], value="Portrait (9:16)", label="Orientation", visible=True)
quality_opt = gr.Dropdown(choices=["Preview (360p)", "480p", "720p", "1080p", "4k"], value="Preview (360p)", label="Quality", visible=True)
timeout_input = gr.Number(label="Render Timeout (seconds)", value=60, visible=True)
preview_speed_factor_input = gr.Number(label="Preview Speed Factor", value=0.5, visible=True)
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=True)
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)