Example: YOLO video demo
A demo app that runs YOLO detection on a video, connects to Central, sends inference results, collects frames and swaps in a trained model.
Required permission: SettingsADMIN
mlops-edge-demo runs YOLO object detection on a video and reports the results to Central. Instead of running the agent (geo-mlops-edge) as a separate service, it embeds the SDK runtime (EdgeRuntime) inside the application. The source is public: https://github.com/osom8979/mlops-edge.
How it fits together
| The demo code | The SDK runtime |
|---|---|
| Reads the video, runs YOLO, draws the results in a window | Registration, heartbeats, command long-polling, local API (8600) |
| Queues an inference result every second | Local queue, upload when online, buffering while offline |
Encodes the frames to collect as JPEG and hands them to the frames collector | LabelMe pre-labels, de-duplication, chunked upload |
| Swaps the playing model when a model is activated | Handles pull_model (download, cache, activate) |
What travels between the device and Central:
| Direction | What | Where to see it on Central |
|---|---|---|
| Device → Central | Registration, heartbeat (10 s) | Edge Fleet (Edge Fleet) list and device detail |
| Device → Central | One inference result per second | Device detail, Inference results (추론 결과) tab |
| Device → Central | One JPEG + one LabelMe JSON every 30 frames | Files (파일) tab of the dataset you name; Collected data (수집 데이터) if you name none |
| Central → Device | Commands ping, restart, resync, set_policy, pull_model | Device detail, Commands (명령) tab |
Prerequisites
- uv. uv installs Python 3.12 to match the repository's
.python-version. - A display for the window. On a device without one, run with
--no-show. - (Optional) An NVIDIA GPU. Without one, inference runs on the CPU.
- Outbound HTTPS from the device to the Central API address, and a tenant ADMIN account to register the device ( SettingsADMIN).
git clone https://github.com/osom8979/mlops-edge.git
cd mlops-edge
uv sync
On Linux this also installs a CUDA build of PyTorch and the GPU libraries (about 3 GB in total). If a slow line gives Failed to download distribution due to network timeout, run UV_HTTP_TIMEOUT=600 uv sync.
1. Create the device and the dataset
- In Edge Fleet (Edge Fleet), use Register device (디바이스 등록) and enter a device ID (for example
edge-yolo-01). Leave the scopes at their default (all on). Inference results needinference:write, frame uploads needdata:write, and model swaps needmodels:read. See 5-minute start for the screens. - Copy the token from the Registered (등록 완료) window. You cannot see it again after closing the window.
- (Optional) Create a dataset to receive the frames: in Datasets (데이터셋), use New dataset (새 데이터셋), pick Direct upload (직접 업로드) and set the modality to image. Copy the ID (
ds-…) from the detail page address. If you skip this, frames go to Collected data (수집 데이터).
2. Configure
Change three places in the repository's edge.yaml.
central:
base_url: https://mlops.example.com # Central API address
device:
id: edge-yolo-01 # the device ID from step 1
collectors:
- type: frames
name: demo-frames
priority: 30 # lower than inference results (50): results go first on a narrow line
options:
every_n_frames: 30 # one frame per second for a 30 fps video
dataset_id: "ds-0123456789ab" # the dataset ID from step 1; empty means Collected data
labels: true # send LabelMe pre-labels too
label_conf: 0.5 # only detections at or above this confidence become pre-labels
Keep the token in .env, not in the config file.
cp .env.example .env
# put in .env: GEO_EDGE_CENTRAL__TOKEN=<device-token>
- Precedence is environment variables >
.env>edge.yaml> defaults, the same as the SDK. See Configuration reference for every key. - The demo's
edge.yamlsets the heartbeat to 10 seconds (default 30) so the screens update quickly. - It runs without a token too. Results only build up in the local queue (
data/edge/) and are sent once you add the token and run again.
3. Run
uv run mlops-edge-demo
With no argument it downloads the default video (17 seconds of city driving) once into cache/videos/ and plays it in a loop. The yolo26n.pt weights (COCO-pretrained) are downloaded into cache/weights/. Press q or ESC in the window to quit.
The status overlay at the top left of the window:
| Line | Content |
|---|---|
| 1 | MLOps Edge Demo and the device ID |
| 2 | Model name:version and inference time per frame. The version starts as pretrained |
| 3 | FPS, number of detections and the count per class |
| 4 | Central link state / registered or not / backlog / sync state / frames collected in this run. Green for online, yellow for offline and probing, red for auth_failed |
Other videos and modes:
uv run mlops-edge-demo ./line-camera.mp4 # local file (opened directly, no download)
uv run mlops-edge-demo rtsp://192.0.2.10/stream # RTSP stream
uv run mlops-edge-demo --no-show # device without a display; logs the status every 5 seconds
| Option | Default | Description |
|---|---|---|
SOURCE | default video URL | HTTP(S) URLs are downloaded and cached; anything else (file, stream address) is opened by OpenCV directly |
--weights | yolo26n.pt | YOLO weights. A bare name downloads the COCO-pretrained model into the cache |
--conf | 0.25 | Detection confidence threshold (for both the window and the inference results) |
--config | edge.yaml | Runtime config file |
--env-file | .env | File with secrets such as the token; ignored if missing |
--cache-dir | cache | Video and weights cache |
--report-interval | 1.0 | Seconds between inference results sent to Central; 0 sends every frame |
--once | Stop at the end of the video instead of looping | |
--no-show | Run without a window |
The runtime inside the demo opens the local API (127.0.0.1:8600), so the SDK CLI can query it (CLI).
uv run geo-mlops-edge status --config edge.yaml # link, backlog, models, attention items
uv run geo-mlops-edge queue --config edge.yaml --limit 5
If a geo-mlops-edge agent on the same PC already holds port 8600, the demo only turns off its local API (local API disabled warning) and keeps talking to Central.
4. Check on Central
- Open the device in Edge Fleet (Edge Fleet). ① SDK version and location ② a backlog of 0 under Backlog (쌓인 데이터) means everything collected has been sent ③ the
demo-framescollector isrunning.The demo device — ① SDK version and location ② zero backlog means everything collected has been sent ③ state of the demo-frames frames collector - The Inference results (추론 결과) tab gets one row per second. ① The model is the weights file name with version
pretrained; after a swap it is the name and version of the model from Central ② Details (자세히) expands the output JSON: per detection, the class number and name, the confidence, and the box[x1, y1, x2, y2]normalized to 0–1.Inference results tab — the YOLO detections the demo sends every second. ① model name and version ② expand the output JSON (class, confidence, normalized box) - The dataset's Files (파일) tab gets one image and a same-named LabelMe JSON every 30 frames — 17 per loop of the default video. ① Images paired with labels count toward trainability ② set the kind filter to images to see only the frames ③ "라벨 있음" on a card marks a matched pair.
Files tab of the collection dataset — ① 17 images paired with labels, ready to train ② kind filter set to Images ③ a frame image (paired with its same-named LabelMe pre-label) - Open an image to see the pre-labels (rectangles) the model drew. ① Hide annotations (주석 숨김) shows the original ② the label pair in the metadata is the same-named
.json.Collected frame preview — the model's pre-labels (rectangles). ① toggle the annotation overlay ② metadata (label pair, size)
- File names are
<video name>_<6-digit frame number>.jpg. For a video downloaded from a URL, the video name is the cache file name (a 12-character URL hash + the original name), for examplec5144a2b5077-kitti-inference-vid_000420.jpg. - Pre-labels are the current model's predictions. Training on them as they are teaches the model its own mistakes, so a person has to review them before training.
- Looping or restarting does not resend frames that were already queued. The record is
data/edge/collectors/demo-frames.seen.
5. Swap in a trained model
Send a model trained on the collected frames back to this device.
- Start a training run with Train with this dataset (이 데이터셋으로 학습) on the collection dataset. In the last step, turn on Register in model registry (모델 레지스트리 등록) and set Registered model name (등록 모델 이름) (for example
edge-demo-yolo). A model that is not registered does not appear in the list the device can download. Do not use/or'in the name (they break the device's download path and the registry lookup). - When training finishes, choose Pull model (모델 내려받기) in the device's Commands (명령) tab, pick the model and version, and press Send command (명령 보내기).
- On its next command check the device downloads the model, caches it under
data/edge/models/and activates it. The demo receives the activation event, swaps the playing model and logsswitched model: yolo26n:pretrained -> edge-demo-yolo:1. From then on, inference results and pre-labels carry the new model name and version.
- Only YOLO-framework models are shown in the window. A model of another framework only logs a warning, and playback continues with the current model.
- A cached model is activated again on the next run. See Models and inference for the activation rules.
- Approval (promotion to Production) and the deployment screens do not send a model to the device. The three ways to send one are the
pull_modelcommand, the device CLI and the local API.
Restart and exit
- Quitting with
q,ESC,Ctrl-CorSIGTERMlets the runtime settle the queue and close its database before exiting. - Restart (재시작) on the device detail page, or a
restartcommand, makes the demo exit with code 3. Run it under a supervisor such as systemd to have it start again (Deploy, Commands, policy and retention).
Code layout
mlops-edge/edge.yamlruntime config (Central address, device ID, intervals, retention, collectors).env.exampletoken templatesrc/mlops_edge_demo/cli.pycommand-line arguments, config loading, wiringvideo.pydownload and cache a video URLdetector.pyYOLO output converted to the SDK inference contract (InferenceOutput)edge.pyruns EdgeRuntime on its own thread; bridges reporting, frame collection and model swapsplayer.pyOpenCV playback loop and status overlayThe key to embedding the runtime in an application is separate threads. The OpenCV window wants the main thread and EdgeRuntime needs an asyncio event loop, so the runtime runs on a loop in its own thread and the video loop hands work over with asyncio.run_coroutine_threadsafe without waiting.
# edge.py, condensed. Runs on the runtime thread
runtime = EdgeRuntime(settings)
runtime.events.add_listener(on_event) # notified when pull_model activates a model
await runtime.start() # registration, heartbeat, commands, uploader, collectors
await serve(runtime, settings, stop) # local API (geo_mlops_sdk.edge.daemon.serve)
# On the video loop (main thread): encode only the frames to collect, hand them to the frames collector
if frames.wants(source, index, seekable=True):
ok, jpeg = cv2.imencode(".jpg", frame, [cv2.IMWRITE_JPEG_QUALITY, 90])
asyncio.run_coroutine_threadsafe(
frames.collect(source, index, jpeg.tobytes(), output, model=model_ref), loop
)
See Data collectors for the frames collector API and Models and inference for the inference output format (InferenceOutput).