- IntakeSession.observe_frame now catches ValueError from crop_to_book (empty-platform capture), cleans up any orphan wide-image file/dir, resets the strategy, and does not advance session state. The error is surfaced via a new last_error field. - /api/observe adds an "error" field to the response when a capture fires but fails, instead of an unhandled 500. - observe_frame only reports fired=True (and only runs capture logic) when state is WAITING_FRONT or WAITING_BACK, so a completed book left on the platform can no longer produce a no-op "fired" capture from an auto-strategy re-triggering. - observe_frame now holds a per-session threading.Lock for its whole body, since the frontend polls /api/observe every ~500ms without chaining requests and Catalogue.update_row() does a non-atomic read-all-then-rewrite-whole-file rewrite of catalogue.csv. Adds tests for empty-platform front/back captures and the BOOK_COMPLETE no-op-fire case, plus an API-level empty-platform test.
54 lines
1.8 KiB
Python
54 lines
1.8 KiB
Python
import cv2
|
|
from fastapi import APIRouter, Request, Response
|
|
from fastapi.responses import JSONResponse
|
|
|
|
router = APIRouter()
|
|
|
|
|
|
@router.get("/api/session-state")
|
|
def get_session_state(request: Request):
|
|
session = request.app.state.session
|
|
return {"state": session.state.value, "book_id": session.current_book_id}
|
|
|
|
|
|
@router.post("/api/observe")
|
|
def observe(request: Request):
|
|
session = request.app.state.session
|
|
camera = request.app.state.camera
|
|
try:
|
|
frame = camera.read_frame()
|
|
except RuntimeError as exc:
|
|
return JSONResponse(status_code=503, content={"error": "Camera unavailable", "detail": str(exc)})
|
|
request.app.state.latest_frame = frame
|
|
fired = session.observe_frame(frame)
|
|
response = {"fired": fired, "state": session.state.value, "book_id": session.current_book_id}
|
|
if session.last_error:
|
|
response["error"] = session.last_error
|
|
return response
|
|
|
|
|
|
@router.post("/api/manual-trigger")
|
|
def manual_trigger(request: Request):
|
|
session = request.app.state.session
|
|
if not hasattr(session.strategy, "trigger"):
|
|
return Response(status_code=400, content="Active strategy does not support manual trigger")
|
|
session.strategy.trigger()
|
|
return {"ok": True}
|
|
|
|
|
|
@router.post("/api/next-book")
|
|
def next_book(request: Request):
|
|
session = request.app.state.session
|
|
session.start_next_book()
|
|
return {"state": session.state.value}
|
|
|
|
|
|
@router.get("/api/preview.jpg")
|
|
def preview(request: Request):
|
|
frame = request.app.state.latest_frame
|
|
if frame is None:
|
|
return Response(status_code=404, content="No frame captured yet")
|
|
ok, buffer = cv2.imencode(".jpg", frame)
|
|
if not ok:
|
|
return Response(status_code=500, content="Failed to encode preview frame")
|
|
return Response(content=buffer.tobytes(), media_type="image/jpeg")
|