Ading cleanup and small improvements

This commit is contained in:
2026-07-16 16:00:19 +02:00
parent 24b04fd55e
commit fa8512ff3d
2 changed files with 62 additions and 11 deletions
+38 -6
View File
@@ -2185,22 +2185,54 @@ def _prune_manifest(
if (key.lower() if lowercase_keys else key) not in keep_ids if (key.lower() if lowercase_keys else key) not in keep_ids
] ]
if not to_remove: # Case-collision duplicates: when keys are case-insensitive (videos), several
# keys can share the same lowercase form (e.g. "KnightRotating" and
# "knightrotating"). Markers resolve by lowercased id, so only the exact
# lowercase key is ever reachable — the other case-variants are dead weight.
# Collapse each surviving collision group to its lowercase canonical.
dup_remove: list[tuple[str, str]] = [] # (removed_key, kept_canonical_key)
if lowercase_keys:
remove_set = set(to_remove)
groups: dict[str, list[str]] = {}
for key in data:
if key in remove_set:
continue
groups.setdefault(key.lower(), []).append(key)
for low, keys in groups.items():
if len(keys) < 2:
continue
# Keep the already-lowercase key (what the renderer resolves); else
# the first-seen, so at least one entry survives.
canonical = next((k for k in keys if k == low), keys[0])
for key in keys:
if key != canonical:
dup_remove.append((key, canonical))
if not to_remove and not dup_remove:
if verbose: if verbose:
print(f" {path.name}: all {len(data)} {label} entries in use.") print(f" {path.name}: all {len(data)} {label} entries in use.")
return 0 return 0
print(f" {path.name}: removing {len(to_remove)} unused {label} entr" if to_remove:
f"{'y' if len(to_remove) == 1 else 'ies'}:") print(f" {path.name}: removing {len(to_remove)} unused {label} entr"
for key in to_remove: f"{'y' if len(to_remove) == 1 else 'ies'}:")
print(f" - {key}") for key in to_remove:
print(f" - {key}")
if dup_remove:
print(f" {path.name}: removing {len(dup_remove)} case-duplicate {label} entr"
f"{'y' if len(dup_remove) == 1 else 'ies'}:")
for key, canonical in dup_remove:
print(f" - {key} (duplicate of '{canonical}')")
if not dry_run: if not dry_run:
for key in to_remove: for key in to_remove:
del data[key] del data[key]
for key, _ in dup_remove:
del data[key]
_write_json_preserve(path, data) _write_json_preserve(path, data)
return len(to_remove) return len(to_remove) + len(dup_remove)
def _prune_narration(project_path: Path, verbose: bool, dry_run: bool) -> int: def _prune_narration(project_path: Path, verbose: bool, dry_run: bool) -> int:
+24 -5
View File
@@ -1019,7 +1019,7 @@ def build_filter_complex(
zoomed_width = int(cut_width * zoom) zoomed_width = int(cut_width * zoom)
zoomed_height = int(cut_height * zoom) zoomed_height = int(cut_height * zoom)
if not plan.narration_pauses or narration_concat: if not plan.narration_pauses:
video_label = f"av{i}" video_label = f"av{i}"
filters.append( filters.append(
f"{narr_src}fps={plan.config.fps},setpts=PTS-STARTPTS," f"{narr_src}fps={plan.config.fps},setpts=PTS-STARTPTS,"
@@ -1035,9 +1035,19 @@ def build_filter_complex(
) )
current_label = next_label current_label = next_label
else: else:
# Freeze-splice: cut the narration video at each pause and re-time the
# remainder so it resumes exactly where it stopped (the cutscene owns
# the frame during the gap). narr_src may be the concat filter output
# [narrsrc_v], which is single-consumer, so split it into one branch
# per segment before trimming.
segments = _build_narration_segments( segments = _build_narration_segments(
plan.narration_pauses, plan.total_duration plan.narration_pauses, plan.total_duration
) )
split_labels = [f"nvsplit{i}_{k}" for k in range(len(segments))]
filters.append(
f"{narr_src}split={len(segments)}"
+ "".join(f"[{lbl}]" for lbl in split_labels)
)
for seg_idx, (src_start, src_end, out_start, out_end) in enumerate( for seg_idx, (src_start, src_end, out_start, out_end) in enumerate(
segments segments
@@ -1045,7 +1055,7 @@ def build_filter_complex(
seg_label = f"av{i}_seg{seg_idx}" seg_label = f"av{i}_seg{seg_idx}"
pts_offset = out_start pts_offset = out_start
filters.append( filters.append(
f"{narr_src}trim={src_start:.3f}:{src_end:.3f}," f"[{split_labels[seg_idx]}]trim={src_start:.3f}:{src_end:.3f},"
f"setpts=PTS-STARTPTS+{pts_offset:.3f}/TB," f"setpts=PTS-STARTPTS+{pts_offset:.3f}/TB,"
f"format=yuva444p10le," f"format=yuva444p10le,"
f"scale={zoomed_width}:{zoomed_height}:force_original_aspect_ratio=increase," f"scale={zoomed_width}:{zoomed_height}:force_original_aspect_ratio=increase,"
@@ -1275,7 +1285,7 @@ def build_filter_complex(
plan.narration_end_time if plan.outro_events else plan.total_duration plan.narration_end_time if plan.outro_events else plan.total_duration
) )
if not plan.narration_pauses or narration_concat: if not plan.narration_pauses:
# Simple case: trim main audio to end before outro (with optional channel and volume filters) # Simple case: trim main audio to end before outro (with optional channel and volume filters)
filter_parts = [] filter_parts = []
if channel_filter: if channel_filter:
@@ -1299,8 +1309,17 @@ def build_filter_complex(
else: else:
audio_labels_to_mix.append(f"{_main_aud}") audio_labels_to_mix.append(f"{_main_aud}")
else: else:
# Complex case: segment the narration audio for pauses # Freeze-splice: cut the narration audio at each pause and delay the
# remainder so it resumes on the exact sample it stopped on (the
# cutscene's own audio fills the gap). _main_aud may be the concat
# output [narrsrc_a], which is single-consumer, so asplit it into one
# branch per segment first.
segments = _build_narration_segments(plan.narration_pauses, audio_end_time) segments = _build_narration_segments(plan.narration_pauses, audio_end_time)
asplit_labels = [f"narr_aud_split{k}" for k in range(len(segments))]
filters.append(
f"{_main_aud}asplit={len(segments)}"
+ "".join(f"[{lbl}]" for lbl in asplit_labels)
)
for seg_idx, (src_start, src_end, out_start, out_end) in enumerate( for seg_idx, (src_start, src_end, out_start, out_end) in enumerate(
segments segments
): ):
@@ -1317,7 +1336,7 @@ def build_filter_complex(
if volume_filter: if volume_filter:
filter_parts.append(volume_filter) filter_parts.append(volume_filter)
filters.append( filters.append(
f"{_main_aud}{','.join(filter_parts)}[{seg_label}]" f"[{asplit_labels[seg_idx]}]{','.join(filter_parts)}[{seg_label}]"
) )
audio_labels_to_mix.append(f"[{seg_label}]") audio_labels_to_mix.append(f"[{seg_label}]")