Adding some files

This commit is contained in:
2026-05-11 21:45:30 +02:00
parent b9376cd650
commit feb4df0506
3 changed files with 142 additions and 43 deletions
+89 -32
View File
@@ -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,