Update cutter report
This commit is contained in:
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
# Cutter-Report — manuelles Nachschneiden
|
# Cutter-Report — manuelles Nachschneiden
|
||||||
|
|
||||||
Generiert: **2026-05-09 19:06:01**
|
Generiert: **2026-05-18 08:46:49**
|
||||||
|
|
||||||
- **Trailer**: `BehindTheRedDoor_Trailer_REFERENCE.mp4` @ 25.000 fps
|
- **Trailer**: `BehindTheRedDoor_Trailer_REFERENCE.mp4` @ 25.000 fps
|
||||||
- **Source** : `BehindTheRedDoor_FTR_1080P_2398_Fixed.mp4` @ 23.976 fps
|
- **Source** : `BehindTheRedDoor_FTR_1080P_2398_Fixed.mp4` @ 23.976 fps
|
||||||
|
|||||||
@@ -384,6 +384,7 @@ def _normalize_cached_results(beats: list, results: list, cfg) -> list:
|
|||||||
|
|
||||||
fps = _scene_fps_light(scene, cfg)
|
fps = _scene_fps_light(scene, cfg)
|
||||||
adjusted_in_s = result.in_point_s
|
adjusted_in_s = result.in_point_s
|
||||||
|
phase_changed = False
|
||||||
scene_changed = int(scene["scene_id"]) != result.scene_id
|
scene_changed = int(scene["scene_id"]) != result.scene_id
|
||||||
starts_before_scene = result.in_point_s < float(scene["start_s"])
|
starts_before_scene = result.in_point_s < float(scene["start_s"])
|
||||||
if scene_changed or starts_before_scene or result.duration_s <= 0.12:
|
if scene_changed or starts_before_scene or result.duration_s <= 0.12:
|
||||||
@@ -392,6 +393,25 @@ def _normalize_cached_results(beats: list, results: list, cfg) -> list:
|
|||||||
scene = _scene_for_time_light(scenes, adjusted_in_s, cfg) or scene
|
scene = _scene_for_time_light(scenes, adjusted_in_s, cfg) or scene
|
||||||
fps = _scene_fps_light(scene, cfg)
|
fps = _scene_fps_light(scene, cfg)
|
||||||
|
|
||||||
|
should_phase_probe = (
|
||||||
|
scene_changed
|
||||||
|
or starts_before_scene
|
||||||
|
or not result.is_confirmed
|
||||||
|
or result.match_score < cfg.cv.deep_scan.match_threshold
|
||||||
|
)
|
||||||
|
phase_score = result.match_score
|
||||||
|
if should_phase_probe:
|
||||||
|
probe = _phase_probe_segment_in_scene(beat, scene, adjusted_in_s, cfg)
|
||||||
|
if probe is not None:
|
||||||
|
probed_in_s, probed_score = probe
|
||||||
|
max_shift_s = max(0.12, min(0.75, beat.duration_s * 0.35))
|
||||||
|
if abs(probed_in_s - adjusted_in_s) <= max_shift_s:
|
||||||
|
adjusted_in_s = probed_in_s
|
||||||
|
phase_changed = True
|
||||||
|
phase_score = max(float(result.match_score), float(probed_score))
|
||||||
|
scene = _scene_for_time_light(scenes, adjusted_in_s, cfg) or scene
|
||||||
|
fps = _scene_fps_light(scene, cfg)
|
||||||
|
|
||||||
matchable_duration_s = beat.duration_s
|
matchable_duration_s = beat.duration_s
|
||||||
try:
|
try:
|
||||||
from src.cv.global_scan import estimate_matchable_reference_duration
|
from src.cv.global_scan import estimate_matchable_reference_duration
|
||||||
@@ -414,6 +434,7 @@ def _normalize_cached_results(beats: list, results: list, cfg) -> list:
|
|||||||
if (
|
if (
|
||||||
scene_changed
|
scene_changed
|
||||||
or starts_before_scene
|
or starts_before_scene
|
||||||
|
or phase_changed
|
||||||
or result.duration_s <= 0.12
|
or result.duration_s <= 0.12
|
||||||
or result.out_point_s > adjusted_in_s + max_duration_s + (1.0 / fps)
|
or result.out_point_s > adjusted_in_s + max_duration_s + (1.0 / fps)
|
||||||
):
|
):
|
||||||
@@ -423,6 +444,8 @@ def _normalize_cached_results(beats: list, results: list, cfg) -> list:
|
|||||||
in_point_s=adjusted_in_s,
|
in_point_s=adjusted_in_s,
|
||||||
out_point_s=adjusted_in_s + max_duration_s,
|
out_point_s=adjusted_in_s + max_duration_s,
|
||||||
in_point_frame=int(adjusted_in_s * fps),
|
in_point_frame=int(adjusted_in_s * fps),
|
||||||
|
match_score=phase_score,
|
||||||
|
is_confirmed=phase_score >= cfg.cv.deep_scan.match_threshold,
|
||||||
)
|
)
|
||||||
|
|
||||||
coverage = (
|
coverage = (
|
||||||
@@ -2404,6 +2427,141 @@ def cmd_run(args: argparse.Namespace, cfg) -> None:
|
|||||||
cmd_export(args, cfg)
|
cmd_export(args, cfg)
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_preview(args: argparse.Namespace, cfg) -> None:
|
||||||
|
"""Assemble a rough preview video from cached source matches, with original audio."""
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
results_path = _results_cache_path(cfg)
|
||||||
|
if not results_path.exists():
|
||||||
|
log.error("No match_results.json — run 'match' first.")
|
||||||
|
return
|
||||||
|
|
||||||
|
data = sorted(
|
||||||
|
json.loads(results_path.read_text(encoding="utf-8")),
|
||||||
|
key=lambda r: r["beat_id"],
|
||||||
|
)
|
||||||
|
|
||||||
|
beats_path = cfg.paths.cache_dir / "trailer_beats.json"
|
||||||
|
beats_by_id: dict = {}
|
||||||
|
if beats_path.exists():
|
||||||
|
for b in json.loads(beats_path.read_text(encoding="utf-8")):
|
||||||
|
beats_by_id[int(b["beat_id"])] = b
|
||||||
|
|
||||||
|
clip_width = 1280
|
||||||
|
fps = 25
|
||||||
|
out_dir = cfg.paths.output_dir / "preview_clips"
|
||||||
|
out_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
preview_out = cfg.paths.output_dir / "preview.mp4"
|
||||||
|
|
||||||
|
def _run(cmd: list, timeout: int = 120) -> bool:
|
||||||
|
r = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout)
|
||||||
|
if r.returncode != 0:
|
||||||
|
log.debug("ffmpeg stderr: %s", r.stderr[-600:])
|
||||||
|
return r.returncode == 0
|
||||||
|
|
||||||
|
def extract_with_audio(src: Path, start_s: float, duration_s: float, out: Path) -> bool:
|
||||||
|
preroll = 2.0 if start_s >= 2.0 else 0.0
|
||||||
|
input_seek = max(0.0, start_s - preroll)
|
||||||
|
accurate_seek = start_s - input_seek
|
||||||
|
return _run([
|
||||||
|
"ffmpeg", "-y", "-loglevel", "error",
|
||||||
|
"-ss", f"{input_seek:.3f}", "-i", str(src),
|
||||||
|
"-ss", f"{accurate_seek:.3f}", "-t", f"{max(0.04, duration_s):.3f}",
|
||||||
|
"-map", "0:v:0", "-map", "0:a:0",
|
||||||
|
"-c:v", "libx264", "-preset", "veryfast", "-crf", "23",
|
||||||
|
"-vf", f"fps={fps},scale={clip_width}:-2,setsar=1,setpts=PTS-STARTPTS",
|
||||||
|
"-c:a", "aac", "-ar", "48000", "-ac", "2",
|
||||||
|
"-pix_fmt", "yuv420p", "-movflags", "+faststart", str(out),
|
||||||
|
])
|
||||||
|
|
||||||
|
def black_silence(duration_s: float, out: Path) -> bool:
|
||||||
|
return _run([
|
||||||
|
"ffmpeg", "-y", "-loglevel", "error",
|
||||||
|
"-f", "lavfi", "-i", f"color=black:s={clip_width}x720:r={fps}",
|
||||||
|
"-f", "lavfi", "-i", "anullsrc=r=48000:cl=stereo",
|
||||||
|
"-t", f"{max(0.5, duration_s):.3f}",
|
||||||
|
"-c:v", "libx264", "-preset", "veryfast", "-crf", "23",
|
||||||
|
"-c:a", "aac", "-pix_fmt", "yuv420p", "-movflags", "+faststart", str(out),
|
||||||
|
])
|
||||||
|
|
||||||
|
def concat_clips(parts: list[Path], out: Path) -> bool:
|
||||||
|
lst = out.with_suffix(".txt")
|
||||||
|
lst.write_text(
|
||||||
|
"\n".join(f"file '{p.resolve().as_posix()}'" for p in parts),
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
ok = _run([
|
||||||
|
"ffmpeg", "-y", "-loglevel", "error",
|
||||||
|
"-f", "concat", "-safe", "0", "-i", str(lst),
|
||||||
|
"-c", "copy", str(out),
|
||||||
|
], timeout=300)
|
||||||
|
lst.unlink(missing_ok=True)
|
||||||
|
return ok
|
||||||
|
|
||||||
|
beat_clips: list[Path] = []
|
||||||
|
|
||||||
|
for rec in data:
|
||||||
|
bid = int(rec["beat_id"])
|
||||||
|
segs = rec.get("segments", [])
|
||||||
|
src = Path(rec["source_path"]) if rec.get("source_path") else None
|
||||||
|
clip_out = out_dir / f"beat_{bid:02d}.mp4"
|
||||||
|
|
||||||
|
if src is None or not src.exists():
|
||||||
|
beat = beats_by_id.get(bid, {})
|
||||||
|
dur = max(0.5, float(beat.get("end_s", 1)) - float(beat.get("start_s", 0)))
|
||||||
|
log.info("Beat %02d: NO MATCH — black/silence %.2fs", bid, dur)
|
||||||
|
if black_silence(dur, clip_out):
|
||||||
|
beat_clips.append(clip_out)
|
||||||
|
continue
|
||||||
|
|
||||||
|
if len(segs) >= 2:
|
||||||
|
parts: list[Path] = []
|
||||||
|
for idx, seg in enumerate(segs):
|
||||||
|
in_s = float(seg["in_point_s"])
|
||||||
|
dur = max(0.04, float(seg["out_point_s"]) - in_s)
|
||||||
|
seg_src = Path(seg["source_path"]) if seg.get("source_path") else src
|
||||||
|
part = out_dir / f"beat_{bid:02d}_seg{idx:02d}.mp4"
|
||||||
|
log.info("Beat %02d seg%d: scene=%s %.2fs–%.2fs", bid, idx, seg.get("scene_id"), in_s, in_s + dur)
|
||||||
|
if extract_with_audio(seg_src, in_s, dur, part):
|
||||||
|
parts.append(part)
|
||||||
|
if not parts:
|
||||||
|
log.warning("Beat %02d: no segments extracted", bid)
|
||||||
|
continue
|
||||||
|
if len(parts) == 1:
|
||||||
|
parts[0].rename(clip_out)
|
||||||
|
beat_clips.append(clip_out)
|
||||||
|
else:
|
||||||
|
if concat_clips(parts, clip_out):
|
||||||
|
beat_clips.append(clip_out)
|
||||||
|
for p in parts:
|
||||||
|
p.unlink(missing_ok=True)
|
||||||
|
else:
|
||||||
|
in_s = float(rec["in_point_s"])
|
||||||
|
beat = beats_by_id.get(bid, {})
|
||||||
|
beat_dur = float(beat["end_s"]) - float(beat["start_s"]) if beat else 0.0
|
||||||
|
source_dur = float(rec["out_point_s"]) - in_s
|
||||||
|
dur = max(0.04, beat_dur if beat_dur > 0.04 else source_dur)
|
||||||
|
log.info("Beat %02d: scene=%s %.2fs+%.2fs (trailer=%.2fs src=%.2fs)", bid, rec.get("scene_id"), in_s, dur, beat_dur, source_dur)
|
||||||
|
if extract_with_audio(src, in_s, dur, clip_out):
|
||||||
|
beat_clips.append(clip_out)
|
||||||
|
else:
|
||||||
|
log.warning("Beat %02d: extraction failed", bid)
|
||||||
|
|
||||||
|
if not beat_clips:
|
||||||
|
log.error("No clips extracted — aborting.")
|
||||||
|
return
|
||||||
|
|
||||||
|
log.info("Concatenating %d beat clips → %s", len(beat_clips), preview_out)
|
||||||
|
if concat_clips(beat_clips, preview_out):
|
||||||
|
size_mb = preview_out.stat().st_size / 1_048_576
|
||||||
|
log.info("Preview ready: %s (%.1f MB)", preview_out, size_mb)
|
||||||
|
print(f"\n Preview → {preview_out} ({size_mb:.1f} MB)")
|
||||||
|
else:
|
||||||
|
log.error("Final concat failed — per-beat clips are in %s", out_dir)
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Argument parser
|
# Argument parser
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -2474,6 +2632,12 @@ def _build_parser() -> argparse.ArgumentParser:
|
|||||||
p_run.add_argument("--beat", type=int,
|
p_run.add_argument("--beat", type=int,
|
||||||
help="Run match/report/export for only one cached beat")
|
help="Run match/report/export for only one cached beat")
|
||||||
|
|
||||||
|
# preview
|
||||||
|
sub.add_parser(
|
||||||
|
"preview",
|
||||||
|
help="Build output/preview.mp4 from cached matches — source clips with audio in beat order",
|
||||||
|
)
|
||||||
|
|
||||||
return parser
|
return parser
|
||||||
|
|
||||||
|
|
||||||
@@ -2498,6 +2662,7 @@ def main() -> None:
|
|||||||
"report": cmd_report,
|
"report": cmd_report,
|
||||||
"export": cmd_export,
|
"export": cmd_export,
|
||||||
"run": cmd_run,
|
"run": cmd_run,
|
||||||
|
"preview": cmd_preview,
|
||||||
}
|
}
|
||||||
|
|
||||||
handler = dispatch[args.command]
|
handler = dispatch[args.command]
|
||||||
|
|||||||
@@ -230,6 +230,11 @@ sondern auch den schlechtesten Einzelvergleich, die ersten sichtbaren Frames
|
|||||||
und die Frame-zu-Frame-Bewegung. Dadurch gewinnt nicht mehr ein späteres
|
und die Frame-zu-Frame-Bewegung. Dadurch gewinnt nicht mehr ein späteres
|
||||||
Standbild derselben Einstellung, nur weil Fenster, Gesichter und Licht fast
|
Standbild derselben Einstellung, nur weil Fenster, Gesichter und Licht fast
|
||||||
identisch aussehen.
|
identisch aussehen.
|
||||||
|
Unsichere Einzeltreffer ohne Segmentliste laufen ebenfalls durch diesen lokalen
|
||||||
|
Phasen-Probe. Das repariert alte Cache-Einträge, deren Szene korrekt ist, deren
|
||||||
|
Inpoint aber einige Frames in der Bewegung daneben liegt. Der Probe bleibt auf
|
||||||
|
kleine lokale Shifts begrenzt und wird nicht für jeden bestätigten Treffer
|
||||||
|
erzwungen, damit Report-Refreshes nicht zum Vollscan werden.
|
||||||
Report-Clips werden zusätzlich an den bekannten Source-Szenenstart plus eine
|
Report-Clips werden zusätzlich an den bekannten Source-Szenenstart plus eine
|
||||||
sehr kurze Ein-Frame-Guard-Zone geklemmt, damit ein knapp vor oder direkt auf
|
sehr kurze Ein-Frame-Guard-Zone geklemmt, damit ein knapp vor oder direkt auf
|
||||||
der Schnittkante liegender Inpoint nicht mit Frames der vorherigen Einstellung
|
der Schnittkante liegender Inpoint nicht mit Frames der vorherigen Einstellung
|
||||||
|
|||||||
Reference in New Issue
Block a user