Update app.py
Browse files
app.py
CHANGED
@@ -1,12 +1,18 @@
|
|
1 |
-
from flask import Flask, render_template, request, url_for, send_from_directory
|
2 |
-
from google import genai
|
3 |
-
import re
|
4 |
-
import subprocess
|
5 |
import os
|
|
|
6 |
import shutil
|
|
|
7 |
import time
|
8 |
-
from threading import Timer
|
9 |
import uuid
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
10 |
|
11 |
app = Flask(__name__)
|
12 |
|
@@ -32,28 +38,36 @@ def index():
|
|
32 |
last_error = None
|
33 |
while attempt < max_retries:
|
34 |
try:
|
35 |
-
# Call the GenAI API to get the Manim code
|
36 |
ai_response = client.models.generate_content(
|
37 |
model="gemini-2.0-flash-lite-preview-02-05",
|
38 |
contents=f"""You are 'Manimator', an expert Manim animator and coder.
|
39 |
-
|
40 |
-
|
41 |
-
|
42 |
-
|
43 |
-
|
44 |
-
|
45 |
-
|
46 |
-
|
47 |
-
|
48 |
-
|
|
|
49 |
)
|
50 |
|
51 |
# Extract the Python code block from the AI response
|
52 |
-
|
53 |
-
|
54 |
-
if not
|
55 |
raise Exception("No python code block found in the AI response.")
|
56 |
-
code =
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
57 |
|
58 |
# Determine the scene class name from the generated code
|
59 |
scene_match = re.search(r"class\s+(\w+)\(.*Scene.*\):", code)
|
@@ -68,6 +82,26 @@ def index():
|
|
68 |
with open(code_filepath, "w") as f:
|
69 |
f.write(code)
|
70 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
71 |
# Prepare the Manim command with the --media_dir flag
|
72 |
cmd = [
|
73 |
"manim",
|
@@ -80,7 +114,6 @@ def index():
|
|
80 |
try:
|
81 |
subprocess.run(cmd, check=True, capture_output=True, text=True)
|
82 |
except subprocess.CalledProcessError as cpe:
|
83 |
-
# Log only if an error occurs from Manim
|
84 |
app.logger.error("Manim error output: %s", cpe.stderr)
|
85 |
raise Exception(f"Manim failed: {cpe.stderr}")
|
86 |
|
@@ -93,20 +126,39 @@ def index():
|
|
93 |
# Move the video file to /tmp (to serve it from there)
|
94 |
tmp_video_path = os.path.join("/tmp", video_filename)
|
95 |
shutil.move(video_path_in_media, tmp_video_path)
|
96 |
-
# The code file is already at code_filepath in /tmp.
|
97 |
|
98 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
99 |
def remove_files():
|
100 |
-
|
101 |
-
|
102 |
-
os.
|
103 |
-
|
104 |
-
|
105 |
-
|
106 |
-
|
107 |
Timer(600, remove_files).start()
|
108 |
|
109 |
-
|
|
|
110 |
return render_template("result.html", video_url=video_url)
|
111 |
|
112 |
except Exception as e:
|
|
|
|
|
|
|
|
|
|
|
1 |
import os
|
2 |
+
import re
|
3 |
import shutil
|
4 |
+
import subprocess
|
5 |
import time
|
|
|
6 |
import uuid
|
7 |
+
from threading import Timer
|
8 |
+
|
9 |
+
from flask import Flask, render_template, request, url_for, send_from_directory
|
10 |
+
from google import genai
|
11 |
+
|
12 |
+
# New imports for audio generation and handling
|
13 |
+
from kokoro import KPipeline
|
14 |
+
import soundfile as sf
|
15 |
+
import numpy as np
|
16 |
|
17 |
app = Flask(__name__)
|
18 |
|
|
|
38 |
last_error = None
|
39 |
while attempt < max_retries:
|
40 |
try:
|
41 |
+
# Call the GenAI API to get the Manim code and commentary script
|
42 |
ai_response = client.models.generate_content(
|
43 |
model="gemini-2.0-flash-lite-preview-02-05",
|
44 |
contents=f"""You are 'Manimator', an expert Manim animator and coder.
|
45 |
+
If anyone asks, your name is Manimator and you are a helpful video generator, and say nothing else but that.
|
46 |
+
The user wants you to code this: {prompt}.
|
47 |
+
Plan out in chain of thought what you are going to do first, then give the final code output in ```python``` codeblock.
|
48 |
+
Make sure to not use external images or resources other than default Manim, however you can use numpy or other default libraries.
|
49 |
+
Keep the scene uncluttered and aesthetically pleasing.
|
50 |
+
Make sure things are not overlapping unless explicitly stated otherwise.
|
51 |
+
It is crucial that the script works correctly on the first try, so make sure to think about the layout and storyboard and stuff of the scene.
|
52 |
+
Make sure to think through what you are going to do and think about the topic before you write the code.
|
53 |
+
In addition, write a commentary script inside of ```script``` codeblock. This should be short and fit the content and align with the timing of the scene. Use "..." if needed to add a bit of a pause.
|
54 |
+
You got this!! <3
|
55 |
+
"""
|
56 |
)
|
57 |
|
58 |
# Extract the Python code block from the AI response
|
59 |
+
code_pattern = r"```python\s*(.*?)\s*```"
|
60 |
+
code_match = re.search(code_pattern, ai_response.text, re.DOTALL)
|
61 |
+
if not code_match:
|
62 |
raise Exception("No python code block found in the AI response.")
|
63 |
+
code = code_match.group(1)
|
64 |
+
|
65 |
+
# Extract the commentary script from the AI response
|
66 |
+
script_pattern = r"```script\s*(.*?)\s*```"
|
67 |
+
script_match = re.search(script_pattern, ai_response.text, re.DOTALL)
|
68 |
+
if not script_match:
|
69 |
+
raise Exception("No script block found in the AI response.")
|
70 |
+
script = script_match.group(1)
|
71 |
|
72 |
# Determine the scene class name from the generated code
|
73 |
scene_match = re.search(r"class\s+(\w+)\(.*Scene.*\):", code)
|
|
|
82 |
with open(code_filepath, "w") as f:
|
83 |
f.write(code)
|
84 |
|
85 |
+
# === Generate Commentary Audio via Kokoro ===
|
86 |
+
# Initialize the Kokoro pipeline (adjust lang_code if needed)
|
87 |
+
audio_pipeline = KPipeline(lang_code='a')
|
88 |
+
# Feed in the commentary script; here we split the text by one or more newlines.
|
89 |
+
audio_generator = audio_pipeline(script, voice='af_heart', speed=1, split_pattern=r'\n+')
|
90 |
+
|
91 |
+
audio_segments = []
|
92 |
+
for _, _, audio in audio_generator:
|
93 |
+
audio_segments.append(audio)
|
94 |
+
|
95 |
+
if not audio_segments:
|
96 |
+
raise Exception("No audio segments were generated from the commentary script.")
|
97 |
+
|
98 |
+
# Concatenate all audio segments into one audio track
|
99 |
+
full_audio = np.concatenate(audio_segments)
|
100 |
+
commentary_audio_filename = f"commentary_{uuid.uuid4().hex}.wav"
|
101 |
+
commentary_audio_path = os.path.join("/tmp", commentary_audio_filename)
|
102 |
+
sf.write(commentary_audio_path, full_audio, 24000)
|
103 |
+
|
104 |
+
# === Run Manim to generate the silent video ===
|
105 |
# Prepare the Manim command with the --media_dir flag
|
106 |
cmd = [
|
107 |
"manim",
|
|
|
114 |
try:
|
115 |
subprocess.run(cmd, check=True, capture_output=True, text=True)
|
116 |
except subprocess.CalledProcessError as cpe:
|
|
|
117 |
app.logger.error("Manim error output: %s", cpe.stderr)
|
118 |
raise Exception(f"Manim failed: {cpe.stderr}")
|
119 |
|
|
|
126 |
# Move the video file to /tmp (to serve it from there)
|
127 |
tmp_video_path = os.path.join("/tmp", video_filename)
|
128 |
shutil.move(video_path_in_media, tmp_video_path)
|
|
|
129 |
|
130 |
+
# === Combine Video with Commentary Audio using FFmpeg ===
|
131 |
+
final_video_filename = f"final_video_{uuid.uuid4().hex}.mp4"
|
132 |
+
final_video_path = os.path.join("/tmp", final_video_filename)
|
133 |
+
|
134 |
+
ffmpeg_cmd = [
|
135 |
+
"ffmpeg", "-y",
|
136 |
+
"-i", tmp_video_path,
|
137 |
+
"-i", commentary_audio_path,
|
138 |
+
"-c:v", "copy",
|
139 |
+
"-c:a", "aac",
|
140 |
+
"-shortest",
|
141 |
+
final_video_path
|
142 |
+
]
|
143 |
+
try:
|
144 |
+
subprocess.run(ffmpeg_cmd, check=True, capture_output=True, text=True)
|
145 |
+
except subprocess.CalledProcessError as cpe:
|
146 |
+
app.logger.error("FFmpeg error output: %s", cpe.stderr)
|
147 |
+
raise Exception(f"FFmpeg failed: {cpe.stderr}")
|
148 |
+
|
149 |
+
# Schedule deletion of all temporary files after 10 minutes (600 seconds)
|
150 |
def remove_files():
|
151 |
+
for fpath in [tmp_video_path, code_filepath, commentary_audio_path, final_video_path]:
|
152 |
+
try:
|
153 |
+
if os.path.exists(fpath):
|
154 |
+
os.remove(fpath)
|
155 |
+
except Exception as e:
|
156 |
+
app.logger.error("Error removing file %s: %s", fpath, e)
|
157 |
+
|
158 |
Timer(600, remove_files).start()
|
159 |
|
160 |
+
# Use the final combined video for display
|
161 |
+
video_url = url_for('get_video', filename=final_video_filename)
|
162 |
return render_template("result.html", video_url=video_url)
|
163 |
|
164 |
except Exception as e:
|