page-to-video / app /src /create_video.py
burtenshaw
speedup video writing
4d71f72
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()