""" Manual smoke test for the intake pipeline using real book cover images composited onto a synthetic platform, in place of real camera hardware. Why: no camera is on hand yet (see AGENTS.md hardware section). Unit tests use solid-color synthetic frames, which never exercise crop_to_book's real contour detection against actual image content. This script fetches a handful of public book cover images from the Open Library covers API, composites each onto a synthetic "platform" background at small/medium/ large footprint sizes, and drives the real IntakeSession/Catalogue/ CaptureStrategy pipeline end to end -- the same modules run.py wires up, just with synthetic frames standing in for CameraSource. Not part of the pytest suite: it fetches over the network and its point is human-visible verification (inspect the saved crops), not CI assertion. Usage: .venv/bin/python scripts/synthetic_platform_smoke_test.py """ import shutil import sys import urllib.request from pathlib import Path import cv2 import numpy as np sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) from bookmark.capture_strategies.manual_button import ManualButtonStrategy from bookmark.catalogue import Catalogue from bookmark.session import IntakeSession from bookmark.sizing import MarkerRegion PLATFORM_SIZE = (800, 600) # width, height PLATFORM_COLOR = (210, 205, 195) # BGR, light neutral background CENTER = (PLATFORM_SIZE[0] // 2, PLATFORM_SIZE[1] // 2) # Marker regions sit along the horizontal centerline at the edge of each # size category's footprint. A book covers a marker once its half-width # reaches that marker's offset from center. MARKERS = [ MarkerRegion(name="small_boundary", bucket="small", x=CENTER[0] + 65, y=CENTER[1] - 10, width=20, height=20), MarkerRegion(name="medium_boundary", bucket="medium", x=CENTER[0] + 115, y=CENTER[1] - 10, width=20, height=20), MarkerRegion(name="large_boundary", bucket="large", x=CENTER[0] + 190, y=CENTER[1] - 10, width=20, height=20), ] # (half_width, half_height) footprints, chosen to straddle each marker boundary. FOOTPRINTS = { "small": (90, 130), "medium": (140, 190), "large": (220, 280), } COVERS = { "small": "https://covers.openlibrary.org/b/isbn/9780141439518-L.jpg", # Pride and Prejudice "medium": "https://covers.openlibrary.org/b/isbn/9780439708180-L.jpg", # Harry Potter "large": "https://covers.openlibrary.org/b/isbn/9780062315007-L.jpg", # The Alchemist } def make_reference_frame() -> np.ndarray: frame = np.full((PLATFORM_SIZE[1], PLATFORM_SIZE[0], 3), PLATFORM_COLOR, dtype=np.uint8) return frame def fetch_cover(url: str, dest: Path) -> np.ndarray: req = urllib.request.Request(url, headers={"User-Agent": "bookmark-smoke-test/0.1"}) with urllib.request.urlopen(req, timeout=15) as response: dest.write_bytes(response.read()) image = cv2.imread(str(dest)) if image is None: raise RuntimeError(f"Could not decode downloaded cover from {url}") return image def composite_book(reference: np.ndarray, cover: np.ndarray, half_width: int, half_height: int) -> np.ndarray: frame = reference.copy() resized_cover = cv2.resize(cover, (half_width * 2, half_height * 2)) x0 = CENTER[0] - half_width y0 = CENTER[1] - half_height frame[y0:y0 + half_height * 2, x0:x0 + half_width * 2] = resized_cover return frame def main() -> None: work_dir = Path(__file__).resolve().parent.parent / ".smoke_test_run" if work_dir.exists(): shutil.rmtree(work_dir) work_dir.mkdir() cache_dir = work_dir / "downloaded_covers" cache_dir.mkdir() reference = make_reference_frame() catalogue = Catalogue(work_dir / "catalogue.csv") strategy = ManualButtonStrategy() session = IntakeSession( catalogue=catalogue, strategy=strategy, reference_frame=reference, markers=MARKERS, captures_dir=work_dir / "captures", ) results = [] for expected_bucket, url in COVERS.items(): print(f"Fetching {expected_bucket} cover from {url} ...") cover_path = cache_dir / f"{expected_bucket}.jpg" cover = fetch_cover(url, cover_path) half_w, half_h = FOOTPRINTS[expected_bucket] front_frame = composite_book(reference, cover, half_w, half_h) back_frame = composite_book(reference, cover, half_w, half_h) # same cover, stands in for a back cover strategy.trigger() fired = session.observe_frame(front_frame) assert fired, f"front capture did not fire for {expected_bucket}" book_id = session.current_book_id strategy.trigger() fired = session.observe_frame(back_frame) assert fired, f"back capture did not fire for {expected_bucket}" session.start_next_book() row = next(r for r in catalogue.read_all() if r["book_id"] == book_id) actual_bucket = row["size_bucket"] status = "PASS" if actual_bucket == expected_bucket else "FAIL" results.append((expected_bucket, actual_bucket, status, book_id)) print(f" book_id={book_id} expected={expected_bucket} got={actual_bucket} [{status}]") print(f" front crop: {row['front_cropped_path']}") print(f" back crop: {row['back_cropped_path']}") print() print("=" * 60) all_pass = all(r[2] == "PASS" for r in results) for expected, actual, status, book_id in results: print(f"{status}: expected={expected:8s} got={actual:8s} book_id={book_id}") print("=" * 60) print(f"Artifacts written to: {work_dir}") print("Overall:", "ALL PASS" if all_pass else "SOME FAILED") if not all_pass: sys.exit(1) if __name__ == "__main__": main()