import ffmpeg import argparse import os import sys import shutil import subprocess from pathlib import Path # Function to get available system fonts def get_system_fonts(): """Get list of available fonts on the system using fc-list command""" available_fonts = [] try: # Try to get fonts using fc-list (works on Linux, macOS with fontconfig) output = subprocess.check_output(['fc-list', ':', 'family']).decode('utf-8') fonts = set() for line in output.splitlines(): for font in line.split(','): font = font.strip() if font: fonts.add(font) available_fonts = sorted(list(fonts)) except (subprocess.SubprocessError, FileNotFoundError): # Fallback for systems without fc-list available_fonts = [ 'Arial', 'Helvetica', 'Times New Roman', 'Courier New', 'Verdana', 'Georgia', 'Palatino', 'Garamond', 'Bookman', 'Comic Sans MS', 'Trebuchet MS', 'Arial Black', 'Impact', 'Tahoma' ] return available_fonts class SubtitleBurner: def __init__(self, video_path, subtitle_path): if not os.path.exists(video_path): raise FileNotFoundError(f"Video file not found: {video_path}") if not os.path.exists(subtitle_path): raise FileNotFoundError(f"Subtitle file not found: {subtitle_path}") self.video_path = video_path self.subtitle_path = subtitle_path # Get available fonts self.available_fonts = get_system_fonts() def burn_subtitles(self, output_path, font='Arial', fontsize=10, primary_color='white', outline_color='black', outline_width=1, position='bottom'): """Burn subtitles into video using FFmpeg with customizable font options Args: output_path: Path for the output video font: Font family to use (default: Arial) fontsize: Font size (default: 24) primary_color: Main text color (default: white) outline_color: Text outline color (default: black) outline_width: Width of the text outline (default: 1) position: Subtitle position, 'bottom' or 'top' (default: bottom) """ print(f"Burning subtitles from {self.subtitle_path} into video with custom styling...") try: # Create output directory if it doesn't exist output_dir = os.path.dirname(output_path) if output_dir and not os.path.exists(output_dir): os.makedirs(output_dir) # Convert position to FFmpeg subtitle style format y_position = 'h-max(th,48)' if position == 'bottom' else '10' # Convert color names to ASS color format (BBGGRR) color_map = { 'white': 'FFFFFF', 'yellow': '00FFFF', 'black': '000000', 'red': '0000FF', 'blue': 'FF0000', 'green': '00FF00', 'orange': '0080FF', 'purple': '800080' } # Get hex color values or use defaults if not found primary_hex = color_map.get(primary_color.lower(), 'FFFFFF') outline_hex = color_map.get(outline_color.lower(), '000000') # Verify font is available or use fallback if font not in self.available_fonts: print(f"Warning: Font '{font}' not available. Using Arial as fallback.") font = 'Arial' # Logging to help debug outline width issues print(f"DEBUG: Using outline width: {outline_width} (type: {type(outline_width)})") # Escape path for FFmpeg filter string: forward slashes + escaped colon subtitle_path_escaped = self.subtitle_path.replace('\\', '/').replace(':', '\\:') # Create advanced subtitles filter with styling options subtitles_filter = ( f"subtitles='{subtitle_path_escaped}':force_style='Fontname={font}," f"Fontsize={fontsize},PrimaryColour=&H{primary_hex}," f"OutlineColour=&H{outline_hex},BorderStyle=1,Outline={outline_width:.1f}," f"Alignment=2,MarginV=10,Shadow=0,MarginL=10,MarginR=10,y={y_position}'" ) # Burn subtitles stream = ffmpeg.input(self.video_path) stream = ffmpeg.output(stream, output_path, vf=subtitles_filter, acodec='copy') # Run FFmpeg command print(f"Creating output video: {output_path}") ffmpeg.run(stream, overwrite_output=True) print(f"Successfully created video with burned subtitles: {output_path}") except ffmpeg.Error as e: print('FFmpeg error:', e.stderr.decode() if e.stderr else str(e)) raise except Exception as e: print(f"Error during subtitle burning: {str(e)}") raise def validate_files(video_path, subtitle_path): """Validate input files exist and have correct extensions""" # Check video extension video_ext = os.path.splitext(video_path)[1].lower() valid_video_extensions = ['.mp4', '.avi', '.mkv', '.mov'] if video_ext not in valid_video_extensions: raise ValueError(f"Invalid video format. Supported formats: {', '.join(valid_video_extensions)}") # Check subtitle extension subtitle_ext = os.path.splitext(subtitle_path)[1].lower() if subtitle_ext != '.srt': raise ValueError("Subtitle file must be in .srt format") def main(): # Set up argument parser parser = argparse.ArgumentParser(description='Burn subtitles into video') parser.add_argument('input_video', help='Path to the input video file') parser.add_argument('subtitle_file', help='Path to the SRT subtitle file') parser.add_argument('--output', '-o', help='Path to the output video file', default='output_with_subtitles.mp4') parser.add_argument('--font', help='Font family for subtitles', default='Arial') parser.add_argument('--fontsize', help='Font size for subtitles', type=int, default=10) parser.add_argument('--text-color', help='Subtitle text color', default='white') parser.add_argument('--outline-color', help='Subtitle outline color', default='black') parser.add_argument('--outline-width', help='Subtitle outline width', type=float, default=1.0) parser.add_argument('--position', help='Subtitle position (bottom or top)', choices=['bottom', 'top'], default='bottom') args = parser.parse_args() try: # Validate input files validate_files(args.input_video, args.subtitle_file) # Resolve all paths to absolute to avoid Windows relative-path issues input_video = str(Path(args.input_video).resolve()) subtitle_file = str(Path(args.subtitle_file).resolve()) output_path = str(Path(args.output).resolve()) # Generate unique names based on input video base_filename = os.path.splitext(os.path.basename(input_video))[0] subtitle_copy = f"{base_filename}_subtitles.srt" # Create temporary directory for processing if needed temp_dir = str(Path(output_path).parent / 'temp') if not os.path.exists(temp_dir): os.makedirs(temp_dir) temp_subtitle_path = os.path.join(temp_dir, subtitle_copy) try: # Copy subtitle file to temp directory with unique name shutil.copy2(subtitle_file, temp_subtitle_path) # Create output directory if it doesn't exist output_dir = os.path.dirname(output_path) if output_dir and not os.path.exists(output_dir): os.makedirs(output_dir) # Create burner and process using the temporary subtitle file print("Initializing subtitle burner...") burner = SubtitleBurner(input_video, temp_subtitle_path) print("Starting subtitle burn process...") burner.burn_subtitles( output_path, font=args.font, fontsize=args.fontsize, primary_color=args.text_color, outline_color=args.outline_color, outline_width=args.outline_width, position=args.position ) finally: # Clean up temporary files if os.path.exists(temp_subtitle_path): os.remove(temp_subtitle_path) if os.path.exists(temp_dir): try: os.rmdir(temp_dir) except OSError: pass # Directory might not be empty except FileNotFoundError as e: print(f"Error: {str(e)}") sys.exit(1) except ValueError as e: print(f"Error: {str(e)}") sys.exit(1) except Exception as e: print(f"Error: An unexpected error occurred - {str(e)}") sys.exit(1) if __name__ == "__main__": main()