Fixing audio that doesn't silence
This commit is contained in:
+23
-2
@@ -841,9 +841,22 @@ def _import_shared_audio(
|
|||||||
existing: dict = _read_json(audio_json_path) if audio_json_path.exists() else {}
|
existing: dict = _read_json(audio_json_path) if audio_json_path.exists() else {}
|
||||||
|
|
||||||
added = 0
|
added = 0
|
||||||
|
normalized = 0
|
||||||
for f in audio_files:
|
for f in audio_files:
|
||||||
audio_id = f.stem.lower()
|
audio_id = f.stem.lower()
|
||||||
if audio_id in existing:
|
if audio_id in existing:
|
||||||
|
# Backfill the audio schema onto an existing entry: make `loop`
|
||||||
|
# explicit (so it's not an invisible optional the way it was before),
|
||||||
|
# and drop `zoom` — a video-only field that means nothing for audio and
|
||||||
|
# only ended up here from legacy/hand-edited entries.
|
||||||
|
entry = existing[audio_id]
|
||||||
|
if isinstance(entry, dict):
|
||||||
|
if "loop" not in entry:
|
||||||
|
entry["loop"] = None
|
||||||
|
normalized += 1
|
||||||
|
if "zoom" in entry:
|
||||||
|
del entry["zoom"]
|
||||||
|
normalized += 1
|
||||||
if verbose:
|
if verbose:
|
||||||
print(f" Skipping {audio_id} (already in audio.json)")
|
print(f" Skipping {audio_id} (already in audio.json)")
|
||||||
continue
|
continue
|
||||||
@@ -851,16 +864,24 @@ def _import_shared_audio(
|
|||||||
"file": f.name,
|
"file": f.name,
|
||||||
"is_shared": True,
|
"is_shared": True,
|
||||||
"volume": 1.0,
|
"volume": 1.0,
|
||||||
|
# One-shot by default (null → not looping); set true for background
|
||||||
|
# music/ambience that should loop for the whole video.
|
||||||
|
"loop": None,
|
||||||
}
|
}
|
||||||
added += 1
|
added += 1
|
||||||
if verbose:
|
if verbose:
|
||||||
print(f" Added shared audio: {audio_id}")
|
print(f" Added shared audio: {audio_id}")
|
||||||
|
|
||||||
if added > 0:
|
if added > 0 or normalized > 0:
|
||||||
with open(audio_json_path, "w", encoding="utf-8") as fh:
|
with open(audio_json_path, "w", encoding="utf-8") as fh:
|
||||||
json.dump(existing, fh, indent=2)
|
json.dump(existing, fh, indent=2)
|
||||||
|
parts = []
|
||||||
|
if added:
|
||||||
|
parts.append(f"+{added} shared audio files")
|
||||||
|
if normalized:
|
||||||
|
parts.append(f"{normalized} field(s) normalized")
|
||||||
print(
|
print(
|
||||||
f" Updated {audio_json_path.relative_to(project_path)} (+{added} shared audio files)"
|
f" Updated {audio_json_path.relative_to(project_path)} ({', '.join(parts)})"
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
if verbose:
|
if verbose:
|
||||||
|
|||||||
+52
-7
@@ -1544,16 +1544,61 @@ def build_filter_complex(
|
|||||||
)
|
)
|
||||||
audio_labels_to_mix.append(f"[{label}]")
|
audio_labels_to_mix.append(f"[{label}]")
|
||||||
else:
|
else:
|
||||||
# One-shot audio: delay to trigger time. Chunking v2: seek in if
|
# One-shot audio. Freeze it through narration pauses too (like the
|
||||||
# the clip began in an earlier chunk (docs/chunking_v2.md).
|
# looping branch above): split the source at each pause and delay
|
||||||
|
# the remainder, so the clip resumes on the exact sample it stopped
|
||||||
|
# on when the cutscene ends — it is never restarted. A pause set on
|
||||||
|
# a cutscene video therefore silences background one-shots for its
|
||||||
|
# duration. `ignore_pauses` opts a clip out (e.g. a stinger meant to
|
||||||
|
# keep playing under the freeze). Chunking v2: src_offset seeks in
|
||||||
|
# when the clip began in an earlier chunk (docs/chunking_v2.md).
|
||||||
label = f"aud{i}"
|
label = f"aud{i}"
|
||||||
delay_ms = int(event.start_time * 1000)
|
|
||||||
_off = getattr(event, "src_offset", 0.0)
|
_off = getattr(event, "src_offset", 0.0)
|
||||||
_seek = f"atrim={_off:.3f},asetpts=PTS-STARTPTS," if _off > 0 else ""
|
relevant_pauses = (
|
||||||
filters.append(
|
[]
|
||||||
f"[{audio_idx}:a]{_seek}adelay={delay_ms}|{delay_ms},volume={volume:.2f}[{label}]"
|
if event.audio_def.ignore_pauses
|
||||||
|
else [
|
||||||
|
p
|
||||||
|
for p in (plan.narration_pauses or [])
|
||||||
|
if p.output_time > event.start_time
|
||||||
|
]
|
||||||
)
|
)
|
||||||
audio_labels_to_mix.append(f"[{label}]")
|
if not relevant_pauses:
|
||||||
|
delay_ms = int(event.start_time * 1000)
|
||||||
|
_seek = f"atrim={_off:.3f},asetpts=PTS-STARTPTS," if _off > 0 else ""
|
||||||
|
filters.append(
|
||||||
|
f"[{audio_idx}:a]{_seek}adelay={delay_ms}|{delay_ms},volume={volume:.2f}[{label}]"
|
||||||
|
)
|
||||||
|
audio_labels_to_mix.append(f"[{label}]")
|
||||||
|
else:
|
||||||
|
# Play [seg_start, pause) of source, freeze during the pause,
|
||||||
|
# then resume — source position (src_pos) never advances across
|
||||||
|
# the gap. Final segment runs to the source's natural end.
|
||||||
|
src_pos = _off
|
||||||
|
seg_start = event.start_time
|
||||||
|
seg_count = 0
|
||||||
|
for pause in relevant_pauses:
|
||||||
|
if pause.output_time > seg_start:
|
||||||
|
seg_dur = pause.output_time - seg_start
|
||||||
|
seg_label = f"{label}_seg{seg_count}"
|
||||||
|
d_ms = int(seg_start * 1000)
|
||||||
|
filters.append(
|
||||||
|
f"[{audio_idx}:a]atrim={src_pos:.3f}:{src_pos + seg_dur:.3f},"
|
||||||
|
f"asetpts=PTS-STARTPTS,adelay={d_ms}|{d_ms},"
|
||||||
|
f"volume={volume:.2f}[{seg_label}]"
|
||||||
|
)
|
||||||
|
audio_labels_to_mix.append(f"[{seg_label}]")
|
||||||
|
src_pos += seg_dur
|
||||||
|
seg_count += 1
|
||||||
|
seg_start = pause.output_time + pause.duration
|
||||||
|
seg_label = f"{label}_seg{seg_count}"
|
||||||
|
d_ms = int(seg_start * 1000)
|
||||||
|
filters.append(
|
||||||
|
f"[{audio_idx}:a]atrim={src_pos:.3f},"
|
||||||
|
f"asetpts=PTS-STARTPTS,adelay={d_ms}|{d_ms},"
|
||||||
|
f"volume={volume:.2f}[{seg_label}]"
|
||||||
|
)
|
||||||
|
audio_labels_to_mix.append(f"[{seg_label}]")
|
||||||
|
|
||||||
# Extract and mix audio from triggered video events
|
# Extract and mix audio from triggered video events
|
||||||
_have_audio = video_events_with_audio or set()
|
_have_audio = video_events_with_audio or set()
|
||||||
|
|||||||
Reference in New Issue
Block a user