From b707aef137153a86fd4d34da2910d98a865b3f0f Mon Sep 17 00:00:00 2001 From: pyr0ball Date: Mon, 13 Jul 2026 10:28:10 -0700 Subject: [PATCH] feat: add capture strategy interface and manual button strategy --- bookmark/capture_strategies/base.py | 15 ++++++++++++ bookmark/capture_strategies/manual_button.py | 22 +++++++++++++++++ tests/test_manual_button_strategy.py | 25 ++++++++++++++++++++ 3 files changed, 62 insertions(+) create mode 100644 bookmark/capture_strategies/base.py create mode 100644 bookmark/capture_strategies/manual_button.py create mode 100644 tests/test_manual_button_strategy.py diff --git a/bookmark/capture_strategies/base.py b/bookmark/capture_strategies/base.py new file mode 100644 index 0000000..87d690a --- /dev/null +++ b/bookmark/capture_strategies/base.py @@ -0,0 +1,15 @@ +from abc import ABC, abstractmethod + +import numpy as np + + +class CaptureStrategy(ABC): + """Watches a stream of frames and decides when to fire a capture.""" + + @abstractmethod + def reset(self) -> None: + """Clear any internal state, e.g. after a capture fires.""" + + @abstractmethod + def observe(self, frame: np.ndarray) -> bool: + """Feed the latest frame. Returns True exactly once per capture.""" diff --git a/bookmark/capture_strategies/manual_button.py b/bookmark/capture_strategies/manual_button.py new file mode 100644 index 0000000..007ea79 --- /dev/null +++ b/bookmark/capture_strategies/manual_button.py @@ -0,0 +1,22 @@ +import numpy as np + +from .base import CaptureStrategy + + +class ManualButtonStrategy(CaptureStrategy): + """Fires only when trigger() has been called since the last observe().""" + + def __init__(self) -> None: + self._armed = False + + def trigger(self) -> None: + self._armed = True + + def reset(self) -> None: + self._armed = False + + def observe(self, frame: np.ndarray) -> bool: + if self._armed: + self._armed = False + return True + return False diff --git a/tests/test_manual_button_strategy.py b/tests/test_manual_button_strategy.py new file mode 100644 index 0000000..e283eea --- /dev/null +++ b/tests/test_manual_button_strategy.py @@ -0,0 +1,25 @@ +import numpy as np + +from bookmark.capture_strategies.manual_button import ManualButtonStrategy + + +def test_does_not_fire_without_trigger(): + strategy = ManualButtonStrategy() + frame = np.zeros((10, 10, 3), dtype=np.uint8) + assert strategy.observe(frame) is False + + +def test_fires_once_after_trigger(): + strategy = ManualButtonStrategy() + frame = np.zeros((10, 10, 3), dtype=np.uint8) + strategy.trigger() + assert strategy.observe(frame) is True + assert strategy.observe(frame) is False + + +def test_reset_clears_armed_state(): + strategy = ManualButtonStrategy() + frame = np.zeros((10, 10, 3), dtype=np.uint8) + strategy.trigger() + strategy.reset() + assert strategy.observe(frame) is False