Use per-clip metadata stamps and date ordering
This commit is contained in:
parent
0e7f309bd8
commit
5a69a31e89
1 changed files with 55 additions and 14 deletions
69
movmaker.py
69
movmaker.py
|
|
@ -424,16 +424,20 @@ def infer_place_from_gps(media: list[MediaItem]) -> str | None:
|
|||
return None
|
||||
|
||||
|
||||
def media_date(path: Path) -> datetime:
|
||||
def media_embedded_date(path: Path) -> datetime | None:
|
||||
ext = path.suffix.lower()
|
||||
dt = None
|
||||
if ext in IMAGE_EXTS:
|
||||
try:
|
||||
dt = exif_image_date(path)
|
||||
if dt is not None:
|
||||
return dt
|
||||
except Exception:
|
||||
dt = None
|
||||
if dt is None:
|
||||
dt = ffprobe_creation_date(path)
|
||||
pass
|
||||
return ffprobe_creation_date(path)
|
||||
|
||||
|
||||
def media_date(path: Path) -> datetime:
|
||||
dt = media_embedded_date(path)
|
||||
if dt is None:
|
||||
dt = datetime.fromtimestamp(path.stat().st_mtime)
|
||||
return dt
|
||||
|
|
@ -470,6 +474,26 @@ def display_date(dt: datetime) -> str:
|
|||
return f"{dt.strftime('%B')} {dt.year}"
|
||||
|
||||
|
||||
def prepare_clip_stamps(media: list[MediaItem], data: MovieData) -> list[str]:
|
||||
geocode_cache: dict[tuple[float, float], str | None] = {}
|
||||
stamps: list[str] = []
|
||||
for item in media:
|
||||
embedded_date = media_embedded_date(item.path)
|
||||
date_text = display_date(embedded_date) if embedded_date else data.date
|
||||
place_text = data.place
|
||||
gps = media_gps(item.path)
|
||||
if gps:
|
||||
key = (round(gps[0], 4), round(gps[1], 4))
|
||||
if key not in geocode_cache:
|
||||
geocode_cache[key] = reverse_geocode_city_country(*gps)
|
||||
place_text = geocode_cache[key] or place_text
|
||||
stamp = " | ".join(x for x in [data.title, date_text, place_text] if x)
|
||||
stamps.append(stamp)
|
||||
if stamp:
|
||||
console(f"Clip stamp: {item.path.name}: {ascii_safe(stamp)}")
|
||||
return stamps
|
||||
|
||||
|
||||
def timestamp() -> str:
|
||||
return datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
|
||||
|
|
@ -515,6 +539,7 @@ def scan_input(input_dir: Path, image_duration: float) -> tuple[list[MediaItem],
|
|||
if not media:
|
||||
raise SystemExit(f"No images or videos found in {input_dir}")
|
||||
|
||||
media.sort(key=lambda item: (media_date(item.path), natural_key(item.path)))
|
||||
return media, audio, parse_data_file(data_file)
|
||||
|
||||
|
||||
|
|
@ -747,6 +772,7 @@ def build_video(
|
|||
data: MovieData,
|
||||
creation_date: datetime,
|
||||
captions: list[str],
|
||||
clip_stamps: list[str],
|
||||
fade: float,
|
||||
audio_fade: float,
|
||||
audio_fade_in: float,
|
||||
|
|
@ -899,16 +925,30 @@ def build_video(
|
|||
caption_label = next_label
|
||||
|
||||
stamp_size = max(16, height // 58)
|
||||
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:
|
||||
stamp_left_label = caption_label
|
||||
for idx, stamp_text in enumerate(clip_stamps):
|
||||
if not stamp_text:
|
||||
continue
|
||||
if n == 1:
|
||||
start = intro_duration
|
||||
end = total_duration
|
||||
elif idx == 0:
|
||||
start = intro_duration
|
||||
end = intro_duration + transition_offsets[0]
|
||||
elif idx == n - 1:
|
||||
start = intro_duration + transition_offsets[idx - 1] + fade
|
||||
end = total_duration
|
||||
else:
|
||||
start = intro_duration + transition_offsets[idx - 1] + fade
|
||||
end = intro_duration + transition_offsets[idx]
|
||||
if end <= start:
|
||||
continue
|
||||
next_label = f"stamp_left_{idx}"
|
||||
filters.append(
|
||||
f"[{caption_label}]drawtext=fontfile='{stamp_font}':{textfile_arg(' | '.join(stamp_lines))}:x=24:y=h-text_h-18:"
|
||||
f"fontsize={stamp_size}:fontcolor=white:borderw=1:bordercolor=black[{stamp_left_label}]"
|
||||
f"[{stamp_left_label}]drawtext=fontfile='{stamp_font}':{textfile_arg(stamp_text)}:x=24:y=h-text_h-18:"
|
||||
f"fontsize={stamp_size}:fontcolor=white:borderw=1:bordercolor=black:enable='between(t,{start:.3f},{end:.3f})'[{next_label}]"
|
||||
)
|
||||
else:
|
||||
filters.append(f"[{caption_label}]copy[{stamp_left_label}]")
|
||||
stamp_left_label = next_label
|
||||
filters.append(
|
||||
f"[{stamp_left_label}]drawtext=fontfile='{stamp_font}':{textfile_arg('Bubulescu.Org')}:x=w-text_w-24:y=h-text_h-18:"
|
||||
f"fontsize={stamp_size}:fontcolor=white:borderw=1:bordercolor=black[{final_label}]"
|
||||
|
|
@ -1049,6 +1089,7 @@ 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]
|
||||
clip_stamps = prepare_clip_stamps(media, data)
|
||||
audio = concat_or_mix_audio(audio_files, tmp / "music.m4a")
|
||||
if len(audio_files) > 1:
|
||||
console(f"Audio: {audio if audio else 'none'}")
|
||||
|
|
@ -1056,7 +1097,7 @@ def main(argv: list[str] | None = None) -> int:
|
|||
artist, title = audio_metadata(audio)
|
||||
if artist or title:
|
||||
console("Audio metadata: " + " - ".join(x for x in [artist, title] if x))
|
||||
build_video(clips, durations, audio, output, data, first_media_date, captions, fade, audio_fade, audio_fade_in, video_fade, width, height, fps, preset, crf, tmp)
|
||||
build_video(clips, durations, audio, output, data, first_media_date, captions, clip_stamps, fade, audio_fade, audio_fade_in, video_fade, width, height, fps, preset, crf, tmp)
|
||||
|
||||
console(f"Done: {output}")
|
||||
return 0
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue