Concat multi-shot source clips and auto-regen match_report.html

1. Cutter-report source clip for multi-segment beats was using only the
   primary in/out, which equals the FIRST segment's range. Beat 10 with
   3 shots therefore showed only ~0.88 s of source instead of all 3.32 s.
   Added extract_concat_clip(): renders each segment as its own MP4 and
   concatenates them via ffmpeg's concat demuxer into one continuous
   source clip the same length as the trailer beat.

   Per-segment intermediate clips (beat_NN_source_seg00.mp4 etc.) are
   kept too so individual shots stay inspectable.

2. _regenerate_cutter_report now also regenerates the legacy
   output/report/match_report.html via src.pipeline.reporter.generate_report.
   Both reports stay in sync after every match command.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Melbar
2026-05-05 04:40:01 +02:00
parent cc27208d2a
commit b70d7e11be
10 changed files with 108 additions and 16 deletions
+27 -8
View File
@@ -93,20 +93,39 @@ def _save_results(results: list, cfg: "AppConfig") -> None: # type: ignore[name
def _regenerate_cutter_report(cfg: "AppConfig") -> None: # type: ignore[name-defined]
"""Re-render CUTTER_REPORT.{md,html} after each cache write so they stay in sync."""
"""Re-render CUTTER_REPORT.{md,html} and output/report/match_report.html.
Called from every match-style command after the cache is written so all
cutter-facing artefacts stay in sync with the current `match_results.json`.
Failures are logged but never abort the run — the cache is the source of
truth, the reports can always be re-rendered manually later.
"""
try:
from scripts.generate_cutter_report import render_report
except Exception as exc:
logging.getLogger(__name__).warning("Cutter report regen skipped: %s", exc)
return
else:
try:
project_root = cfg.paths.cache_dir.parent
md, html = render_report(project_root, with_stills=True, with_clips=True)
(project_root / "CUTTER_REPORT.md").write_text(md, encoding="utf-8")
(project_root / "CUTTER_REPORT.html").write_text(html, encoding="utf-8")
logging.getLogger(__name__).info("Cutter report regenerated (md + html)")
except Exception as exc:
logging.getLogger(__name__).warning("Cutter report regen failed: %s", exc)
# Also keep the legacy output/report/match_report.html in sync. It uses
# its own preview-clip pipeline (frame-locked compare videos) and is the
# heavier of the two reports — kept up-to-date so the cutter can choose
# whichever view they prefer.
try:
project_root = cfg.paths.cache_dir.parent
md, html = render_report(project_root, with_stills=True, with_clips=True)
(project_root / "CUTTER_REPORT.md").write_text(md, encoding="utf-8")
(project_root / "CUTTER_REPORT.html").write_text(html, encoding="utf-8")
logging.getLogger(__name__).info("Cutter report regenerated (md + html)")
from src.pipeline.reporter import generate_report
all_beats = _load_beats(cfg)
all_results = _normalize_cached_results(all_beats, _load_results(cfg), cfg)
generate_report(all_beats, all_results, cfg)
logging.getLogger(__name__).info("Match report regenerated → output/report/match_report.html")
except Exception as exc:
logging.getLogger(__name__).warning("Cutter report regen failed: %s", exc)
logging.getLogger(__name__).warning("Match report regen failed: %s", exc)
def _load_results(cfg: "AppConfig") -> list: # type: ignore[name-defined]