A small internal web app for uploading videos, uploading matching JSON person-tracking annotations, and browsing coverage stats/filters across the library.
Storage is an S3-compatible object store (MinIO). The bucket is the database: the
app browses the existing processed/<device>/<clip>/ clips (each with a
*_detections_visualized.mp4, a metadata.json, and — where present — a matching
person/station annotation JSON) and lets you upload new videos into an app-uploads/
prefix. Per-video technical metadata (duration/resolution/fps) is probed lazily from the
object and cached to a small _appmeta.json sidecar so large videos aren't re-probed on
every page load. Videos are streamed to the browser through the app (with HTTP Range
support for seeking) — clients never talk to the object store directly.
pip install -r requirements.txtpython app.pyThis starts the server on 0.0.0.0:5000, so it's reachable from other machines on the
same network at http://<this-machine's-hostname-or-IP>:5000/ — e.g.
http://hu-rohadutt-hyd:5000/. Share that URL with the USA colleague uploading videos.
The Flask dev server is fine for a couple of internal users. If you want a more robust
server (e.g. it should survive terminal closing, or handle concurrent large uploads more
gracefully), run it via waitress instead:
waitress-serve --host=0.0.0.0 --port=5000 app:appThe app talks to the object store over its S3 API. All connection settings are loaded
from a .env file in the project root (via python-dotenv). Copy the template and fill
in the credentials before first run:
cp .env.example .env
# then edit .env and set VIDEO_S3_SECRET (and any other values)| Variable | Example | Notes |
|---|---|---|
VIDEO_S3_ENDPOINT |
http://<minio-host>:80 |
The S3 API endpoint (required). Use the S3 API port (e.g. 80), not the Console port. |
VIDEO_S3_KEY |
(required) | Access key. |
VIDEO_S3_SECRET |
(required) | Secret key. |
VIDEO_S3_BUCKET |
(required) | Bucket name. |
VIDEO_S3_REGION |
us-east-1 |
Region (optional; any value works for MinIO). |
VIDEO_S3_PROCESSED_PREFIX |
processed/ |
Existing clips browsed by the app (optional). |
VIDEO_S3_UPLOAD_PREFIX |
app-uploads/ |
Where new uploads are stored (optional). |
The four connection settings (ENDPOINT, KEY, SECRET, BUCKET) have no defaults in
the code — the app refuses to start if any are missing. .env holds them and is
gitignored — never commit it. .env.example (with values redacted) is the committed
template. Real OS environment variables, if set, take precedence over .env, so you can
still override per-run:
VIDEO_S3_ENDPOINT="http://localhost:9000" VIDEO_S3_BUCKET="test" python app.pyMinIO Console vs S3 API. MinIO serves a browser Console (a
:9001-style port) that is separate from its S3 API port. The app uses the S3 API only — pointVIDEO_S3_ENDPOINTat the API port, not the Console.
- A clip's detail page lists every file in that clip's folder (the video, tracklets
0.zip,metadata.json, annotation JSON, ...) with its size and a download link. Video files are click-to-play — clicking one loads it into an inline player with seeking; nothing is streamed until you click. - Uploading a video creates a new prefix under
app-uploads/, named after the (sanitized) video filename, and streams the file into the bucket. If that name already exists, a numeric suffix is appended (_2,_3, ...). - Uploading a JSON annotation auto-matches to a clip by filename: a file named
clip_01.jsonis matched to the clip whose video isclip_01.mp4. If no matching clip is found, the upload is rejected with an error (and the closest candidates are suggested) rather than silently stored in the wrong place. - From a clip's detail page, you can also upload/replace its JSON directly — that upload always targets that specific clip, bypassing filename matching.
- Building an annotation in-app. Instead of uploading a file, the clip detail page has a
🛠️ Build annotation in-app button that opens an interactive builder (persons →
station HEAD/MIDDLE/TAIL → start-time rows). Clicking Save to this clip writes the
generated JSON straight to that clip — no file handling. The builder auto-saves your
in-progress work to the browser's
localStorage(scoped per clip), so closing/reopening restores your draft; the draft is cleared once you save successfully. It's also available as a standalone page at/builder(generate + download only, no clip target).
{
"persons": [
{
"uuid": "PERSON",
"station": "HEAD",
"start_times": [{"id": "50002", "t": "00:00:00"}]
}
]
}station is one of HEAD / MIDDLE / TAIL. Stats shown per video: total persons,
total detections, detections per station (with any station at zero detections
highlighted), and the fraction of the video's duration spanned by the earliest-to-latest
annotated timestamp ("time coverage").