Update app.py
Browse files
app.py
CHANGED
@@ -2,7 +2,7 @@ import asyncio
|
|
2 |
import os
|
3 |
import shutil
|
4 |
import subprocess
|
5 |
-
import tempfile
|
6 |
from typing import List
|
7 |
import logging
|
8 |
|
@@ -95,15 +95,21 @@ def generate_video_outline(concept: str) -> VideoOutline:
|
|
95 |
|
96 |
def create_video_from_code(code: str, chapter_number: int) -> str:
|
97 |
"""Creates a video from Manim code and returns the video file path using subprocess.Popen."""
|
98 |
-
|
|
|
|
|
99 |
temp_file.write(code)
|
100 |
temp_file_name = temp_file.name
|
101 |
|
102 |
process = None
|
103 |
try:
|
|
|
|
|
|
|
104 |
command = ["manim", temp_file_name, "-ql", "--disable_caching"]
|
|
|
105 |
process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True, text=True)
|
106 |
-
stdout, stderr = process.communicate(timeout=
|
107 |
|
108 |
if process.returncode == 0:
|
109 |
logging.info(f"Manim execution successful for chapter {chapter_number}.")
|
@@ -123,17 +129,38 @@ def create_video_from_code(code: str, chapter_number: int) -> str:
|
|
123 |
logging.error("Error: The 'manim' command was not found. Ensure Manim is installed and in your system's PATH.")
|
124 |
raise
|
125 |
finally:
|
126 |
-
|
127 |
-
|
128 |
-
|
|
|
129 |
|
130 |
# Construct the video file name. Manim names the file based on the class name.
|
131 |
-
#
|
132 |
match = re.search(r"class\s+(\w+)\(Scene\):", code)
|
133 |
if match:
|
134 |
class_name = match.group(1)
|
135 |
video_file_name = f"{class_name}.mp4"
|
136 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
137 |
else:
|
138 |
raise ValueError(f"Could not extract class name from Manim code for chapter {chapter_number}")
|
139 |
|
@@ -155,6 +182,7 @@ async def generate_video(concept: str):
|
|
155 |
|
156 |
while attempts < max_attempts and not success:
|
157 |
try:
|
|
|
158 |
video_file = create_video_from_code(manim_code, i + 1)
|
159 |
video_files.append(video_file)
|
160 |
logging.info(f"Video file created for chapter {i + 1}: {video_file}")
|
@@ -162,19 +190,25 @@ async def generate_video(concept: str):
|
|
162 |
except subprocess.CalledProcessError as e:
|
163 |
attempts += 1
|
164 |
logging.error(f"Manim execution failed for chapter {i + 1} (Attempt {attempts}): {e}")
|
165 |
-
|
166 |
manim_code = fix_manim_code(str(e), manim_code)
|
167 |
logging.debug(f"Fixed Manim code (Attempt {attempts}):\n{manim_code}")
|
168 |
except ValueError as e:
|
169 |
logging.error(f"Error processing Manim code for chapter {i + 1}: {e}")
|
170 |
st.error(f"Error processing chapter {i + 1}: {e}")
|
171 |
return None # Stop processing if a critical error occurs with code structure
|
172 |
-
except FileNotFoundError:
|
173 |
-
|
174 |
-
|
|
|
|
|
|
|
|
|
175 |
return None
|
176 |
except subprocess.TimeoutExpired:
|
177 |
-
|
|
|
|
|
178 |
manim_code = fix_manim_code(f"Manim process timed out.", manim_code)
|
179 |
logging.debug(f"Fixed Manim code (Attempt {attempts}):\n{manim_code}")
|
180 |
|
@@ -187,8 +221,16 @@ async def generate_video(concept: str):
|
|
187 |
final_video_path = None
|
188 |
if video_files:
|
189 |
logging.info("Combining video files...")
|
|
|
190 |
try:
|
191 |
-
clips = [
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
192 |
if clips:
|
193 |
final_video_path = f"final_video.mp4"
|
194 |
final_clip = concatenate_videoclips(clips)
|
@@ -198,12 +240,13 @@ async def generate_video(concept: str):
|
|
198 |
st.success("Video generation complete!")
|
199 |
else:
|
200 |
logging.warning("No valid video files to combine.")
|
201 |
-
st.warning("No valid video files were generated.")
|
202 |
except Exception as e:
|
203 |
logging.error(f"Error combining video files: {e}")
|
204 |
st.error(f"Error combining video files: {e}")
|
205 |
|
206 |
-
# Clean up intermediate video files
|
|
|
207 |
for video_file in video_files:
|
208 |
try:
|
209 |
if os.path.exists(video_file):
|
@@ -211,9 +254,16 @@ async def generate_video(concept: str):
|
|
211 |
logging.info(f"Deleted intermediate video file: {video_file}")
|
212 |
except Exception as e:
|
213 |
logging.error(f"Error deleting intermediate video file {video_file}: {e}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
214 |
else:
|
215 |
logging.warning("No video files to combine.")
|
216 |
-
st.
|
217 |
|
218 |
return final_video_path
|
219 |
|
@@ -227,8 +277,16 @@ def main():
|
|
227 |
final_video_file = asyncio.run(generate_video(concept))
|
228 |
if final_video_file and os.path.exists(final_video_file):
|
229 |
st.video(final_video_file)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
230 |
elif final_video_file:
|
231 |
-
st.error("Error: Final video file not found.")
|
232 |
else:
|
233 |
st.info("Video generation process completed without creating a final video.")
|
234 |
else:
|
|
|
2 |
import os
|
3 |
import shutil
|
4 |
import subprocess
|
5 |
+
import tempfile # Import the tempfile module
|
6 |
from typing import List
|
7 |
import logging
|
8 |
|
|
|
95 |
|
96 |
def create_video_from_code(code: str, chapter_number: int) -> str:
|
97 |
"""Creates a video from Manim code and returns the video file path using subprocess.Popen."""
|
98 |
+
# Use tempfile to create a temporary Python file
|
99 |
+
# This ensures that the script has write permissions and handles cleanup
|
100 |
+
with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as temp_file:
|
101 |
temp_file.write(code)
|
102 |
temp_file_name = temp_file.name
|
103 |
|
104 |
process = None
|
105 |
try:
|
106 |
+
# Manim needs the class name from the code to correctly generate the video file name.
|
107 |
+
# It also implicitly determines the output path based on the current working directory
|
108 |
+
# unless specified. We will get the output path later.
|
109 |
command = ["manim", temp_file_name, "-ql", "--disable_caching"]
|
110 |
+
logging.info(f"Executing Manim command: {' '.join(command)}")
|
111 |
process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True, text=True)
|
112 |
+
stdout, stderr = process.communicate(timeout=120) # Increased timeout for potentially longer renders
|
113 |
|
114 |
if process.returncode == 0:
|
115 |
logging.info(f"Manim execution successful for chapter {chapter_number}.")
|
|
|
129 |
logging.error("Error: The 'manim' command was not found. Ensure Manim is installed and in your system's PATH.")
|
130 |
raise
|
131 |
finally:
|
132 |
+
# Ensure the temporary file is deleted after Manim tries to use it
|
133 |
+
if os.path.exists(temp_file_name):
|
134 |
+
os.remove(temp_file_name)
|
135 |
+
logging.info(f"Deleted temporary Manim script: {temp_file_name}")
|
136 |
|
137 |
# Construct the video file name. Manim names the file based on the class name.
|
138 |
+
# The output directory for Manim in this configuration is ./media/videos/temp/480p15/
|
139 |
match = re.search(r"class\s+(\w+)\(Scene\):", code)
|
140 |
if match:
|
141 |
class_name = match.group(1)
|
142 |
video_file_name = f"{class_name}.mp4"
|
143 |
+
# Manim renders to a specific default output path if not specified
|
144 |
+
# Adjust this path if your Manim configuration or command differs
|
145 |
+
manim_output_dir = "./media/videos/temp/480p15/"
|
146 |
+
# Ensure the output directory exists
|
147 |
+
os.makedirs(manim_output_dir, exist_ok=True)
|
148 |
+
video_file_path = os.path.join(manim_output_dir, video_file_name)
|
149 |
+
|
150 |
+
# In some cases, Manim might output to the current directory before moving it.
|
151 |
+
# We need to make sure we're checking the correct path where Manim places the final video.
|
152 |
+
# A more robust solution might involve parsing Manim's stdout for the actual path.
|
153 |
+
# For now, let's assume the default output path.
|
154 |
+
if not os.path.exists(video_file_path):
|
155 |
+
logging.warning(f"Expected Manim output at {video_file_path} but not found. Checking current directory for {video_file_name}.")
|
156 |
+
# Fallback check in current directory, although not typical for default Manim behavior
|
157 |
+
if os.path.exists(video_file_name):
|
158 |
+
video_file_path = video_file_name
|
159 |
+
logging.info(f"Found video in current directory: {video_file_name}")
|
160 |
+
else:
|
161 |
+
raise FileNotFoundError(f"Manim output video file '{video_file_name}' not found at '{video_file_path}' or current directory after successful execution.")
|
162 |
+
|
163 |
+
return video_file_path
|
164 |
else:
|
165 |
raise ValueError(f"Could not extract class name from Manim code for chapter {chapter_number}")
|
166 |
|
|
|
182 |
|
183 |
while attempts < max_attempts and not success:
|
184 |
try:
|
185 |
+
st.info(f"Attempting to render chapter {i + 1}: {chapter.title} (Attempt {attempts + 1}/{max_attempts})")
|
186 |
video_file = create_video_from_code(manim_code, i + 1)
|
187 |
video_files.append(video_file)
|
188 |
logging.info(f"Video file created for chapter {i + 1}: {video_file}")
|
|
|
190 |
except subprocess.CalledProcessError as e:
|
191 |
attempts += 1
|
192 |
logging.error(f"Manim execution failed for chapter {i + 1} (Attempt {attempts}): {e}")
|
193 |
+
st.warning(f"Manim rendering failed for chapter {i + 1}. Attempting to fix code...")
|
194 |
manim_code = fix_manim_code(str(e), manim_code)
|
195 |
logging.debug(f"Fixed Manim code (Attempt {attempts}):\n{manim_code}")
|
196 |
except ValueError as e:
|
197 |
logging.error(f"Error processing Manim code for chapter {i + 1}: {e}")
|
198 |
st.error(f"Error processing chapter {i + 1}: {e}")
|
199 |
return None # Stop processing if a critical error occurs with code structure
|
200 |
+
except FileNotFoundError as e:
|
201 |
+
if "Manim output video file" in str(e):
|
202 |
+
logging.error(f"Manim rendering failed to produce expected output file for chapter {i + 1}: {e}")
|
203 |
+
st.error(f"Manim rendering failed to produce video for chapter {i + 1}. Please check Manim installation and configuration.")
|
204 |
+
else:
|
205 |
+
logging.error(f"Manim not found or output path issue: {e}")
|
206 |
+
st.error("Manim not found. Please ensure it's installed and in your PATH, or check Manim's output configuration.")
|
207 |
return None
|
208 |
except subprocess.TimeoutExpired:
|
209 |
+
attempts += 1
|
210 |
+
logging.error(f"Manim process timed out for chapter {i + 1}. Attempt {attempts}. Attempting to fix...")
|
211 |
+
st.warning(f"Manim rendering timed out for chapter {i + 1}. Attempting to fix code...")
|
212 |
manim_code = fix_manim_code(f"Manim process timed out.", manim_code)
|
213 |
logging.debug(f"Fixed Manim code (Attempt {attempts}):\n{manim_code}")
|
214 |
|
|
|
221 |
final_video_path = None
|
222 |
if video_files:
|
223 |
logging.info("Combining video files...")
|
224 |
+
st.info("Combining all chapter videos...")
|
225 |
try:
|
226 |
+
clips = []
|
227 |
+
for vf in video_files:
|
228 |
+
if os.path.exists(vf):
|
229 |
+
clips.append(VideoFileClip(vf))
|
230 |
+
else:
|
231 |
+
logging.warning(f"Skipping non-existent video file: {vf}")
|
232 |
+
st.warning(f"Skipping missing chapter video: {vf.split('/')[-1]}")
|
233 |
+
|
234 |
if clips:
|
235 |
final_video_path = f"final_video.mp4"
|
236 |
final_clip = concatenate_videoclips(clips)
|
|
|
240 |
st.success("Video generation complete!")
|
241 |
else:
|
242 |
logging.warning("No valid video files to combine.")
|
243 |
+
st.warning("No valid video files were generated to combine.")
|
244 |
except Exception as e:
|
245 |
logging.error(f"Error combining video files: {e}")
|
246 |
st.error(f"Error combining video files: {e}")
|
247 |
|
248 |
+
# Clean up intermediate video files and the temporary Manim output directory if it's no longer needed
|
249 |
+
temp_manim_output_dir = "./media/videos/temp/480p15/"
|
250 |
for video_file in video_files:
|
251 |
try:
|
252 |
if os.path.exists(video_file):
|
|
|
254 |
logging.info(f"Deleted intermediate video file: {video_file}")
|
255 |
except Exception as e:
|
256 |
logging.error(f"Error deleting intermediate video file {video_file}: {e}")
|
257 |
+
# Optionally, remove the entire temp Manim output directory if it's empty
|
258 |
+
try:
|
259 |
+
if os.path.exists(temp_manim_output_dir) and not os.listdir(temp_manim_output_dir):
|
260 |
+
os.rmdir(temp_manim_output_dir)
|
261 |
+
logging.info(f"Deleted empty Manim temp output directory: {temp_manim_output_dir}")
|
262 |
+
except Exception as e:
|
263 |
+
logging.error(f"Error deleting empty Manim temp output directory {temp_manim_output_dir}: {e}")
|
264 |
else:
|
265 |
logging.warning("No video files to combine.")
|
266 |
+
st.info("No video files were successfully generated.")
|
267 |
|
268 |
return final_video_path
|
269 |
|
|
|
277 |
final_video_file = asyncio.run(generate_video(concept))
|
278 |
if final_video_file and os.path.exists(final_video_file):
|
279 |
st.video(final_video_file)
|
280 |
+
# Provide a download button for the video
|
281 |
+
with open(final_video_file, "rb") as file:
|
282 |
+
st.download_button(
|
283 |
+
label="Download Video",
|
284 |
+
data=file,
|
285 |
+
file_name=os.path.basename(final_video_file),
|
286 |
+
mime="video/mp4"
|
287 |
+
)
|
288 |
elif final_video_file:
|
289 |
+
st.error("Error: Final video file not found after generation.")
|
290 |
else:
|
291 |
st.info("Video generation process completed without creating a final video.")
|
292 |
else:
|