Use per-clip metadata stamps and date ordering

This commit is contained in:
hbrain 2026-05-26 09:46:37 +00:00
parent 0e7f309bd8
commit 5a69a31e89

View file

@ -424,16 +424,20 @@ def infer_place_from_gps(media: list[MediaItem]) -> str | None:
return None return None
def media_date(path: Path) -> datetime: def media_embedded_date(path: Path) -> datetime | None:
ext = path.suffix.lower() ext = path.suffix.lower()
dt = None
if ext in IMAGE_EXTS: if ext in IMAGE_EXTS:
try: try:
dt = exif_image_date(path) dt = exif_image_date(path)
if dt is not None:
return dt
except Exception: except Exception:
dt = None pass
if dt is None: return ffprobe_creation_date(path)
dt = ffprobe_creation_date(path)
def media_date(path: Path) -> datetime:
dt = media_embedded_date(path)
if dt is None: if dt is None:
dt = datetime.fromtimestamp(path.stat().st_mtime) dt = datetime.fromtimestamp(path.stat().st_mtime)
return dt return dt
@ -470,6 +474,26 @@ def display_date(dt: datetime) -> str:
return f"{dt.strftime('%B')} {dt.year}" 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: def timestamp() -> str:
return datetime.now().strftime("%Y-%m-%d %H:%M:%S") 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: if not media:
raise SystemExit(f"No images or videos found in {input_dir}") 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) return media, audio, parse_data_file(data_file)
@ -747,6 +772,7 @@ def build_video(
data: MovieData, data: MovieData,
creation_date: datetime, creation_date: datetime,
captions: list[str], captions: list[str],
clip_stamps: list[str],
fade: float, fade: float,
audio_fade: float, audio_fade: float,
audio_fade_in: float, audio_fade_in: float,
@ -899,16 +925,30 @@ def build_video(
caption_label = next_label caption_label = next_label
stamp_size = max(16, height // 58) stamp_size = max(16, height // 58)
stamp_lines = [x for x in [data.title, data.date, data.place] if x] stamp_left_label = caption_label
stamp_text = drawtext_escape(" | ".join(stamp_lines)) for idx, stamp_text in enumerate(clip_stamps):
stamp_left_label = "stamp_left" if not stamp_text:
if 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( filters.append(
f"[{caption_label}]drawtext=fontfile='{stamp_font}':{textfile_arg(' | '.join(stamp_lines))}:x=24:y=h-text_h-18:" 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[{stamp_left_label}]" f"fontsize={stamp_size}:fontcolor=white:borderw=1:bordercolor=black:enable='between(t,{start:.3f},{end:.3f})'[{next_label}]"
) )
else: stamp_left_label = next_label
filters.append(f"[{caption_label}]copy[{stamp_left_label}]")
filters.append( 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"[{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}]" 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. # 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] 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") audio = concat_or_mix_audio(audio_files, tmp / "music.m4a")
if len(audio_files) > 1: if len(audio_files) > 1:
console(f"Audio: {audio if audio else 'none'}") 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) artist, title = audio_metadata(audio)
if artist or title: if artist or title:
console("Audio metadata: " + " - ".join(x for x in [artist, title] if x)) 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}") console(f"Done: {output}")
return 0 return 0