Skip to content

feat(api): serve /predict frames from the local filesystem#49

Merged
Chouffe merged 16 commits into
mainfrom
arthur/feat-api-local-frames
Jun 12, 2026
Merged

feat(api): serve /predict frames from the local filesystem#49
Chouffe merged 16 commits into
mainfrom
arthur/feat-api-local-frames

Conversation

@Chouffe

@Chouffe Chouffe commented Jun 11, 2026

Copy link
Copy Markdown
Collaborator

Summary

Lets /predict serve frames from a local shared volume (edge deployments) instead of S3, per the design in docs/specs/2026-06-11-api-local-frames-design.md.

  • New optional source: "s3" | "local" field on PredictRequest, defaulting to the new TEMPORAL_API_FRAME_SOURCE setting (default s3) — zero behavior change for existing deployments and callers.
  • In local mode, frames are relative paths resolved under the settings-only TEMPORAL_API_FRAMES_ROOT with a symlink-aware containment check; the root is never request-suppliable by design.
  • Local frames are read in place — no temp dir, no copy, the whole fetch stage drops out of the latency budget (profiled as local_resolve instead of s3_fetch; resolution runs off the event loop like the S3 fetch).
  • FRAME_SOURCE=local without FRAMES_ROOT fails at startup; a root that is not a directory at request time is a distinct 400 so misconfiguration is never masked as missing frames.
  • Response schema untouched; runner, ROI handling, and detection cache unchanged.

Request formats

S3 (unchanged — today's alert-api requests behave byte-for-byte identically):

POST /predict
{
  "frames": ["cam12/2026-06-11/frame_0001.jpg", "cam12/2026-06-11/frame_0002.jpg"],
  "bucket": "2eb7ac42fbbf-alert-api-2"
}

bucket is optional and falls back to TEMPORAL_API_S3_BUCKET, as before. An explicit "source": "s3" is equivalent to omitting it on an s3-default server.

Local (edge box configured with TEMPORAL_API_FRAME_SOURCE=local and TEMPORAL_API_FRAMES_ROOT=/data/frames):

POST /predict
{
  "frames": ["cam12/2026-06-11/frame_0001.jpg", "cam12/2026-06-11/frame_0002.jpg"],
  "source": "local"
}

Each frame resolves to /data/frames/<frame> and is read in place. source may be omitted when the server's default is already local (the expected edge setup). roi_xyxyn and ?verbose=true work identically for both sources.

Error contract (local additions):

Request / state Response
"source": "local" + "bucket": "..." 400 invalid_request (bucket is not valid with local frames)
"source": "local" override on a server without FRAMES_ROOT 400 invalid_request (a local-default server without a root refuses to start)
FRAMES_ROOT set but not a directory (typo, unmounted volume) 400 invalid_request — distinct from per-frame 404
frame empty, ., absolute, .., or escapes the root via symlink 400 invalid_request
frame file missing 404 frame_not_found (same as a missing S3 key)

Producer invariants for local mode (documented in api/README.md): publish frames atomically (write + rename — frames are read in place), and keep frame basename stems globally unique when the detection cache is enabled (the same invariant S3 keys already carry).

Test Plan

  • make -C api test — 147 passed, 1 skipped (115 pre-existing tests pass unmodified — back-compat proof; 32 new tests cover the resolver, settings, schema, and route)
  • make -C api lint / make -C api format — clean
  • Traversal cases covered: absolute path, .. (escaping and non-escaping), symlink escaping the root, empty/. frames
  • Reviewed at high effort (7-angle multi-agent review); all confirmed findings fixed on this branch

@Chouffe Chouffe requested a review from MateoLostanlen June 11, 2026 16:44
…-frames

# Conflicts:
#	api/README.md
#	api/tests/test_app.py
@Chouffe Chouffe merged commit 07a7bac into main Jun 12, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants