Adding pexels downloader and fixes
This commit is contained in:
+116
-38
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user