fix: clip pad ±3s + OSD-clock alignment at cut points (single-frame OCR)
ci / smoke (push) Canceled after 0s
ci / smoke (push) Canceled after 0s
This commit is contained in:
1 parent
110d557dbf
commit
14f5ed4aac
7 files changed
+314
-12
No files matched your search
+155
-2
@@ -201,7 +201,8 @@ def test_clip_batch_single_piece_argv(monkeypatch, tmp_path):
|
||||
calls, captured = [], {}
|
||||
monkeypatch.setattr(clip, "_run_ffmpeg", fake_run_factory(calls, captured))
|
||||
out = tmp_path / "sub" / "batch.mp4"
|
||||
clip.clip_batch(BASE, 1, "2026-09-26T09:00:10", "2026-09-26T09:00:50", str(out))
|
||||
clip.clip_batch(BASE, 1, "2026-09-26T09:00:10", "2026-09-26T09:00:50", str(out),
|
||||
pad_seconds=0, align_osd=False)
|
||||
assert len(calls) == 1
|
||||
args, timeout = calls[0]
|
||||
assert timeout == 180
|
||||
@@ -229,7 +230,8 @@ def test_clip_batch_two_pieces_trim_and_concat(monkeypatch, tmp_path):
|
||||
calls, captured = [], {}
|
||||
monkeypatch.setattr(clip, "_run_ffmpeg", fake_run_factory(calls, captured))
|
||||
out = tmp_path / "batch.mp4"
|
||||
clip.clip_batch(BASE, 1, START_ISO, END_ISO, str(out), timeout=60)
|
||||
clip.clip_batch(BASE, 1, START_ISO, END_ISO, str(out), timeout=60,
|
||||
pad_seconds=0, align_osd=False)
|
||||
assert len(calls) == 3
|
||||
(piece_a, timeout_a), (piece_b, _), (concat_args, timeout_b) = calls
|
||||
assert timeout_a == timeout_b == 60
|
||||
@@ -273,3 +275,154 @@ def test_clip_batch_bad_iso_raises_cliperror(tmp_path):
|
||||
with pytest.raises(ClipError):
|
||||
clip.clip_batch(BASE, 1, "bukan-tanggal", "2026-09-26T10:00:00",
|
||||
str(tmp_path / "out.mp4"))
|
||||
|
||||
|
||||
# ------------------------------------------------------- pad + OSD alignment
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _clear_osd_cache():
|
||||
clip._OSD_CACHE.clear()
|
||||
yield
|
||||
clip._OSD_CACHE.clear()
|
||||
|
||||
|
||||
def _one_movie(monkeypatch, start_iso, duration):
|
||||
movies = [{"path": CLIP_A, "start": dt("2026-09-26 09:00:00")}]
|
||||
patch_batch_env(monkeypatch, movies, duration=duration)
|
||||
|
||||
|
||||
def test_pad_extends_selection_window(monkeypatch, tmp_path):
|
||||
# clip 09:00:00 + 5 s ends exactly at batch start → pad 0 finds nothing
|
||||
movies = [{"path": CLIP_A, "start": dt("2026-09-26 09:00:00")}]
|
||||
patch_batch_env(monkeypatch, movies, duration=5.0)
|
||||
calls, captured = [], {}
|
||||
monkeypatch.setattr(clip, "_run_ffmpeg", fake_run_factory(calls, captured))
|
||||
with pytest.raises(ClipError, match="Tidak ada rekaman"):
|
||||
clip.clip_batch(BASE, 1, "2026-09-26T09:00:05", "2026-09-26T09:00:10",
|
||||
str(tmp_path / "a.mp4"), pad_seconds=0, align_osd=False)
|
||||
clip.clip_batch(BASE, 1, "2026-09-26T09:00:05", "2026-09-26T09:00:10",
|
||||
str(tmp_path / "b.mp4"), pad_seconds=3, align_osd=False)
|
||||
args = calls[0][0]
|
||||
assert args[args.index("-ss") + 1] == "2.000" # window_start 09:00:02
|
||||
assert args[args.index("-to") + 1] == "5.000" # clip ends 09:00:05
|
||||
|
||||
|
||||
def test_osd_alignment_corrects_start_offset(monkeypatch, tmp_path):
|
||||
_one_movie(monkeypatch, None, 600.0)
|
||||
calls, captured = [], {}
|
||||
monkeypatch.setattr(clip, "_run_ffmpeg", fake_run_factory(calls, captured))
|
||||
# batch 09:01:10→09:02:00 (pad 0): filename off0=70, off1=120.
|
||||
# OSD at media 70 reads desired+14 → shift to 56; at 56 reads desired.
|
||||
reads = {
|
||||
70: dt("2026-09-26 09:01:24"),
|
||||
56: dt("2026-09-26 09:01:10"),
|
||||
120: dt("2026-09-26 09:02:00"),
|
||||
}
|
||||
|
||||
def fake_osd(url, media_offset, timeout=45):
|
||||
return reads.get(int(round(media_offset)))
|
||||
|
||||
monkeypatch.setattr(clip, "osd_time_at", fake_osd)
|
||||
out = tmp_path / "out.mp4"
|
||||
clip.clip_batch(BASE, 1, "2026-09-26T09:01:10", "2026-09-26T09:02:00",
|
||||
str(out), pad_seconds=0, align_osd=True)
|
||||
args = calls[0][0]
|
||||
assert args[args.index("-ss") + 1] == "56.000"
|
||||
assert args[args.index("-to") + 1] == "120.000"
|
||||
|
||||
|
||||
def test_align_disabled_never_probes_osd(monkeypatch, tmp_path):
|
||||
_one_movie(monkeypatch, None, 600.0)
|
||||
calls, captured = [], {}
|
||||
monkeypatch.setattr(clip, "_run_ffmpeg", fake_run_factory(calls, captured))
|
||||
|
||||
def boom(*a, **k):
|
||||
raise AssertionError("osd_time_at must not be called")
|
||||
|
||||
monkeypatch.setattr(clip, "osd_time_at", boom)
|
||||
clip.clip_batch(BASE, 1, "2026-09-26T09:00:10", "2026-09-26T09:00:50",
|
||||
str(tmp_path / "o.mp4"), pad_seconds=0, align_osd=False)
|
||||
args = calls[0][0]
|
||||
assert args[args.index("-ss") + 1] == "10.000"
|
||||
|
||||
|
||||
def test_osd_failure_keeps_filename_offsets(monkeypatch, tmp_path):
|
||||
_one_movie(monkeypatch, None, 600.0)
|
||||
calls, captured = [], {}
|
||||
monkeypatch.setattr(clip, "_run_ffmpeg", fake_run_factory(calls, captured))
|
||||
monkeypatch.setattr(clip, "osd_time_at", lambda *a, **k: None)
|
||||
clip.clip_batch(BASE, 1, "2026-09-26T09:00:10", "2026-09-26T09:00:50",
|
||||
str(tmp_path / "o.mp4"), pad_seconds=0, align_osd=True)
|
||||
args = calls[0][0]
|
||||
assert args[args.index("-ss") + 1] == "10.000"
|
||||
assert args[args.index("-to") + 1] == "50.000"
|
||||
|
||||
|
||||
def test_osd_alignment_probe_iteration_cap(monkeypatch, tmp_path):
|
||||
_one_movie(monkeypatch, None, 600.0)
|
||||
calls, captured = [], {}
|
||||
monkeypatch.setattr(clip, "_run_ffmpeg", fake_run_factory(calls, captured))
|
||||
probes = []
|
||||
|
||||
def always_off(url, media_offset, timeout=45):
|
||||
probes.append(int(round(media_offset)))
|
||||
return dt("2026-09-26 09:01:24") # always +14 vs desired 09:01:10
|
||||
|
||||
monkeypatch.setattr(clip, "osd_time_at", always_off)
|
||||
clip.clip_batch(BASE, 1, "2026-09-26T09:01:10", "2026-09-26T09:02:00",
|
||||
str(tmp_path / "o.mp4"), pad_seconds=0, align_osd=True)
|
||||
# start align: at most 2 probes; end align: 2 more (same cap)
|
||||
assert len(probes) <= 4
|
||||
start_probes = [p for p in probes if p in (70, 56, 42)]
|
||||
assert len(start_probes) <= 2
|
||||
args = calls[0][0]
|
||||
# after two corrections 70 -> 56 -> 42
|
||||
assert args[args.index("-ss") + 1] == "42.000"
|
||||
|
||||
|
||||
def test_osd_date_mismatch_corrects_once_then_stops(monkeypatch, tmp_path):
|
||||
# batch spans New Year's Eve; OSD read lands on the wrong calendar day
|
||||
movies = [{"path": "/2026-12-31/23-00-00.mp4",
|
||||
"start": dt("2026-12-31 23:00:00")}]
|
||||
monkeypatch.setattr(clip, "list_movies", lambda *a, **k: list(movies))
|
||||
monkeypatch.setattr(clip, "probe_duration", lambda url, timeout=30: 7200.0)
|
||||
calls, captured = [], {}
|
||||
monkeypatch.setattr(clip, "_run_ffmpeg", fake_run_factory(calls, captured))
|
||||
probes = []
|
||||
|
||||
def wrong_day(url, media_offset, timeout=45):
|
||||
off = int(round(media_offset))
|
||||
probes.append(off)
|
||||
if off == 1924:
|
||||
return dt("2027-01-01 00:32:04") # +1 h vs desired Dec31 23:32:04
|
||||
if off == 0:
|
||||
return dt("2027-01-01 00:31:50") # still Jan 1 after correction
|
||||
return None # end-offset probes do nothing
|
||||
|
||||
monkeypatch.setattr(clip, "osd_time_at", wrong_day)
|
||||
clip.clip_batch(BASE, 1, "2026-12-31T23:32:04", "2026-12-31T23:35:00",
|
||||
str(tmp_path / "o.mp4"), pad_seconds=0, align_osd=True)
|
||||
# start align: probe at 1924 → corrects (clamped to 0), probe at 0 has
|
||||
# wrong date again → rejected by date check (2 probes max).
|
||||
assert probes[:2] == [1924, 0]
|
||||
args = calls[0][0]
|
||||
assert args[args.index("-ss") + 1] == "0.000"
|
||||
assert args[args.index("-to") + 1] == "2100.000" # end offset untouched
|
||||
|
||||
|
||||
def test_alignment_span_collapse_falls_back(monkeypatch, tmp_path):
|
||||
_one_movie(monkeypatch, None, 600.0)
|
||||
calls, captured = [], {}
|
||||
monkeypatch.setattr(clip, "_run_ffmpeg", fake_run_factory(calls, captured))
|
||||
|
||||
def collapse(url, media_offset, timeout=45):
|
||||
# readings that would push off1 before off0
|
||||
return dt("2026-09-26 09:02:59") # end desired 09:00:50 → huge shift
|
||||
|
||||
monkeypatch.setattr(clip, "osd_time_at", collapse)
|
||||
clip.clip_batch(BASE, 1, "2026-09-26T09:00:10", "2026-09-26T09:00:50",
|
||||
str(tmp_path / "o.mp4"), pad_seconds=0, align_osd=True)
|
||||
args = calls[0][0]
|
||||
# fallback keeps filename-based offsets
|
||||
assert args[args.index("-ss") + 1] == "10.000"
|
||||
assert args[args.index("-to") + 1] == "50.000"
|
||||
Reference in new issue
Block a user