Show per-file captions
This commit is contained in:
parent
0c1ee1c42a
commit
24f226aa75
1 changed files with 54 additions and 13 deletions
67
movmaker.py
67
movmaker.py
|
|
@ -19,6 +19,7 @@ import shlex
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
|
import unicodedata
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
@ -208,6 +209,14 @@ def media_date(path: Path) -> datetime:
|
||||||
|
|
||||||
|
|
||||||
def slugify(value: str) -> str:
|
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 = value.strip().lower()
|
||||||
value = re.sub(r"[^a-z0-9]+", "_", value)
|
value = re.sub(r"[^a-z0-9]+", "_", value)
|
||||||
value = value.strip("_")
|
value = value.strip("_")
|
||||||
|
|
@ -348,6 +357,7 @@ def build_video(
|
||||||
audio: Path | None,
|
audio: Path | None,
|
||||||
output: Path,
|
output: Path,
|
||||||
data: MovieData,
|
data: MovieData,
|
||||||
|
captions: list[str],
|
||||||
fade: float,
|
fade: float,
|
||||||
audio_fade: float,
|
audio_fade: float,
|
||||||
width: int,
|
width: int,
|
||||||
|
|
@ -368,16 +378,19 @@ def build_video(
|
||||||
n = len(clips)
|
n = len(clips)
|
||||||
|
|
||||||
total_duration = durations[0]
|
total_duration = durations[0]
|
||||||
|
transition_offsets: list[float] = []
|
||||||
if n == 1:
|
if n == 1:
|
||||||
current = "0:v"
|
current = "0:v"
|
||||||
else:
|
else:
|
||||||
current = "v01"
|
current = "v01"
|
||||||
offset = max(0.0, durations[0] - fade)
|
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}]")
|
filters.append(f"[0:v][1:v]xfade=transition=fade:duration={fade:.3f}:offset={offset:.3f}[{current}]")
|
||||||
total_duration = durations[0] + durations[1] - fade
|
total_duration = durations[0] + durations[1] - fade
|
||||||
for i in range(2, n):
|
for i in range(2, n):
|
||||||
next_label = f"v{i:02d}"
|
next_label = f"v{i:02d}"
|
||||||
offset = max(0.0, total_duration - fade)
|
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}]")
|
filters.append(f"[{current}][{i}:v]xfade=transition=fade:duration={fade:.3f}:offset={offset:.3f}[{next_label}]")
|
||||||
current = next_label
|
current = next_label
|
||||||
total_duration += durations[i] - fade
|
total_duration += durations[i] - fade
|
||||||
|
|
@ -388,8 +401,8 @@ def build_video(
|
||||||
title_font = str(Path.home() / ".local/share/fonts/google/BebasNeue-Regular.ttf")
|
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")
|
stamp_font = str(Path.home() / ".local/share/fonts/google/Inter%5Bopsz,wght%5D.ttf")
|
||||||
if title_lines:
|
if title_lines:
|
||||||
title_font_size = max(50, height // 12)
|
title_font_size = max(60, height // 10)
|
||||||
subtitle_font_size = max(28, height // 24)
|
subtitle_font_size = max(34, height // 20)
|
||||||
main_title = data.title or title_lines[0]
|
main_title = data.title or title_lines[0]
|
||||||
subtitle = " | ".join(x for x in [data.date, data.place] if x)
|
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))"
|
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}"
|
title_y = f"(h-text_h)/2-{title_font_size // 2}"
|
||||||
filters.append(
|
filters.append(
|
||||||
f"[{current}]drawtext=fontfile='{title_font}':text='{drawtext_escape(main_title)}':x=(w-text_w)/2:y={title_y}:"
|
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]"
|
f"alpha='{title_alpha}':enable='between(t,0,6)'[title0]"
|
||||||
)
|
)
|
||||||
filters.append(
|
filters.append(
|
||||||
f"[title0]drawtext=fontfile='{title_font}':text='{drawtext_escape(main_title)}':x=(w-text_w)/2+1:y={title_y}:"
|
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]"
|
f"alpha='{title_alpha}':enable='between(t,0,6)'[title0b]"
|
||||||
)
|
)
|
||||||
filters.append(
|
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"[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}]"
|
f"alpha='{title_alpha}':enable='between(t,0,6)'[{title_label}]"
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
filters.append(
|
filters.append(
|
||||||
f"[{current}]drawtext=fontfile='{title_font}':text='{drawtext_escape(main_title)}':x=(w-text_w)/2:y=(h-text_h)/2:"
|
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]"
|
f"alpha='{title_alpha}':enable='between(t,0,6)'[title0]"
|
||||||
)
|
)
|
||||||
filters.append(
|
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"[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}]"
|
f"alpha='{title_alpha}':enable='between(t,0,6)'[{title_label}]"
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
filters.append(f"[{current}]copy[{title_label}]")
|
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_lines = [x for x in [data.title, data.date, data.place] if x]
|
||||||
stamp_text = drawtext_escape(" | ".join(stamp_lines))
|
stamp_text = drawtext_escape(" | ".join(stamp_lines))
|
||||||
stamp_left_label = "stamp_left"
|
stamp_left_label = "stamp_left"
|
||||||
if stamp_text:
|
if stamp_text:
|
||||||
filters.append(
|
filters.append(
|
||||||
f"[{title_label}]drawtext=fontfile='{stamp_font}':text='{stamp_text}':x=24:y=h-text_h-18:"
|
f"[{caption_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"fontsize={stamp_size}:fontcolor=white:borderw=1:bordercolor=black[{stamp_left_label}]"
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
filters.append(f"[{title_label}]copy[{stamp_left_label}]")
|
filters.append(f"[{caption_label}]copy[{stamp_left_label}]")
|
||||||
filters.append(
|
filters.append(
|
||||||
f"[{stamp_left_label}]drawtext=fontfile='{stamp_font}':text='@bubulescu':x=w-text_w-24:y=h-text_h-18:"
|
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}]"]
|
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.
|
# Use probed normalized duration for accuracy.
|
||||||
durations = [ffprobe_duration(out) for out in clips]
|
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")
|
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}")
|
print(f"Done: {output}")
|
||||||
return 0
|
return 0
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue