memex-in commited on
Commit
5d01c4a
·
verified ·
1 Parent(s): ee20022

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +143 -113
app.py CHANGED
@@ -2,7 +2,7 @@ import asyncio
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,26 +95,87 @@ 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
- # 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}.")
116
  logging.debug(f"Manim stdout:\n{stdout}")
117
  logging.debug(f"Manim stderr:\n{stderr}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
118
  else:
119
  error_msg = f"Manim execution failed for chapter {chapter_number} with return code {process.returncode}:\nStdout:\n{stdout}\nStderr:\n{stderr}"
120
  logging.error(str(error_msg).split('\n')[-1])
@@ -128,41 +189,32 @@ def create_video_from_code(code: str, chapter_number: int) -> str:
128
  except FileNotFoundError:
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
 
167
  async def generate_video(concept: str):
168
  """Generates a video explanation for a given concept using Manim with error correction."""
@@ -170,7 +222,9 @@ async def generate_video(concept: str):
170
  outline = generate_video_outline(concept)
171
  logging.info(f"Video outline: {outline}")
172
 
173
- video_files = []
 
 
174
  for i, chapter in enumerate(outline.chapters):
175
  logging.info(f"Processing chapter {i + 1}: {chapter.title}")
176
  manim_code = generate_manim_code(chapter)
@@ -178,14 +232,24 @@ async def generate_video(concept: str):
178
 
179
  success = False
180
  attempts = 0
181
- max_attempts = 2 # Try fixing the code once
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}")
 
 
 
 
 
 
 
 
 
 
189
  success = True
190
  except subprocess.CalledProcessError as e:
191
  attempts += 1
@@ -196,14 +260,10 @@ async def generate_video(concept: str):
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
@@ -211,6 +271,11 @@ async def generate_video(concept: str):
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
 
215
  if not success:
216
  logging.error(f"Failed to generate video for chapter {i + 1} after {max_attempts} attempts. Skipping chapter.")
@@ -219,17 +284,17 @@ async def generate_video(concept: str):
219
 
220
  # Combine the video files
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"
@@ -244,53 +309,18 @@ async def generate_video(concept: str):
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):
253
- os.remove(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
-
270
- def main():
271
- st.title("Explanatory Video Generator")
272
- concept = st.text_input("Enter the concept for the video:")
273
-
274
- if st.button("Generate Video"):
275
- if concept:
276
- with st.spinner("Generating video... This might take a few minutes."):
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:
293
- st.warning("Please enter a concept.")
294
-
295
- if __name__ == "__main__":
296
- main()
 
2
  import os
3
  import shutil
4
  import subprocess
5
+ import tempfile
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
+ temp_dir = None
99
+ temp_file_name = None
100
+ video_file_path = None
 
 
101
 
 
102
  try:
103
+ # Create a temporary directory for Manim's output
104
+ temp_dir = tempfile.mkdtemp()
105
+ logging.info(f"Created temporary directory for Manim output: {temp_dir}")
106
+
107
+ # Use tempfile to create a temporary Python file within the temporary directory
108
+ with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False, dir=temp_dir) as temp_file:
109
+ temp_file.write(code)
110
+ temp_file_name = temp_file.name
111
+ logging.info(f"Created temporary Manim script: {temp_file_name}")
112
+
113
+ process = None
114
+ # Manim command needs to specify the output directory
115
+ # The -o flag followed by a filename will output the video to that specific path relative to the current working directory,
116
+ # or an absolute path if provided.
117
+ # Alternatively, we can use -o with just the class name and specify the output directory using --output_directory
118
+ # For simplicity, we'll try to explicitly specify the output path if the Manim version supports it,
119
+ # or we'll ensure the manim command is executed from within the temporary directory.
120
+
121
+ # We need to extract the class name to get Manim's default filename
122
+ match = re.search(r"class\s+(\w+)\(Scene\):", code)
123
+ if not match:
124
+ raise ValueError(f"Could not extract class name from Manim code for chapter {chapter_number}")
125
+ class_name = match.group(1)
126
+ expected_output_filename = f"{class_name}.mp4"
127
+
128
+ # Construct the command. We will run manim from the temporary directory.
129
+ # Manim by default creates media/videos/<resolution>/<filename>.mp4
130
+ # We want it in our temp_dir. The easiest way is to change the CWD.
131
+ # So the manim command becomes `manim <script_name> -ql --media_dir <temp_dir>`
132
+ # This will put the videos inside <temp_dir>/videos/<resolution>/
133
+ command = [
134
+ "manim",
135
+ os.path.basename(temp_file_name), # Just the filename, as we're changing CWD
136
+ "-ql",
137
+ "--disable_caching",
138
+ "--media_dir", # Specify the media directory directly
139
+ temp_dir
140
+ ]
141
+ logging.info(f"Executing Manim command in '{temp_dir}': {' '.join(command)}")
142
+
143
+ # Execute Manim from within the temporary directory
144
+ process = subprocess.Popen(
145
+ command,
146
+ stdout=subprocess.PIPE,
147
+ stderr=subprocess.PIPE,
148
+ cwd=temp_dir, # Crucial: set the current working directory for the subprocess
149
+ shell=True,
150
+ text=True
151
+ )
152
+ stdout, stderr = process.communicate(timeout=120)
153
 
