fix(obico): POST image bytes directly to ML API instead of callback URL (#1003)

The ML API previously called back into Bambuddy to fetch snapshots,
  which failed behind reverse proxies with external auth (Authelia, etc.).
  Now the detection loop captures the JPEG locally and POSTs it directly
  as multipart form data — no callback URL, no nonce cache, no
  external_url dependency.
This commit is contained in:
maziggy
2026-04-17 09:06:31 +02:00
parent c6aba21278
commit 475e34ebda
20 changed files with 62 additions and 313 deletions
+2 -24
View File
@@ -2,13 +2,13 @@
import logging
from fastapi import APIRouter, HTTPException, Response
from fastapi import APIRouter
from pydantic import BaseModel
from backend.app.core.auth import RequirePermissionIfAuthEnabled
from backend.app.core.permissions import Permission
from backend.app.models.user import User
from backend.app.services.obico_detection import obico_detection_service, pop_frame
from backend.app.services.obico_detection import obico_detection_service
logger = logging.getLogger(__name__)
@@ -33,7 +33,6 @@ async def get_status(
"sensitivity": settings["sensitivity"],
"action": settings["action"],
"poll_interval": settings["poll_interval"],
"external_url_configured": bool(settings["external_url"]),
}
@@ -46,24 +45,3 @@ async def test_connection(
if not req.url:
return {"ok": False, "status_code": None, "body": None, "error": "URL is empty"}
return await obico_detection_service.test_connection(req.url)
@router.get("/cached-frame/{nonce}")
async def cached_frame(nonce: str):
"""Serve a pre-captured JPEG to the Obico ML API.
The detection loop captures a snapshot locally (where we control the timeout),
stashes the bytes under a one-shot random nonce, then hands this URL to Obico's
ML API. Obico's hardcoded 5s read timeout never races our snapshot pipeline.
Unauthenticated: the unguessable 32-byte nonce is single-use and expires in
seconds, so exposing this path doesn't widen the camera access surface.
"""
data = await pop_frame(nonce)
if data is None:
raise HTTPException(status_code=404, detail="Frame not found or expired")
return Response(
content=data,
media_type="image/jpeg",
headers={"Cache-Control": "no-store"},
)