From df0af86f619103dccd742e045ad1c96428a61dd5 Mon Sep 17 00:00:00 2001 From: John Menke Date: Sun, 1 Nov 2026 14:30:36 -0500 Subject: [PATCH] Refuse a non-positive OCR sample interval. A zero or negative sample_interval_sec is a valid YAML number and makes the frame loop run forever. The scan now fails before that loop. Co-authored-by: Cursor --- src/docgen/ocr.py | 19 ++++++++++++++++++- tests/test_ocr.py | 18 ++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/src/docgen/ocr.py b/src/docgen/ocr.py index 726f068..94962a9 100644 --- a/src/docgen/ocr.py +++ b/src/docgen/ocr.py @@ -40,6 +40,21 @@ def _require_positive_fps(cap: Any, raw: float) -> float: return fps +def _require_positive_sample_interval(raw: object) -> float: + """A zero or negative interval makes the sample loop run forever.""" + try: + interval = float(raw) # type: ignore[arg-type] + except (TypeError, ValueError) as exc: + raise ValueError( + "OCR sample interval must be a positive finite number of seconds" + ) from exc + if not math.isfinite(interval) or interval <= 0: + raise ValueError( + "OCR sample interval must be a positive finite number of seconds" + ) + return interval + + class OCRScanner: def __init__(self, config: Config) -> None: self.config = config @@ -59,9 +74,11 @@ def scan_video(self, video_path: str | Path) -> OCRReport: return report fps = _require_positive_fps(cap, cap.get(cv2.CAP_PROP_FPS)) + interval = _require_positive_sample_interval( + self.ocr_cfg.get("sample_interval_sec", 2) + ) total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) duration = total_frames / fps if fps > 0 else 0 - interval = self.ocr_cfg.get("sample_interval_sec", 2) error_patterns = self.ocr_cfg.get("error_patterns", []) min_conf = self.ocr_cfg.get("min_confidence", 40) diff --git a/tests/test_ocr.py b/tests/test_ocr.py index 85076f4..2be9435 100644 --- a/tests/test_ocr.py +++ b/tests/test_ocr.py @@ -52,6 +52,24 @@ def test_zero_fps_is_refused(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> assert cap.sampled is False +def test_zero_sample_interval_is_refused( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + raw = { + "segments": {"all": ["01"], "default": ["01"]}, + "validation": {"ocr": {"sample_interval_sec": 0}}, + } + path = tmp_path / "docgen.yaml" + path.write_text(yaml.dump(raw), encoding="utf-8") + cap = _Cap() + cap.fps = 30.0 + cap.frames = 0.0 + monkeypatch.setattr(cv2, "VideoCapture", lambda *_a, **_k: cap) + with pytest.raises(ValueError, match="sample interval"): + OCRScanner(Config.from_yaml(path)).scan_video(tmp_path / "clip.mp4") + assert cap.sampled is False + + def test_positive_fps_still_samples(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: cap = _Cap() cap.fps = 30.0