Upgrade CUTTER_REPORT: Frame-Locked Compare, full metadata, auto-commit+push

generate_cutter_report.py:
- Frame-Locked Compare video (trailer left / source right, single MP4) per
  beat replaces two separate side-by-side clips; rendered via accurate
  double-seek + black-fill segmented source reconstruction
- Generation timestamp now includes HH:MM:SS (Uhrzeit-Angabe)
- Per-beat segment list for multi-shot beats (TC, duration, offset, scene,
  score per segment)
- Score warning badge (yellow) if score < 0.65
- python cli.py rematch --beat N command hint in every card
- Overview table links to each beat card via #anchor
- Cleaner dark/light CSS using design tokens (--fg/--bg/--card/--bd)
- --no-clips flag (replaces --with-clips; default is now with clips)

cli.py:
- _auto_commit_push_reports(): after every report regeneration, stages the
  report output files (CUTTER_REPORT.*, output/cutter_clips/, output/report/)
  and auto-commits + pushes to origin/main so remote is always current
- Removed the legacy match_report.html call from _regenerate_cutter_report
  (CUTTER_REPORT now supersedes it)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Melbar
2026-05-06 07:39:02 +02:00
parent 4afb438a4d
commit 64e0132cc7
75 changed files with 924 additions and 515 deletions
+54 -26
View File
@@ -92,40 +92,68 @@ def _save_results(results: list, cfg: "AppConfig") -> None: # type: ignore[name
logging.getLogger(__name__).info("Match results cached → %s", p)
def _auto_commit_push_reports(project_root: "Path") -> None: # type: ignore[name-defined]
"""Stage changed report files, commit, and push to origin.
Only touches report output files — never stages source or config changes.
Failures are logged but never propagate.
"""
import subprocess as _sp
from datetime import datetime as _dt
report_globs = [
"CUTTER_REPORT.html",
"CUTTER_REPORT.md",
"output/report/match_report.html",
"output/report/beat_*_compare.mp4",
"output/report/beat_*_src.mp4",
"output/report/beat_*_ref.mp4",
"output/cutter_clips/beat_*_compare.mp4",
"output/cutter_clips/beat_*_source.mp4",
"output/cutter_clips/beat_*_source_seg*.mp4",
"output/cutter_clips/beat_*_trailer.mp4",
"output/cutter_stills/beat_*_source.jpg",
"output/cutter_stills/beat_*_trailer.jpg",
]
log = logging.getLogger(__name__)
cwd = str(project_root)
try:
for pattern in report_globs:
_sp.run(["git", "add", "--", pattern], capture_output=True, cwd=cwd)
status = _sp.run(
["git", "status", "--porcelain"], capture_output=True, text=True, cwd=cwd
)
if not status.stdout.strip():
log.info("Auto-commit: nothing changed in report files.")
return
now = _dt.now().strftime("%Y-%m-%d %H:%M")
msg = f"Auto-update cutter report {now}\n\nCo-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>"
_sp.run(["git", "commit", "-m", msg], capture_output=True, cwd=cwd, check=True)
_sp.run(["git", "push", "origin", "main"], capture_output=True, cwd=cwd, check=True)
log.info("Auto-commit+push: cutter report updated → remote.")
except Exception as exc:
log.warning("Auto-commit/push failed (non-fatal): %s", exc)
def _regenerate_cutter_report(cfg: "AppConfig") -> None: # type: ignore[name-defined]
"""Re-render CUTTER_REPORT.{md,html} and output/report/match_report.html.
"""Re-render CUTTER_REPORT.{md,html} with Frame-Locked Compare clips.
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.
cutter-facing artefacts stay in sync with `match_results.json`.
After rendering, stages and pushes changed report files to the remote.
Failures are logged but never abort the run.
"""
project_root = cfg.paths.cache_dir.parent
try:
from scripts.generate_cutter_report import render_report
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 + compare clips)")
except Exception as exc:
logging.getLogger(__name__).warning("Cutter report regen skipped: %s", exc)
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)
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:
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("Match report regen failed: %s", exc)
_auto_commit_push_reports(project_root)
def _load_results(cfg: "AppConfig") -> list: # type: ignore[name-defined]