154
  if process.returncode == 0:
155
  logging.info(f"Manim execution successful for chapter {chapter_number}.")
156
  logging.debug(f"Manim stdout:\n{stdout}")
157
  logging.debug(f"Manim stderr:\n{stderr}")
158
+ # Manim 0.16.0+ changed output paths slightly.
159
+ # It usually creates a structure like <media_dir>/videos/<quality>/<scene_name>.mp4
160
+ # We explicitly set media_dir to temp_dir, so the video should be in:
161
+ # temp_dir/videos/temp/480p15/<class_name>.mp4
162
+ # Or if --output_file is used, it directly goes to temp_dir/<filename>.
163
+ # With --media_dir, it will stick to its internal folder structure.
164
+ manim_rendered_video_subdir = "videos/temp/480p15" # This is Manim's internal structure
165
+ video_file_path_in_temp = os.path.join(temp_dir, manim_rendered_video_subdir, expected_output_filename)
166
+
167
+ if not os.path.exists(video_file_path_in_temp):
168
+ logging.error(f"Manim output file not found at expected path: {video_file_path_in_temp}")
169
+ # As a fallback, try to find it directly in temp_dir if Manim changed its behavior or a custom flag was added
170
+ fallback_path = os.path.join(temp_dir, expected_output_filename)
171
+ if os.path.exists(fallback_path):
172
+ logging.info(f"Found video at fallback path: {fallback_path}")
173
+ video_file_path = fallback_path
174
+ else:
175
+ raise FileNotFoundError(f"Manim output video file '{expected_output_filename}' not found at '{video_file_path_in_temp}' or '{fallback_path}' after successful execution. Check Manim's output configuration.")
176
+ else:
177
+ video_file_path = video_file_path_in_temp
178
+
179
  else:
180
  error_msg = f"Manim execution failed for chapter {chapter_number} with return code {process.returncode}:\nStdout:\n{stdout}\nStderr:\n{stderr}"
181
  logging.error(str(error_msg).split('\n')[-1])
 
189
  except FileNotFoundError:
190
  logging.error("Error: The 'manim' command was not found. Ensure Manim is installed and in your system's PATH.")
191
  raise
192
+ except Exception as e:
193
+ logging.error(f"An unexpected error occurred during video creation: {e}")
194
+ raise
195
  finally:
196
  # Ensure the temporary file is deleted after Manim tries to use it
197
+ if temp_file_name and os.path.exists(temp_file_name):
198
  os.remove(temp_file_name)
199
  logging.info(f"Deleted temporary Manim script: {temp_file_name}")
200
+ # Clean up the entire temporary directory created for Manim output
201
+ # It's important to do this AFTER the video file has been moved or processed
202
+ if temp_dir and os.path.exists(temp_dir):
203
+ try:
204
+ # Need to wait until the video is processed/moved by the calling function.
205
+ # For now, let's assume the video_file_path returned will be copied.
206
+ # If we don't copy, we need to defer this cleanup.
207
+ # Since the final combination happens in `generate_video`,
208
+ # it's better to clean up the `temp_dir` there after all clips are loaded.
209
+ # For now, we'll only clean up the script itself.
210
+ pass # Shifting temp_dir cleanup to generate_video for safer handling
211
+ except Exception as e:
212
+ logging.error(f"Error cleaning up temporary directory {temp_dir}: {e}")
213
 
214
+ # The video_file_path now points to a file within temp_dir.
215
+ # The calling function (generate_video) will need to load this using moviepy,
216
+ # and then we can clean up the temp_dir after all clips are loaded.
217
+ return video_file_path
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
218
 
219
  async def generate_video(concept: str):
220
  """Generates a video explanation for a given concept using Manim with error correction."""
 
222
  outline = generate_video_outline(concept)
223
  logging.info(f"Video outline: {outline}")
224
 
