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() | |