diff --git a/movmaker.py b/movmaker.py index 3c67dfc..e6bb8ec 100755 --- a/movmaker.py +++ b/movmaker.py @@ -19,6 +19,7 @@ import shlex import subprocess import sys import tempfile +import unicodedata from dataclasses import dataclass from datetime import datetime from pathlib import Path @@ -208,6 +209,14 @@ def media_date(path: Path) -> datetime: def slugify(value: str) -> str: + replacements = { + "æ": "ae", "Æ": "ae", + "ø": "o", "Ø": "o", + "å": "a", "Å": "a", + "đ": "d", "Đ": "d", + } + value = "".join(replacements.get(ch, ch) for ch in value) + value = unicodedata.normalize("NFKD", value).encode("ascii", "ignore").decode("ascii") value = value.strip().lower() value = re.sub(r"[^a-z0-9]+", "_", value) value = value.strip("_") @@ -348,6 +357,7 @@ def build_video( audio: Path | None, output: Path, data: MovieData, + captions: list[str], fade: float, audio_fade: float, width: int, @@ -368,16 +378,19 @@ def build_video( n = len(clips) total_duration = durations[0] + transition_offsets: list[float] = [] if n == 1: current = "0:v" else: current = "v01" offset = max(0.0, durations[0] - fade) + transition_offsets.append(offset) filters.append(f"[0:v][1:v]xfade=transition=fade:duration={fade:.3f}:offset={offset:.3f}[{current}]") total_duration = durations[0] + durations[1] - fade for i in range(2, n): next_label = f"v{i:02d}" offset = max(0.0, total_duration - fade) + transition_offsets.append(offset) filters.append(f"[{current}][{i}:v]xfade=transition=fade:duration={fade:.3f}:offset={offset:.3f}[{next_label}]") current = next_label total_duration += durations[i] - fade @@ -388,8 +401,8 @@ def build_video( title_font = str(Path.home() / ".local/share/fonts/google/BebasNeue-Regular.ttf") stamp_font = str(Path.home() / ".local/share/fonts/google/Inter%5Bopsz,wght%5D.ttf") if title_lines: - title_font_size = max(50, height // 12) - subtitle_font_size = max(28, height // 24) + title_font_size = max(60, height // 10) + subtitle_font_size = max(34, height // 20) main_title = data.title or title_lines[0] subtitle = " | ".join(x for x in [data.date, data.place] if x) title_alpha = "if(lt(t\\,5)\\,1\\,if(lt(t\\,6)\\,6-t\\,0))" @@ -397,47 +410,74 @@ def build_video( title_y = f"(h-text_h)/2-{title_font_size // 2}" filters.append( f"[{current}]drawtext=fontfile='{title_font}':text='{drawtext_escape(main_title)}':x=(w-text_w)/2:y={title_y}:" - f"fontsize={title_font_size}:text_spacing=4:fontcolor=white:borderw=3:bordercolor=black:" + f"fontsize={title_font_size}:fontcolor=white:borderw=0:shadowcolor=black@0.75:shadowx=3:shadowy=3:" f"alpha='{title_alpha}':enable='between(t,0,6)'[title0]" ) filters.append( f"[title0]drawtext=fontfile='{title_font}':text='{drawtext_escape(main_title)}':x=(w-text_w)/2+1:y={title_y}:" - f"fontsize={title_font_size}:text_spacing=4:fontcolor=white:borderw=3:bordercolor=black:" + f"fontsize={title_font_size}:fontcolor=white:borderw=0:shadowcolor=black@0.75:shadowx=3:shadowy=3:" f"alpha='{title_alpha}':enable='between(t,0,6)'[title0b]" ) filters.append( f"[title0b]drawtext=fontfile='{title_font}':text='{drawtext_escape(subtitle)}':x=(w-text_w)/2:y=(h-text_h)/2+{subtitle_font_size}:" - f"fontsize={subtitle_font_size}:fontcolor=white:borderw=3:bordercolor=black:" + f"fontsize={subtitle_font_size}:fontcolor=white:borderw=0:shadowcolor=black@0.75:shadowx=2:shadowy=2:" f"alpha='{title_alpha}':enable='between(t,0,6)'[{title_label}]" ) else: filters.append( f"[{current}]drawtext=fontfile='{title_font}':text='{drawtext_escape(main_title)}':x=(w-text_w)/2:y=(h-text_h)/2:" - f"fontsize={title_font_size}:text_spacing=4:fontcolor=white:borderw=3:bordercolor=black:" + f"fontsize={title_font_size}:fontcolor=white:borderw=0:shadowcolor=black@0.75:shadowx=3:shadowy=3:" f"alpha='{title_alpha}':enable='between(t,0,6)'[title0]" ) filters.append( f"[title0]drawtext=fontfile='{title_font}':text='{drawtext_escape(main_title)}':x=(w-text_w)/2+1:y=(h-text_h)/2:" - f"fontsize={title_font_size}:text_spacing=4:fontcolor=white:borderw=3:bordercolor=black:" + f"fontsize={title_font_size}:fontcolor=white:borderw=0:shadowcolor=black@0.75:shadowx=3:shadowy=3:" f"alpha='{title_alpha}':enable='between(t,0,6)'[{title_label}]" ) else: filters.append(f"[{current}]copy[{title_label}]") - stamp_size = max(11, height // 80) + caption_label = title_label + caption_size = max(18, height // 42) + for idx, caption in enumerate(captions): + if not caption: + continue + if n == 1: + start = 0.0 + end = total_duration + elif idx == 0: + start = 0.0 + end = transition_offsets[0] + elif idx == n - 1: + start = transition_offsets[idx - 1] + fade + end = total_duration + else: + start = transition_offsets[idx - 1] + fade + end = transition_offsets[idx] + if end <= start: + continue + next_label = f"caption{idx}" + filters.append( + f"[{caption_label}]drawtext=fontfile='{stamp_font}':text='{drawtext_escape(caption)}':x=(w-text_w)/2:y=h-text_h-62:" + f"fontsize={caption_size}:fontcolor=white:borderw=1:bordercolor=black:" + f"enable='between(t,{start:.3f},{end:.3f})'[{next_label}]" + ) + caption_label = next_label + + stamp_size = max(14, height // 65) stamp_lines = [x for x in [data.title, data.date, data.place] if x] stamp_text = drawtext_escape(" | ".join(stamp_lines)) stamp_left_label = "stamp_left" if stamp_text: filters.append( - f"[{title_label}]drawtext=fontfile='{stamp_font}':text='{stamp_text}':x=24:y=h-text_h-18:" - f"fontsize={stamp_size}:fontcolor=white:borderw=3:bordercolor=black[{stamp_left_label}]" + f"[{caption_label}]drawtext=fontfile='{stamp_font}':text='{stamp_text}':x=24:y=h-text_h-18:" + f"fontsize={stamp_size}:fontcolor=white:borderw=1:bordercolor=black[{stamp_left_label}]" ) else: - filters.append(f"[{title_label}]copy[{stamp_left_label}]") + filters.append(f"[{caption_label}]copy[{stamp_left_label}]") filters.append( f"[{stamp_left_label}]drawtext=fontfile='{stamp_font}':text='@bubulescu':x=w-text_w-24:y=h-text_h-18:" - f"fontsize={stamp_size}:fontcolor=white:borderw=3:bordercolor=black[{final_label}]" + f"fontsize={stamp_size}:fontcolor=white:borderw=1:bordercolor=black[{final_label}]" ) cmd += ["-filter_complex", ";".join(filters), "-map", f"[{final_label}]"] @@ -529,8 +569,9 @@ def main(argv: list[str] | None = None) -> int: # Use probed normalized duration for accuracy. durations = [ffprobe_duration(out) for out in clips] + captions = [data.captions.get(item.path.name, data.captions.get(str(item.path), "")) for item in media] audio = concat_or_mix_audio(audio_files, tmp / "music.m4a") - build_video(clips, durations, audio, output, data, fade, audio_fade, width, height, fps, preset, crf) + build_video(clips, durations, audio, output, data, captions, fade, audio_fade, width, height, fps, preset, crf) print(f"Done: {output}") return 0