Fixing volume propagation settings through events.json, and fixed bug where failure to match the text on the end slide of a segment now issues a warning, and uses the last word + 2 seconds, rather than the full duration of the clip
This commit is contained in:
+13
-2
@@ -3241,8 +3241,19 @@ def cmd_trim(
|
||||
end_text = slide_texts.get(end_slide, "")
|
||||
end_ts = _find_slide_end_in_transcript(words, end_text, verbose) if end_text else None
|
||||
if end_ts is None:
|
||||
end_abs = total_dur
|
||||
end_note = f"end auto: S{end_slide} not found → to end {total_dur:.2f}s"
|
||||
# Slide-end text didn't match the transcript (e.g. the
|
||||
# narrator drifted from the manuscript). Fall back to the
|
||||
# last spoken word + tail — NOT total_dur, which includes
|
||||
# trailing silence and leaves the clip playing long.
|
||||
end_abs = min(words[-1].end + _TRIM_TAIL_OUT, total_dur)
|
||||
end_note = f"end auto: S{end_slide} not found → last word {words[-1].end:.2f}s +{_TRIM_TAIL_OUT:g}s → {end_abs:.2f}s"
|
||||
print(
|
||||
f"\n ⚠️ {seg_id}: could NOT match the end of S{end_slide} in the transcript.\n"
|
||||
f" The manuscript text for S{end_slide} likely drifted from what was\n"
|
||||
f" spoken. Falling back to the last spoken word ({words[-1].end:.2f}s)\n"
|
||||
f" + {_TRIM_TAIL_OUT:g}s instead of the raw file length ({total_dur:.2f}s).\n"
|
||||
f" Check S{end_slide}'s text or set an explicit `end` for {seg_id}."
|
||||
)
|
||||
else:
|
||||
end_abs = min(end_ts + _TRIM_TAIL_OUT, total_dur)
|
||||
end_note = f"end auto: S{end_slide} last word {end_ts:.2f}s +{_TRIM_TAIL_OUT:g}s → {end_abs:.2f}s"
|
||||
|
||||
@@ -434,6 +434,9 @@ class VideoEvent:
|
||||
cutout: "CutoutDefinition"
|
||||
cutout_name: str = "" # resolved cutout name (e.g. "fullscreen"), for display
|
||||
layer: str = "above" # "above" = on top of slides; "below" = behind slides
|
||||
# Effective audio volume for THIS occurrence: an events.json override if present,
|
||||
# else the videos.json default. The renderer reads this (not video_source.volume).
|
||||
volume: float = 1.0
|
||||
# Chunking v2 seam (see docs/chunking_v2.md): when a clip began before this
|
||||
# chunk's window, the render must seek into it so it resumes mid-clip instead of
|
||||
# restarting at the boundary. None = play from video_source.skip (the v1/default).
|
||||
|
||||
+5
-3
@@ -58,13 +58,15 @@ def _resolve_case_insensitive(path: Path) -> Path:
|
||||
# presentation fields materialized onto events.json and resolved per-event
|
||||
# (transformer.resolve_video_presentation). Global params (skip/zoom/volume/…) are not
|
||||
# yet overridable inline; unknown keys are ignored.
|
||||
_MARKER_OVERRIDE_KEYS = frozenset({"cutout", "layer", "end_on", "take"})
|
||||
_MARKER_OVERRIDE_KEYS = frozenset({"cutout", "layer", "end_on", "take", "volume"})
|
||||
# Override keys that are numeric (coerced to float).
|
||||
_MARKER_NUMERIC_KEYS = frozenset({"take", "volume"})
|
||||
|
||||
|
||||
def _coerce_marker_value(key: str, raw: str):
|
||||
"""Coerce an inline override value: `take` → float; everything else → string."""
|
||||
"""Coerce an inline override value: numeric keys → float; everything else → string."""
|
||||
v = raw.strip().strip('"').strip("'")
|
||||
if key == "take":
|
||||
if key in _MARKER_NUMERIC_KEYS:
|
||||
try:
|
||||
return float(v)
|
||||
except ValueError:
|
||||
|
||||
+2
-1
@@ -1568,7 +1568,8 @@ def build_filter_complex(
|
||||
delay_ms = int(event.start_time * 1000)
|
||||
label = f"tvaud{i}"
|
||||
|
||||
vol = event.video_source.volume
|
||||
# event.volume = events.json override if set, else the videos.json default.
|
||||
vol = event.volume
|
||||
vol_filter = f",volume={vol:.2f}" if vol != 1.0 else ""
|
||||
filters.append(
|
||||
f"[{video_idx}:a]atrim=0:{duration:.3f},"
|
||||
|
||||
+11
-3
@@ -30,9 +30,13 @@ from typing import Optional
|
||||
from .models import CAMERA_PRESETS
|
||||
from .transformer import MarkerTiming, resolve_video_presentation
|
||||
|
||||
# Per-occurrence presentation fields materialized onto video events (atomic events.json,
|
||||
# GUI-ready). Round-tripped as overrides so a stored value drives render over the default.
|
||||
# Per-occurrence presentation fields ALWAYS materialized onto video events (atomic
|
||||
# events.json, GUI-ready). Round-tripped as overrides so a stored value drives render.
|
||||
_PRESENTATION_KEYS = ("cutout", "layer", "end_on", "take")
|
||||
# `volume` is materialized SPARSELY — only when actually overridden (inline/GUI/manual),
|
||||
# so the videos.json value keeps flowing as the default and only a real override pins it.
|
||||
# Round-tripping still carries it whenever present in the event dict.
|
||||
_EVENT_OVERRIDE_KEYS = _PRESENTATION_KEYS + ("volume",)
|
||||
|
||||
EVENTS_FILE = "events.json"
|
||||
SCAFFOLD_FILE = "scaffold.json"
|
||||
@@ -184,6 +188,10 @@ def derive_events(
|
||||
e["end_on"] = pres["end_on"]
|
||||
if pres["take"] is not None:
|
||||
e["take"] = pres["take"]
|
||||
# volume is sparse: written only when actually overridden, so the
|
||||
# videos.json default keeps flowing until someone pins it here.
|
||||
if t.overrides and "volume" in t.overrides:
|
||||
e["volume"] = pres["volume"]
|
||||
pd = _pause_duration(t.marker_id, videos)
|
||||
if pd:
|
||||
e["pause_duration"] = pd
|
||||
@@ -327,7 +335,7 @@ def events_to_marker_timings(events: list[dict]) -> list[MarkerTiming]:
|
||||
eff = (n + e.get("adjustment", 0.0)) if n is not None else -1.0
|
||||
# Carry any stored presentation as overrides so render honors the atomic event
|
||||
# (e.g. a GUI edit) over the shorthand/videos.json default. Absent on old events.
|
||||
overrides = {k: e[k] for k in _PRESENTATION_KEYS if k in e} or None
|
||||
overrides = {k: e[k] for k in _EVENT_OVERRIDE_KEYS if k in e} or None
|
||||
timings.append(
|
||||
MarkerTiming(
|
||||
marker_id=e["id"],
|
||||
|
||||
@@ -97,6 +97,8 @@ def resolve_video_presentation(
|
||||
pause_narration = overrides.get(
|
||||
"pause_narration", video_source.pause_narration or 0.0
|
||||
)
|
||||
# Volume defaults to the videos.json value; an events.json/inline override wins.
|
||||
volume = overrides["volume"] if "volume" in overrides else video_source.volume
|
||||
|
||||
return {
|
||||
"handle": handle,
|
||||
@@ -105,6 +107,7 @@ def resolve_video_presentation(
|
||||
"end_on": end_on,
|
||||
"take": take,
|
||||
"pause_narration": float(pause_narration or 0.0),
|
||||
"volume": float(volume if volume is not None else 1.0),
|
||||
}
|
||||
|
||||
|
||||
@@ -1368,6 +1371,7 @@ def _extract_video_events(
|
||||
end_on = pres["end_on"]
|
||||
take = pres["take"]
|
||||
pause_narration = pres["pause_narration"]
|
||||
volume = pres["volume"]
|
||||
|
||||
if end_on == "take" and take is not None:
|
||||
end_time = start_time + take
|
||||
@@ -1440,6 +1444,7 @@ def _extract_video_events(
|
||||
cutout=cutout,
|
||||
cutout_name=cutout_name,
|
||||
layer=layer,
|
||||
volume=volume,
|
||||
skip_override=skip_override,
|
||||
)
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user