Adding some files
This commit is contained in:
+89
-32
@@ -182,14 +182,17 @@ def _extract_marker_contexts(
|
||||
slides: dict = None,
|
||||
videos: dict = None,
|
||||
audio: dict = None,
|
||||
) -> list[tuple[str, str, bool]]:
|
||||
) -> list[tuple[str, str, bool, str]]:
|
||||
"""
|
||||
Extract known markers and the text immediately following them from manuscript.
|
||||
|
||||
Unknown markers are filtered out and stripped from following text.
|
||||
Note: [cite:...] markers are already stripped at parse time.
|
||||
|
||||
Returns list of (marker_id, following_text, is_borrowed) tuples for known markers only.
|
||||
Returns list of (marker_id, anchor_text, is_borrowed, anchor_type) tuples.
|
||||
anchor_type is "before" (default — place before the matched phrase) or
|
||||
"after" (place at the end of the matched phrase — used for markers that
|
||||
trail a narration block and have no following text of their own).
|
||||
"""
|
||||
slides = slides or {}
|
||||
videos = videos or {}
|
||||
@@ -227,7 +230,7 @@ def _extract_marker_contexts(
|
||||
for i, (marker_id, following_text) in enumerate(raw_contexts):
|
||||
if following_text:
|
||||
words = following_text.split()[:10]
|
||||
contexts.append((marker_id, " ".join(words), False))
|
||||
contexts.append((marker_id, " ".join(words), False, "before"))
|
||||
else:
|
||||
borrowed = False
|
||||
for j in range(i + 1, len(raw_contexts)):
|
||||
@@ -236,11 +239,24 @@ def _extract_marker_contexts(
|
||||
if next_marker_id in (slides or {}):
|
||||
break
|
||||
words = next_text.split()[:10]
|
||||
contexts.append((marker_id, " ".join(words), True))
|
||||
contexts.append((marker_id, " ".join(words), True, "before"))
|
||||
borrowed = True
|
||||
break
|
||||
if not borrowed:
|
||||
contexts.append((marker_id, "", False))
|
||||
# No following text and blocked by a slide boundary — look
|
||||
# backward for the tail of the preceding narration block and
|
||||
# anchor to the END of those words instead of extrapolating.
|
||||
preceding_text = ""
|
||||
for k in range(i - 1, -1, -1):
|
||||
if raw_contexts[k][1]:
|
||||
preceding_text = raw_contexts[k][1]
|
||||
break
|
||||
if preceding_text:
|
||||
words = preceding_text.split()
|
||||
tail = " ".join(words[-6:])
|
||||
contexts.append((marker_id, tail, False, "after"))
|
||||
else:
|
||||
contexts.append((marker_id, "", False, "before"))
|
||||
|
||||
return contexts
|
||||
|
||||
@@ -250,13 +266,18 @@ def _fuzzy_match_ratio(
|
||||
transcription: list[TranscribedWord],
|
||||
start_idx: int,
|
||||
window_size: int = 10,
|
||||
pre_filler: int = 30,
|
||||
inter_filler: int = 3,
|
||||
) -> tuple[float, int, int]:
|
||||
"""
|
||||
Calculate how many words from phrase match the transcription at start_idx.
|
||||
|
||||
Words are matched sequentially: each phrase word must appear at or after
|
||||
the position of the previous match. This prevents false matches where
|
||||
phrase words appear out of order or far into the window.
|
||||
Words are matched sequentially. Two separate filler tolerances:
|
||||
- pre_filler: max words before the FIRST phrase word (absorbs ad-libs)
|
||||
- inter_filler: max words between consecutive phrase words (keeps the
|
||||
match tight so common words don't stretch the window far
|
||||
into later text, which would push last_idx past subsequent
|
||||
markers' positions)
|
||||
|
||||
Returns (ratio, first_match_offset, last_match_end_offset) where offsets
|
||||
are relative to start_idx. last_match_end_offset points past the last
|
||||
@@ -265,14 +286,13 @@ def _fuzzy_match_ratio(
|
||||
if not phrase_words:
|
||||
return 0.0, 0, 0
|
||||
|
||||
words_to_check = min(len(phrase_words), window_size)
|
||||
# +30 filler allowance: absorbs ad-libbed words spoken before or between
|
||||
# the manuscript cue words without breaking the match ratio.
|
||||
transcript_end = min(start_idx + words_to_check + 30, len(transcription))
|
||||
|
||||
if start_idx >= len(transcription):
|
||||
return 0.0, 0, 0
|
||||
|
||||
words_to_check = min(len(phrase_words), window_size)
|
||||
# Window only needs to cover pre_filler + phrase words + inter_filler slack
|
||||
transcript_end = min(start_idx + pre_filler + words_to_check + inter_filler, len(transcription))
|
||||
|
||||
transcript_words = [
|
||||
_normalize_token(transcription[j].word)
|
||||
for j in range(start_idx, transcript_end)
|
||||
@@ -290,7 +310,14 @@ def _fuzzy_match_ratio(
|
||||
continue
|
||||
words_checked += 1
|
||||
|
||||
for j in range(t_pos, len(transcript_words)):
|
||||
# First phrase word may be preceded by a long ad-lib; subsequent words
|
||||
# should appear within a few positions of each other.
|
||||
if matches == 0:
|
||||
search_end = min(t_pos + pre_filler + 1, len(transcript_words))
|
||||
else:
|
||||
search_end = min(t_pos + inter_filler + 1, len(transcript_words))
|
||||
|
||||
for j in range(t_pos, search_end):
|
||||
t_word = transcript_words[j]
|
||||
matched = False
|
||||
if normalized == t_word:
|
||||
@@ -344,7 +371,11 @@ def _find_phrase_timestamp(
|
||||
best_first_offset = first_offset
|
||||
best_end_offset = end_offset
|
||||
|
||||
if ratio >= 0.95:
|
||||
# Sequential alignment: stop at the first position that clears the
|
||||
# threshold. Continuing to scan the full transcript risks jumping
|
||||
# to a higher-ratio match much later and skipping over subsequent
|
||||
# markers' positions entirely.
|
||||
if best_ratio >= fuzzy_threshold:
|
||||
break
|
||||
|
||||
if best_ratio >= fuzzy_threshold and best_idx >= 0:
|
||||
@@ -393,8 +424,8 @@ def align_markers_to_transcription(
|
||||
last_idx = 0
|
||||
last_end_time = 0.0
|
||||
|
||||
for marker_id, following_text, is_borrowed in contexts:
|
||||
if not following_text.strip():
|
||||
for marker_id, anchor_text, is_borrowed, anchor_type in contexts:
|
||||
if not anchor_text.strip():
|
||||
marker_time = last_end_time + 1.0
|
||||
timings.append(
|
||||
MarkerTiming(
|
||||
@@ -408,34 +439,50 @@ def align_markers_to_transcription(
|
||||
continue
|
||||
|
||||
idx, timestamp, confidence, match_end_idx = _find_phrase_timestamp(
|
||||
following_text,
|
||||
anchor_text,
|
||||
transcription,
|
||||
start_from=last_idx,
|
||||
fuzzy_threshold=fuzzy_threshold,
|
||||
)
|
||||
|
||||
if idx >= 0:
|
||||
adjusted_time = max(0.0, timestamp - 0.5)
|
||||
timings.append(
|
||||
MarkerTiming(
|
||||
marker_id=marker_id,
|
||||
timestamp=adjusted_time,
|
||||
context=following_text[:50],
|
||||
confidence=confidence,
|
||||
if anchor_type == "after":
|
||||
# Marker trails a narration block — place it at the END of the
|
||||
# matched phrase (when those words finish being spoken).
|
||||
end_idx = min(match_end_idx - 1, len(transcription) - 1)
|
||||
marker_time = transcription[end_idx].end if transcription else 0.0
|
||||
timings.append(
|
||||
MarkerTiming(
|
||||
marker_id=marker_id,
|
||||
timestamp=marker_time,
|
||||
context=f"(end of: {anchor_text[:40]})",
|
||||
confidence=confidence,
|
||||
)
|
||||
)
|
||||
)
|
||||
if not is_borrowed:
|
||||
last_idx = match_end_idx
|
||||
if last_idx > 0 and last_idx <= len(transcription):
|
||||
last_end_time = transcription[last_idx - 1].end
|
||||
else:
|
||||
last_end_time = transcription[-1].end if transcription else 0.0
|
||||
last_end_time = marker_time
|
||||
else:
|
||||
adjusted_time = max(0.0, timestamp - 0.5)
|
||||
timings.append(
|
||||
MarkerTiming(
|
||||
marker_id=marker_id,
|
||||
timestamp=adjusted_time,
|
||||
context=anchor_text[:50],
|
||||
confidence=confidence,
|
||||
)
|
||||
)
|
||||
if not is_borrowed:
|
||||
last_idx = match_end_idx
|
||||
if last_idx > 0 and last_idx <= len(transcription):
|
||||
last_end_time = transcription[last_idx - 1].end
|
||||
else:
|
||||
last_end_time = transcription[-1].end if transcription else 0.0
|
||||
else:
|
||||
timings.append(
|
||||
MarkerTiming(
|
||||
marker_id=marker_id,
|
||||
timestamp=-1.0,
|
||||
context=following_text[:50],
|
||||
context=anchor_text[:50],
|
||||
confidence=0.0,
|
||||
)
|
||||
)
|
||||
@@ -696,6 +743,16 @@ def build_render_plan(
|
||||
# Save narration end time (before outro)
|
||||
narration_end_time = total_duration
|
||||
|
||||
# Resolve any outro videos missing from videos.json via shared_assets.
|
||||
if config.outro:
|
||||
missing_outro_ids = [vid_id for vid_id in config.outro if vid_id not in videos]
|
||||
if missing_outro_ids:
|
||||
found = resolve_missing_videos(missing_outro_ids, project_path, config)
|
||||
videos.update(found)
|
||||
still_missing = [vid_id for vid_id in config.outro if vid_id not in videos]
|
||||
for vid_id in still_missing:
|
||||
print(f" WARNING: outro video '{vid_id}' not found in videos.json or shared_assets — skipped", flush=True)
|
||||
|
||||
# Build outro events (plays after narration ends)
|
||||
outro_events = _extract_outro_events(
|
||||
config.outro,
|
||||
|
||||
Reference in New Issue
Block a user