Spaces:
Running
Running
| import argparse | |
| import glob | |
| import os | |
| import sys | |
| from typing import List, Tuple | |
| import natsort | |
| from moviepy import * | |
| from pdf2image import convert_from_path | |
| def parse_arguments(): | |
| """Parse command line arguments.""" | |
| parser = argparse.ArgumentParser( | |
| description="Create a video from PDF slides and audio files." | |
| ) | |
| parser.add_argument( | |
| "--pdf", required=True, help="Path to the PDF file containing slides" | |
| ) | |
| parser.add_argument( | |
| "--audio-dir", required=True, help="Directory containing audio files" | |
| ) | |
| parser.add_argument( | |
| "--audio-pattern", | |
| default="*.wav", | |
| help="Pattern to match audio files (default: *.wav)", | |
| ) | |
| parser.add_argument( | |
| "--buffer", | |
| type=float, | |
| default=1.5, | |
| help="Buffer time in seconds after each audio clip (default: 1.5)", | |
| ) | |
| parser.add_argument( | |
| "--output", | |
| default="final_presentation.mp4", | |
| help="Output video filename (default: final_presentation.mp4)", | |
| ) | |
| parser.add_argument( | |
| "--fps", type=int, default=5, help="Frame rate of output video (default: 5)" | |
| ) | |
| parser.add_argument( | |
| "--dpi", | |
| type=int, | |
| default=72, | |
| help="DPI for PDF to image conversion (default: 120)", | |
| ) | |
| return parser.parse_args() | |
| def find_audio_files(audio_dir: str, pattern: str) -> List[str]: | |
| """Find and sort audio files in the specified directory.""" | |
| search_pattern = os.path.join(audio_dir, pattern) | |
| audio_files = natsort.natsorted(glob.glob(search_pattern)) | |
| return audio_files | |
| def convert_pdf_to_images(pdf_path: str, dpi: int) -> List: | |
| """Convert PDF pages to images.""" | |
| print(f"Converting PDF '{pdf_path}' to images...") | |
| try: | |
| pdf_images = convert_from_path(pdf_path, dpi=dpi) | |
| print(f"Successfully converted {len(pdf_images)} pages from PDF.") | |
| return pdf_images | |
| except Exception as e: | |
| print(f"Error converting PDF to images: {e}") | |
| sys.exit(1) | |
| def create_video_clips( | |
| pdf_images: List, audio_files: List[str], buffer_seconds: float, output_fps: int | |
| ) -> List: | |
| """Create video clips from images and audio files.""" | |
| video_clips_list = [] | |
| print("\nCreating individual video clips...") | |
| for i, (img, aud_file) in enumerate(zip(pdf_images, audio_files)): | |
| print( | |
| f"Processing pair {i + 1}/{len(pdf_images)}: " | |
| f"Page {i + 1} + {os.path.basename(aud_file)}" | |
| ) | |
| try: | |
| # Load audio to get duration | |
| audio_clip = AudioFileClip(aud_file) | |
| audio_duration = audio_clip.duration | |
| # Calculate target duration for the image clip | |
| target_duration = audio_duration + buffer_seconds | |
| # Create a temporary file for the image | |
| temp_img_path = f"temp_slide_{i + 1}.png" | |
| img.save(temp_img_path, "PNG") | |
| # Create video clip from image with the correct duration | |
| # In MoviePy v2.0+, we use ImageSequenceClip with a single image | |
| img_clip = ImageSequenceClip([temp_img_path], durations=[target_duration]) | |
| # Set FPS for the individual clip | |
| img_clip = img_clip.with_fps(output_fps) | |
| # Set the audio for the image clip | |
| video_clip_with_audio = img_clip.with_audio(audio_clip) | |
| video_clips_list.append(video_clip_with_audio) | |
| print( | |
| f" -> Clip created (Audio: {audio_duration:.2f}s + " | |
| f"Buffer: {buffer_seconds:.2f}s = " | |
| f"Total: {target_duration:.2f}s)" | |
| ) | |
| except Exception as e: | |
| print(f" Error processing pair {i + 1}: {e}") | |
| print(" Skipping this pair.") | |
| # Close clips if they were opened, to release file handles | |
| if "audio_clip" in locals() and audio_clip: | |
| audio_clip.close() | |
| if "img_clip" in locals() and img_clip: | |
| img_clip.close() | |
| if "video_clip_with_audio" in locals() and video_clip_with_audio: | |
| video_clip_with_audio.close() | |
| return video_clips_list | |
| def concatenate_clips( | |
| video_clips_list: List, output_file: str, output_fps: int | |
| ) -> None: | |
| """Concatenate video clips and write to output file.""" | |
| if not video_clips_list: | |
| print("\nNo video clips were successfully created. Exiting.") | |
| sys.exit(1) | |
| print(f"\nConcatenating {len(video_clips_list)} clips...") | |
| final_clip = None | |
| try: | |
| final_clip = concatenate_videoclips(video_clips_list, method="compose") | |
| print(f"Writing final video file: {output_file}...") | |
| # Write the final video file | |
| final_clip.write_videofile( | |
| output_file, | |
| fps=output_fps, | |
| codec="libx264", | |
| audio_codec="aac", | |
| threads=16, | |
| # logger=None, # Suppress verbose output | |
| ) | |
| print("Final video file written successfully.") | |
| except Exception as e: | |
| print(f"\nError during concatenation or writing video file: {e}") | |
| print("Ensure you have enough free disk space and RAM.") | |
| finally: | |
| # Close clips to release resources | |
| if final_clip: | |
| final_clip.close() | |
| for clip in video_clips_list: | |
| clip.close() | |
| def cleanup_temp_files(pdf_images: List) -> None: | |
| """Clean up temporary image files.""" | |
| print("\nCleaning up temporary files...") | |
| for i in range(len(pdf_images)): | |
| temp_img_path = f"temp_slide_{i + 1}.png" | |
| if os.path.exists(temp_img_path): | |
| os.remove(temp_img_path) | |
| def main(): | |
| """Main function to run the script.""" | |
| args = parse_arguments() | |
| # Validate inputs | |
| if not os.path.exists(args.pdf): | |
| print(f"Error: PDF file '{args.pdf}' not found.") | |
| sys.exit(1) | |
| if not os.path.exists(args.audio_dir): | |
| print(f"Error: Audio directory '{args.audio_dir}' not found.") | |
| sys.exit(1) | |
| # Find audio files | |
| audio_files = find_audio_files(args.audio_dir, args.audio_pattern) | |
| if not audio_files: | |
| print( | |
| f"Error: No audio files found matching pattern '{args.audio_pattern}' " | |
| f"in directory '{args.audio_dir}'." | |
| ) | |
| sys.exit(1) | |
| # Convert PDF to images | |
| pdf_images = convert_pdf_to_images(args.pdf, args.dpi) | |
| # Check if number of PDF pages matches number of audio files | |
| if len(pdf_images) != len(audio_files): | |
| print("Error: Mismatched number of files found.") | |
| print(f" PDF pages ({len(pdf_images)})") | |
| print(f" Audio files ({len(audio_files)}): {audio_files}") | |
| print("Please ensure you have one corresponding audio file for each PDF page.") | |
| sys.exit(1) | |
| print(f"Found {len(pdf_images)} PDF pages with {len(audio_files)} audio files.") | |
| # Create video clips | |
| video_clips = create_video_clips(pdf_images, audio_files, args.buffer, args.fps) | |
| # Concatenate clips and create final video | |
| concatenate_clips(video_clips, args.output, args.fps) | |
| # Clean up | |
| cleanup_temp_files(pdf_images) | |
| print("\nScript finished.") | |
| if __name__ == "__main__": | |
| main() | |