215 lines
No EOL
9 KiB
Python
Executable file
215 lines
No EOL
9 KiB
Python
Executable file
|
|
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() |