refactor: delete the legacy session model
Two session models shared one process. `/v1/*` and `/v1/stream` are the live one; `/api/sessions*` and `/api/compare_live_vs_still` were the earlier model, still constructed on every `create_app()` through `build_legacy_scans` even though nothing called them. The web client and `stream_client.py` speak only `/v1`. Deleted: `observe.py` (ScanService/PerceptionService/PerceptionAdapter), `yolo_adapter.py` (imported only by the legacy builder), `pipeline.py` and `test_suite.py` (the old single-image CLI, reachable only from each other), `test_observe.py` (its Session* tests), the eight legacy route handlers, `build_legacy_scans` with its two facades, `create_fastapi_app`, and the `scans` parameter on `create_app`. `IdentityRankingTests` moved to `test_identity.py` before `test_observe.py` was deleted: those five ranking rules are the live path's contract, not the legacy model's. `test_api.py` keeps every `/v1` and WebSocket test. Verified: 246 tests OK (2 skipped), and a route dump of a freshly built app shows no `/api/*` path with all nine `/v1` routes and the WebSocket intact.
This commit is contained in:
parent
004212a8cf
commit
eca03e0c91
10 changed files with 54 additions and 1348 deletions
|
|
@ -250,39 +250,6 @@ def get_available_shelf_images(add_dir: str = "add") -> List[Dict[str, str]]:
|
|||
return items
|
||||
|
||||
|
||||
def build_legacy_scans(engine: RecognitionEngine):
|
||||
"""The /api/sessions* routes kept from the earlier session model, now backed
|
||||
by the real engine instead of the fake adapter."""
|
||||
from observe import PerceptionService, ScanService
|
||||
from yolo_adapter import YoloMatcherAdapter
|
||||
|
||||
class _DetectorFacade:
|
||||
def __init__(self, engine: RecognitionEngine):
|
||||
self.engine = engine
|
||||
self.weights_path = engine.settings.weights_path
|
||||
|
||||
def detect(self, image, conf_threshold: float = 0.25, iou_threshold: float = 0.45):
|
||||
boxes = self.engine.detect(image, conf=conf_threshold)
|
||||
return {"status": "success", "boxes": boxes, "total_detected": len(boxes)}
|
||||
|
||||
def detect_empty_slots(self, image_shape, boxes, min_gap_ratio: float = 0.75):
|
||||
return self.engine.empty_slots(image_shape, boxes)
|
||||
|
||||
def crop_roi(self, image, box):
|
||||
return self.engine.crop(image, box)
|
||||
|
||||
class _MatcherFacade:
|
||||
def __init__(self, engine: RecognitionEngine):
|
||||
self.engine = engine
|
||||
|
||||
def classify_crop(self, crop, method: str = "hybrid"):
|
||||
scores = self.engine.score_crop(crop, method=method)
|
||||
from identity import decide_identity
|
||||
|
||||
return decide_identity(scores, method=method)
|
||||
|
||||
adapter = YoloMatcherAdapter(_DetectorFacade(engine), _MatcherFacade(engine), method="hybrid")
|
||||
return ScanService(PerceptionService(adapter))
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
|
|
@ -295,7 +262,6 @@ def create_app(
|
|||
settings: Optional[ServerSettings] = None,
|
||||
*,
|
||||
web_dir: Optional[str] = None,
|
||||
scans: Any = None,
|
||||
) -> FastAPI:
|
||||
settings = settings or server_settings_from_env()
|
||||
@asynccontextmanager
|
||||
|
|
@ -323,7 +289,6 @@ def create_app(
|
|||
allow_headers=["Authorization", "Content-Type"],
|
||||
)
|
||||
|
||||
scans = scans if scans is not None else build_legacy_scans(engine)
|
||||
# A live session can be recorded so a real scan becomes data to label later.
|
||||
recorder = FrameRecorder(
|
||||
settings.record_frames,
|
||||
|
|
@ -994,127 +959,6 @@ def create_app(
|
|||
async with sessions_lock:
|
||||
sessions.pop(session_id, None)
|
||||
|
||||
# ---------------------------------------------------------------- legacy session routes
|
||||
@app.post("/api/sessions", dependencies=[Depends(authorize)])
|
||||
async def legacy_start_session(payload: Dict[str, Any] = None) -> Dict[str, Any]:
|
||||
payload = payload or {}
|
||||
session = scans.start_session(
|
||||
zone_id=str(payload.get("zone_id") or "zone-1"),
|
||||
catalog_version=str(payload.get("catalog_version") or "stock-v1"),
|
||||
capture_kind=str(payload.get("capture_kind") or "sampled"),
|
||||
)
|
||||
return {"status": "success", "session": session.snapshot()}
|
||||
|
||||
@app.post("/api/sessions/{session_id}/frames", dependencies=[Depends(authorize)])
|
||||
async def legacy_observe_frame(session_id: str, payload: Dict[str, Any] = None) -> Dict[str, Any]:
|
||||
from observe import FrameRequest, ObserveError
|
||||
|
||||
payload = payload or {}
|
||||
try:
|
||||
session = scans.get_session(session_id)
|
||||
raw = payload.get("frame_bytes_b64") or payload.get("image_base64") or ""
|
||||
if not isinstance(raw, str):
|
||||
raw = ""
|
||||
if "," in raw:
|
||||
raw = raw.split(",", 1)[1]
|
||||
# Base64 expands ~4/3. Reject the text before decoding it.
|
||||
if len(raw) > (MAX_FRAME_BYTES * 4) // 3 + 8:
|
||||
raise _error("PAYLOAD_TOO_LARGE", f"frame larger than {MAX_FRAME_BYTES} bytes", 413)
|
||||
loop = asyncio.get_running_loop()
|
||||
try:
|
||||
frame_bytes = (
|
||||
await loop.run_in_executor(None, lambda: base64.b64decode(raw, validate=True))
|
||||
if raw else b""
|
||||
)
|
||||
except (ValueError, base64.binascii.Error):
|
||||
raise _error("BAD_REQUEST", "frame_bytes_b64 is not valid base64", 400)
|
||||
if len(frame_bytes) > MAX_FRAME_BYTES:
|
||||
raise _error("PAYLOAD_TOO_LARGE", f"frame larger than {MAX_FRAME_BYTES} bytes", 413)
|
||||
request = FrameRequest(
|
||||
frame_id=str(payload.get("frame_id") or ""),
|
||||
frame_bytes=frame_bytes,
|
||||
sequence=int(payload.get("sequence", 0)),
|
||||
captured_at=float(payload.get("captured_at") or 0),
|
||||
session_id=session.session_id,
|
||||
zone_id=session.zone_id,
|
||||
catalog_version=session.catalog_version,
|
||||
capture_kind=str(payload.get("capture_kind") or session.capture_kind),
|
||||
width=payload.get("width"),
|
||||
height=payload.get("height"),
|
||||
)
|
||||
if not request.frame_id:
|
||||
raise _error("BAD_REQUEST", "frame_id required", 400)
|
||||
observation = await loop.run_in_executor(None, scans.observe_frame, request)
|
||||
return {"status": "success", "observation": observation.as_dict(), "session": session.snapshot()}
|
||||
except ObserveError as exc:
|
||||
raise _error("CONFLICT", str(exc), 409)
|
||||
|
||||
@app.get("/api/sessions/{session_id}", dependencies=[Depends(authorize)])
|
||||
async def legacy_get_session(session_id: str) -> Dict[str, Any]:
|
||||
from observe import ObserveError
|
||||
|
||||
try:
|
||||
return {"status": "success", "session": scans.get_session(session_id).snapshot()}
|
||||
except ObserveError as exc:
|
||||
raise _error("NOT_FOUND", str(exc), 404)
|
||||
|
||||
@app.post("/api/sessions/{session_id}/review", dependencies=[Depends(authorize)])
|
||||
async def legacy_review(session_id: str, payload: Dict[str, Any] = None) -> Dict[str, Any]:
|
||||
from observe import ObserveError
|
||||
|
||||
payload = payload or {}
|
||||
|
||||
try:
|
||||
snapshot = scans.review(
|
||||
session_id,
|
||||
actor=str(payload.get("actor") or "operator"),
|
||||
reason=str(payload.get("reason") or "manual-review"),
|
||||
note=payload.get("note"),
|
||||
)
|
||||
return {"status": "success", "session": snapshot}
|
||||
except ObserveError as exc:
|
||||
raise _error("CONFLICT", str(exc), 409)
|
||||
|
||||
@app.post("/api/sessions/{session_id}/finalize", dependencies=[Depends(authorize)])
|
||||
async def legacy_finalize(session_id: str) -> Dict[str, Any]:
|
||||
from observe import ObserveError
|
||||
|
||||
try:
|
||||
return {"status": "success", "session": scans.finalize(session_id)}
|
||||
except ObserveError as exc:
|
||||
raise _error("CONFLICT", str(exc), 409)
|
||||
|
||||
@app.post("/api/sessions/{session_id}/cancel", dependencies=[Depends(authorize)])
|
||||
async def legacy_cancel(session_id: str) -> Dict[str, Any]:
|
||||
from observe import ObserveError
|
||||
|
||||
try:
|
||||
return {"status": "success", "session": scans.cancel(session_id)}
|
||||
except ObserveError as exc:
|
||||
raise _error("CONFLICT", str(exc), 409)
|
||||
|
||||
@app.post("/api/sessions/{session_id}/rescan", dependencies=[Depends(authorize)])
|
||||
async def legacy_rescan(session_id: str) -> Dict[str, Any]:
|
||||
from observe import ObserveError
|
||||
|
||||
try:
|
||||
return {"status": "success", "session": scans.rescan(session_id).snapshot()}
|
||||
except ObserveError as exc:
|
||||
raise _error("CONFLICT", str(exc), 409)
|
||||
|
||||
@app.post("/api/compare_live_vs_still", dependencies=[Depends(authorize)])
|
||||
async def legacy_compare(payload: Dict[str, Any] = None) -> Dict[str, Any]:
|
||||
from observe import ObserveError
|
||||
|
||||
payload = payload or {}
|
||||
try:
|
||||
comparison = scans.compare_live_vs_still(
|
||||
str(payload.get("sampled_session_id")), str(payload.get("still_session_id"))
|
||||
)
|
||||
return {"status": "success", "comparison": comparison}
|
||||
except ObserveError as exc:
|
||||
raise _error("CONFLICT", str(exc), 409)
|
||||
|
||||
# ---------------------------------------------------------------- errors
|
||||
@app.exception_handler(RequestValidationError)
|
||||
async def validation_exception_handler(request: Request, exc: RequestValidationError) -> JSONResponse:
|
||||
|
|
@ -1148,25 +992,12 @@ def create_app(
|
|||
if os.path.isdir(resolved_web_dir):
|
||||
app.mount("/", StaticFiles(directory=resolved_web_dir, html=True), name="web")
|
||||
|
||||
|
||||
|
||||
return app
|
||||
|
||||
|
||||
def create_fastapi_app(
|
||||
engine: RecognitionEngine,
|
||||
scans: Any = None,
|
||||
settings: Optional[ServerSettings] = None,
|
||||
) -> FastAPI:
|
||||
"""Back-compat entry point; the server entry point is create_app()."""
|
||||
return create_app(engine, settings, scans=scans)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# entry point
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description="Dorayaki - shelf recognition server")
|
||||
parser.add_argument("--host", type=str, default=None)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue