Spaces:
Running
Running
import gradio as gr | |
import os | |
import tempfile | |
import subprocess | |
import shutil | |
import logging | |
import time | |
from openai import OpenAI | |
from dotenv import load_dotenv | |
load_dotenv() | |
logging.basicConfig(level=logging.INFO) | |
logger = logging.getLogger(__name__) | |
def get_client(): | |
return OpenAI( | |
api_key=os.environ.get("TOGETHER_API_KEY"), | |
base_url="https://api.together.xyz/v1" | |
) | |
AVAILABLE_MODELS = [ | |
"meta-llama/Llama-3.3-70B-Instruct-Turbo", | |
"deepseek-ai/DeepSeek-V3", | |
"deepseek-ai/DeepSeek-R1", | |
"Qwen/QwQ-32B-Preview", | |
"meta-llama/Llama-3.3-70B-Instruct-Turbo-Free", | |
"Qwen/Qwen2.5-Coder-32B-Instruct" | |
] | |
def generate_manim_code(prompt, model_name, temperature=0.7, max_tokens=8192): | |
try: | |
client = get_client() | |
system_prompt = """ | |
You are an expert in creating mathematical and physics visualizations using Manim (Mathematical Animation Engine). | |
Your task is to convert a text prompt into valid, executable Manim Python code. | |
IMPORTANT RULES FOR COMPILATION SUCCESS: | |
1. Only return valid Python code that works with the latest version of Manim Community edition | |
2. Do NOT include any explanations outside of code comments | |
3. Use ONLY the Scene class as the base class | |
4. Include ALL necessary imports at the top (from manim import *) | |
5. Use descriptive variable names that follow Python conventions | |
6. Include helpful comments for complex parts of the visualization | |
7. The class name MUST be "Screen" - always use this exact name | |
8. Always implement the construct method correctly | |
9. Ensure all objects are properly added to the scene with self.play() or self.add() | |
10. Do not create custom classes other than the main Scene class | |
11. Include proper self.wait() calls after animations for better viewing | |
12. Check all mathematical expressions are valid LaTeX syntax | |
13. Avoid advanced or experimental Manim features that might not be widely available | |
14. Keep animations under 20 seconds total for better performance | |
15. Ensure all coordinates and dimensions are appropriate for the default canvas size | |
16. DO NOT include any backticks (```) or markdown formatting in your response | |
RESPOND WITH ONLY THE EXECUTABLE PYTHON CODE, NO INTRODUCTION OR EXPLANATION, NO MARKDOWN FORMATTING. | |
""" | |
final_prompt = f"Create a Manim visualization that explains: {prompt}" | |
logger.info(f"Generating code with model: {model_name}") | |
response = client.chat.completions.create( | |
model=model_name, | |
temperature=temperature, | |
max_tokens=max_tokens, | |
messages=[ | |
{"role": "system", "content": system_prompt}, | |
{"role": "user", "content": final_prompt} | |
] | |
) | |
generated_code = response.choices[0].message.content | |
# Strip markdown formatting if it appears in the response | |
if "```python" in generated_code: | |
generated_code = generated_code.split("```python")[1] | |
if "```" in generated_code: | |
generated_code = generated_code.split("```")[0] | |
elif "```" in generated_code: | |
generated_code = generated_code.split("```")[1] | |
if "```" in generated_code: | |
generated_code = generated_code.split("```")[0] | |
# Remove any additional backticks that might cause syntax errors | |
generated_code = generated_code.replace('```', '') | |
# Ensure code starts with proper import | |
if not generated_code.strip().startswith('from manim import'): | |
generated_code = 'from manim import *\n\n' + generated_code | |
return generated_code.strip() | |
except Exception as e: | |
logger.error(f"Error generating code: {e}") | |
return f"Error generating code: {str(e)}" | |
def render_manim_video(code, quality="medium_quality"): | |
try: | |
temp_dir = tempfile.mkdtemp() | |
script_path = os.path.join(temp_dir, "manim_script.py") | |
with open(script_path, "w") as f: | |
f.write(code) | |
class_name = None | |
for line in code.split("\n"): | |
if line.startswith("class ") and "Scene" in line: | |
class_name = line.split("class ")[1].split("(")[0].strip() | |
break | |
if not class_name: | |
return "Error: Could not identify the Scene class in the generated code." | |
if quality == "high_quality": | |
command = ["manim", "-qh", script_path, class_name] | |
quality_dir = "1080p60" | |
elif quality == "low_quality": | |
command = ["manim", "-ql", script_path, class_name] | |
quality_dir = "480p15" | |
else: | |
command = ["manim", "-qm", script_path, class_name] | |
quality_dir = "720p30" | |
logger.info(f"Executing command: {' '.join(command)}") | |
result = subprocess.run(command, cwd=temp_dir, capture_output=True, text=True) | |
logger.info(f"Manim stdout: {result.stdout}") | |
logger.error(f"Manim stderr: {result.stderr}") | |
if result.returncode != 0: | |
logger.error(f"Manim execution failed: {result.stderr}") | |
return f"Error rendering video: {result.stderr}" | |
media_dir = os.path.join(temp_dir, "media") | |
videos_dir = os.path.join(media_dir, "videos") | |
if not os.path.exists(videos_dir): | |
return "Error: No video was generated. Check if Manim is installed correctly." | |
scene_dirs = [d for d in os.listdir(videos_dir) if os.path.isdir(os.path.join(videos_dir, d))] | |
if not scene_dirs: | |
return "Error: No scene directory found in the output." | |
scene_dir = max([os.path.join(videos_dir, d) for d in scene_dirs], key=os.path.getctime) | |
mp4_files = [f for f in os.listdir(os.path.join(scene_dir, quality_dir)) if f.endswith(".mp4")] | |
if not mp4_files: | |
return "Error: No MP4 file was generated." | |
video_file = max([os.path.join(scene_dir, quality_dir, f) for f in mp4_files], key=os.path.getctime) | |
output_dir = os.path.join(os.getcwd(), "generated_videos") | |
os.makedirs(output_dir, exist_ok=True) | |
timestamp = int(time.time()) | |
output_file = os.path.join(output_dir, f"manim_video_{timestamp}.mp4") | |
shutil.copy2(video_file, output_file) | |
logger.info(f"Video generated: {output_file}") | |
return output_file | |
except Exception as e: | |
logger.error(f"Error rendering video: {e}") | |
return f"Error rendering video: {str(e)}" | |
finally: | |
if 'temp_dir' in locals(): | |
try: | |
shutil.rmtree(temp_dir) | |
except Exception as e: | |
logger.error(f"Error cleaning up temporary directory: {e}") | |
def placeholder_for_examples(prompt, model, quality): | |
code = """ | |
from manim import * | |
class PythagoreanTheorem(Scene): | |
def construct(self): | |
# This is placeholder code for examples | |
# Creating a right triangle | |
triangle = Polygon( | |
ORIGIN, | |
RIGHT * 3, | |
UP * 4, | |
color=WHITE | |
) | |
# Adding labels | |
a = Text("a", font_size=30).next_to(triangle, DOWN) | |
b = Text("b", font_size=30).next_to(triangle, RIGHT) | |
c = Text("c", font_size=30).next_to( | |
triangle.get_center(), | |
UP + LEFT | |
) | |
# Add to scene | |
self.play(Create(triangle)) | |
self.play(Write(a), Write(b), Write(c)) | |
# Wait at the end | |
self.wait(2) | |
""" | |
return code, None, "Example mode: Click 'Generate Video' to actually process this example" | |
def process_prompt(prompt, model_name, quality="medium_quality"): | |
try: | |
code = generate_manim_code(prompt, model_name) | |
video_path = render_manim_video(code, quality) | |
return code, video_path | |
except Exception as e: | |
logger.error(f"Error processing prompt: {e}") | |
return f"Error: {str(e)}", None | |
def process_prompt_with_status(prompt, model, quality, progress=gr.Progress()): | |
try: | |
progress(0, desc="Starting...") | |
progress(0.3, desc="Generating Manim code using AI...") | |
code = generate_manim_code(prompt, model) | |
progress(0.6, desc="Rendering video with Manim (this may take a few minutes)...") | |
video_path = render_manim_video(code, quality) | |
progress(1.0, desc="Complete") | |
if not video_path or video_path.startswith("Error"): | |
status = video_path if video_path else "Error: Failed to generate video." | |
return code, None, status | |
else: | |
status = "Video generated successfully!" | |
return code, video_path, status | |
except Exception as e: | |
logger.error(f"Error in processing: {e}") | |
return (code if 'code' in locals() else "Error generating code"), None, f"Error: {str(e)}" | |
def create_interface(): | |
with gr.Blocks(title="Math & Physics Video Generator") as app: | |
gr.Markdown("# Interactive Math & Physics Video Generator") | |
gr.Markdown("Generate educational videos from text prompts using AI and Manim") | |
with gr.Row(): | |
with gr.Column(): | |
model_dropdown = gr.Dropdown( | |
choices=AVAILABLE_MODELS, | |
value=AVAILABLE_MODELS[1], | |
label="Select AI Model" | |
) | |
quality_radio = gr.Radio( | |
choices=["low_quality", "medium_quality", "high_quality"], | |
value="medium_quality", | |
label="Output Quality (affects rendering time)" | |
) | |
prompt_input = gr.Textbox( | |
placeholder="Enter a mathematical or physics concept to visualize...", | |
label="Prompt", | |
lines=3 | |
) | |
submit_btn = gr.Button("Generate Video", variant="primary") | |
with gr.Accordion("Generated Manim Code", open=False): | |
code_output = gr.Code( | |
language="python", | |
label="Generated Manim Code", | |
lines=20 | |
) | |
with gr.Column(): | |
video_output = gr.Video( | |
label="Generated Animation", | |
width="100%", | |
height=500 | |
) | |
status_output = gr.Textbox( | |
label="Status", | |
value="Ready. Enter a prompt and click 'Generate Video'.", | |
interactive=False | |
) | |
submit_btn.click( | |
fn=process_prompt_with_status, | |
inputs=[prompt_input, model_dropdown, quality_radio], | |
outputs=[code_output, video_output, status_output] | |
) | |
gr.Examples( | |
examples=[ | |
["Explain the Pythagorean theorem", AVAILABLE_MODELS[1], "medium_quality"], | |
["Show how a pendulum works with damping", AVAILABLE_MODELS[1], "medium_quality"], | |
["Demonstrate the concept of derivatives in calculus", AVAILABLE_MODELS[1], "medium_quality"], | |
["Visualize the wave function of a particle in a box", AVAILABLE_MODELS[1], "medium_quality"], | |
["Explain how a capacitor charges and discharges", AVAILABLE_MODELS[1], "medium_quality"] | |
], | |
inputs=[prompt_input, model_dropdown, quality_radio], | |
fn=placeholder_for_examples | |
) | |
return app | |
if __name__ == "__main__": | |
app = create_interface() | |
app.launch(share=True) |