225
+ video_files_paths = [] # Renamed for clarity, these are paths in temp dirs
226
+ temp_dirs_to_clean = [] # To keep track of temporary directories to delete
227
+
228
  for i, chapter in enumerate(outline.chapters):
229
  logging.info(f"Processing chapter {i + 1}: {chapter.title}")
230
  manim_code = generate_manim_code(chapter)
 
232
 
233
  success = False
234
  attempts = 0
235
+ max_attempts = 2
236
 
237
  while attempts < max_attempts and not success:
238
  try:
239
  st.info(f"Attempting to render chapter {i + 1}: {chapter.title} (Attempt {attempts + 1}/{max_attempts})")
240
+ # create_video_from_code will return the path within a temp dir
241
+ video_file_path_for_chapter = create_video_from_code(manim_code, i + 1)
242
+ video_files_paths.append(video_file_path_for_chapter)
243
+ # We need to extract the base temp directory for cleanup later
244
+ # It's usually the parent of 'videos/temp/480p15'
245
+ # Find the root temp directory path from video_file_path_for_chapter
246
+ # Example: /tmp/tmpxyz/videos/temp/480p15/MyScene.mp4 -> /tmp/tmpxyz
247
+ # This assumes temp_dir is directly below the media/videos structure
248
+ root_temp_dir = os.path.abspath(os.path.join(video_file_path_for_chapter, os.pardir, os.pardir, os.pardir))
249
+ if root_temp_dir not in temp_dirs_to_clean:
250
+ temp_dirs_to_clean.append(root_temp_dir)
251
+
252
+ logging.info(f"Video file created for chapter {i + 1}: {video_file_path_for_chapter}")
253
  success = True
254
  except subprocess.CalledProcessError as e:
255
  attempts += 1
 
260
  except ValueError as e:
261
  logging.error(f"Error processing Manim code for chapter {i + 1}: {e}")
262
  st.error(f"Error processing chapter {i + 1}: {e}")
263
+ return None
264
  except FileNotFoundError as e:
265
+ logging.error(f"Manim or output file not found for chapter {i + 1}: {e}")
266
+ st.error(f"Manim or its output file was not found for chapter {i + 1}: {e}. Please ensure Manim is installed and configured correctly.")
 
 
 
 
267
  return None
268
  except subprocess.TimeoutExpired:
269
  attempts += 1
 
271
  st.warning(f"Manim rendering timed out for chapter {i + 1}. Attempting to fix code...")
272
  manim_code = fix_manim_code(f"Manim process timed out.", manim_code)
273
  logging.debug(f"Fixed Manim code (Attempt {attempts}):\n{manim_code}")
274
+ except Exception as e:
275
+ logging.error(f"An unexpected error occurred during chapter {i + 1} processing: {e}")
276
+ st.error(f"An unexpected error occurred for chapter {i + 1}: {e}")
277
+ return None
278
+
279
 
280
  if not success:
281
  logging.error(f"Failed to generate video for chapter {i + 1} after {max_attempts} attempts. Skipping chapter.")
 
284
 
285
  # Combine the video files
286
  final_video_path = None
287
+ if video_files_paths:
288
  logging.info("Combining video files...")
289
  st.info("Combining all chapter videos...")
290
+ clips = []
291
  try:
292
+ for vf_path in video_files_paths:
293
+ if os.path.exists(vf_path):
294
+ clips.append(VideoFileClip(vf_path))
 
295
  else:
296
+ logging.warning(f"Skipping non-existent video file for concatenation: {vf_path}")
297
+ st.warning(f"Skipping missing chapter video for final combination: {vf_path.split('/')[-1]}")
298
 
299
  if clips:
300
  final_video_path = f"final_video.mp4"
 
309
  except Exception as e:
310
  logging.error(f"Error combining video files: {e}")
311
  st.error(f"Error combining video files: {e}")
312
+ finally:
313
+ # Clean up all temporary directories after all clips are loaded and combined
314
+ for clip in clips: # Ensure all clips are closed
315
+ try:
316
+ clip.close()
317
+ except Exception as close_e:
318
+ logging.warning(f"Error closing clip: {close_e}")
319
+
320
+ for temp_dir_path in temp_dirs_to_clean:
321
+ try:
322
+ if os.path.exists(temp_dir_path):
323
+ shutil.rmtree(temp_dir_path) # Recursively remove the temporary directory
324
+ logging.info(f"Deleted temporary directory: {temp_dir_path}")
325
+ except Exception as e:
326
+ logging.error(f"Error deleting temporary directory {temp_dir_path}: {e}")