Adding pexels downloader and fixes

This commit is contained in:
2026-06-07 11:19:19 +02:00
parent 980bb84dac
commit b9b5a8e77d
12 changed files with 957 additions and 146 deletions
+116 -38
View File
@@ -34,18 +34,24 @@ AUDIO_OFFSET_SECONDS = 1.0
# The pause-variant entries (vftp: etc.) carry a third element "pause_narration"
# which is a per-event property, not stored in videos.json.
_SHORTHAND_PREFIXES: dict[str, tuple] = {
"vft:": ("fullscreen", "above"),
"vfb:": ("fullscreen", "below"),
"vft:": ("fullscreen", "above"),
"vfb:": ("fullscreen", "below"),
"vfm:": ("fullscreen", "mid"),
"vf2t:": ("fullscreen2", "above"),
"vf2b:": ("fullscreen2", "below"),
"vst:": ("square", "above"),
"vsb:": ("square", "below"),
"vftp:": ("fullscreen", "above"),
"vfbp:": ("fullscreen", "below"),
"vf2m:": ("fullscreen2", "mid"),
"vst:": ("square", "above"),
"vsb:": ("square", "below"),
"vsm:": ("square", "mid"),
"vftp:": ("fullscreen", "above"),
"vfbp:": ("fullscreen", "below"),
"vfmp:": ("fullscreen", "mid"),
"vf2tp:": ("fullscreen2", "above"),
"vf2bp:": ("fullscreen2", "below"),
"vstp:": ("square", "above"),
"vsbp:": ("square", "below"),
"vf2mp:": ("fullscreen2", "mid"),
"vstp:": ("square", "above"),
"vsbp:": ("square", "below"),
"vsmp:": ("square", "mid"),
}
@@ -157,18 +163,12 @@ def _is_known_marker(
_VIDEO_PREFIXES = (
"video:",
"narration:",
"vft:",
"vfb:",
"vf2t:",
"vf2b:",
"vst:",
"vsb:",
"vftp:",
"vfbp:",
"vf2tp:",
"vf2bp:",
"vstp:",
"vsbp:",
"vft:", "vfb:", "vfm:",
"vf2t:", "vf2b:", "vf2m:",
"vst:", "vsb:", "vsm:",
"vftp:", "vfbp:", "vfmp:",
"vf2tp:", "vf2bp:", "vf2mp:",
"vstp:", "vsbp:", "vsmp:",
)
if any(marker_id.startswith(p) for p in _VIDEO_PREFIXES):
return True
@@ -513,6 +513,73 @@ def align_markers_to_transcription(
)
)
# Repair pass: retry INTERPOLATED markers that the forward scan missed.
# Root cause of cascade failures: one bad match advances last_idx past
# the true positions of several subsequent markers. Fix: search in a
# bounded window [prev_marker_time - 1s, next_marker_time + 2s] so we
# avoid false early matches while still recovering from cascade failures.
if any(t.timestamp < 0 for t in timings):
for i, timing in enumerate(timings):
if timing.timestamp >= 0:
continue
marker_id, anchor_text, is_borrowed, anchor_type = contexts[i]
if not anchor_text.strip():
continue
# Lower bound: previous matched marker's timestamp → word index.
# Repairs processed in order, so already-repaired markers count too.
prev_time = 0.0
for j in range(i - 1, -1, -1):
if timings[j].timestamp >= 0:
prev_time = max(0.0, timings[j].timestamp - 1.0)
break
win_start = next(
(j for j, w in enumerate(transcription) if w.start >= prev_time),
0,
)
# Upper bound: next matched marker in the timings list (+2s padding)
next_time = float("inf")
for j in range(i + 1, len(timings)):
if timings[j].timestamp >= 0:
next_time = timings[j].timestamp + 2.0
break
win_end = (
next(
(j for j, w in enumerate(transcription) if w.start > next_time),
len(transcription),
)
if next_time < float("inf")
else len(transcription)
)
if win_end <= win_start:
continue
# Search in the bounded window with a relaxed threshold
sub = transcription[win_start:win_end]
idx, timestamp, confidence, match_end_idx = _find_phrase_timestamp(
anchor_text,
sub,
start_from=0,
fuzzy_threshold=max(0.4, fuzzy_threshold - 0.1),
)
if idx >= 0:
if anchor_type == "after" and match_end_idx > 0:
end_word = sub[min(match_end_idx - 1, len(sub) - 1)]
marker_time = end_word.end
else:
marker_time = max(0.0, timestamp - 0.5)
timings[i] = MarkerTiming(
marker_id=marker_id,
timestamp=marker_time,
context=f"(repaired: {anchor_text[:40]})",
confidence=confidence,
)
# Deduplicate slide markers. The manuscript pattern [SN]\n\n[SN] text... is
# common: the first blank occurrence is a visual-transition cue and the second
# carries the narration text used for alignment. We keep the first entry in
@@ -531,10 +598,24 @@ def align_markers_to_transcription(
else:
prev_idx = seen[timing.marker_id]
prev = deduped[prev_idx]
if (
# Upgrade if: previous was a placeholder/interpolated and the new one is better.
# Also upgrade if previous used the backward-looking "after" anchor —
# that heuristic gives end-of-preceding-section timing, but a direct
# "before" match on the second occurrence (start-of-new-section 0.5s)
# is more accurate for when the slide should appear.
should_upgrade = (
prev.context == "(after previous)"
and timing.context != "(after previous)"
):
) or (
prev.timestamp < 0
and timing.timestamp >= 0
) or (
prev.context.startswith("(end of:")
and timing.timestamp >= 0
and timing.context != "(after previous)"
and not timing.context.startswith("(end of:")
)
if should_upgrade:
deduped[prev_idx] = MarkerTiming(
marker_id=prev.marker_id,
timestamp=timing.timestamp,
@@ -658,18 +739,12 @@ def build_render_plan(
_VIDEO_MARKER_PREFIXES = (
"video:",
"narration:",
"vft:",
"vfb:",
"vf2t:",
"vf2b:",
"vst:",
"vsb:",
"vftp:",
"vfbp:",
"vf2tp:",
"vf2bp:",
"vstp:",
"vsbp:",
"vft:", "vfb:", "vfm:",
"vf2t:", "vf2b:", "vf2m:",
"vst:", "vsb:", "vsm:",
"vftp:", "vfbp:", "vfmp:",
"vf2tp:", "vf2bp:", "vf2mp:",
"vstp:", "vsbp:", "vsmp:",
)
missing_video_ids = [
timing.marker_id[len(prefix) :]
@@ -764,7 +839,10 @@ def build_render_plan(
slide_event.end_time += pause_duration
for vid_event in video_events:
if vid_event.start_time > narration_time:
if vid_event is event:
# Don't shift the pause event by its own pause
continue
if vid_event.start_time >= narration_time:
vid_event.start_time += pause_duration
if vid_event.end_time > narration_time:
vid_event.end_time += pause_duration
@@ -1004,7 +1082,7 @@ def _extract_video_events(
# Pause-variant prefixes — the only thing the render pass still needs from
# shorthand markers at event-build time (pause_narration is per-event, not stored in videos.json).
_PAUSE_PREFIXES = {"vftp:", "vfbp:", "vf2tp:", "vf2bp:", "vstp:", "vsbp:"}
_PAUSE_PREFIXES = {"vftp:", "vfbp:", "vfmp:", "vf2tp:", "vf2bp:", "vf2mp:", "vstp:", "vsbp:", "vsmp:"}
# Collect video markers: (time, video_id, event_type, pause_narration)
# video_markers: (timestamp, video_id, marker_type, pause_narration)
@@ -1088,8 +1166,8 @@ def _extract_video_events(
end_time = start_time + video_source.take
elif end_on == "end":
end_time = total_duration
elif end_on == "next_slide" or (end_on is None and marker_type == "video"):
# End at next slide marker
elif end_on in ("next_slide", "slide") or (end_on is None and marker_type == "video"):
# End at next slide marker ("slide" is a recognised alias for "next_slide")
end_time = total_duration
for slide_time in slide_times:
if slide_time > start_time: