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.

The demo window — device ID, model and inference time, FPS and detections, and the Central link state in the top left. Video from the KITTI dataset (CC BY-NC-SA 3.0)

How it fits together

Data flow of the demo app. Video playback and inference run on the main thread; the SDK runtime runs on its own thread.
The demo codeThe SDK runtime
Reads the video, runs YOLO, draws the results in a windowRegistration, heartbeats, command long-polling, local API (8600)
Queues an inference result every secondLocal queue, upload when online, buffering while offline
Encodes the frames to collect as JPEG and hands them to the frames collectorLabelMe pre-labels, de-duplication, chunked upload
Swaps the playing model when a model is activatedHandles pull_model (download, cache, activate)

What travels between the device and Central:

DirectionWhatWhere to see it on Central
Device → CentralRegistration, heartbeat (10 s)Edge Fleet (Edge Fleet) list and device detail
Device → CentralOne inference result per secondDevice detail, Inference results (추론 결과) tab
Device → CentralOne JPEG + one LabelMe JSON every 30 framesFiles (파일) tab of the dataset you name; Collected data (수집 데이터) if you name none
Central → DeviceCommands ping, restart, resync, set_policy, pull_modelDevice 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

  1. 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 need inference:write, frame uploads need data:write, and model swaps need models:read. See 5-minute start for the screens.
  2. Copy the token from the Registered (등록 완료) window. You cannot see it again after closing the window.
  3. (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.yaml sets 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:

LineContent
1MLOps Edge Demo and the device ID
2Model name:version and inference time per frame. The version starts as pretrained
3FPS, number of detections and the count per class
4Central 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
OptionDefaultDescription
SOURCEdefault video URLHTTP(S) URLs are downloaded and cached; anything else (file, stream address) is opened by OpenCV directly
--weightsyolo26n.ptYOLO weights. A bare name downloads the COCO-pretrained model into the cache
--conf0.25Detection confidence threshold (for both the window and the inference results)
--configedge.yamlRuntime config file
--env-file.envFile with secrets such as the token; ignored if missing
--cache-dircacheVideo and weights cache
--report-interval1.0Seconds between inference results sent to Central; 0 sends every frame
--onceStop at the end of the video instead of looping
--no-showRun 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

  1. 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-frames collector is running.
    The demo device — ① SDK version and location ② zero backlog means everything collected has been sent ③ state of the demo-frames frames collector
  2. 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)
  3. 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)
  4. 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 example c5144a2b5077-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.

  1. 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).
  2. When training finishes, choose Pull model (모델 내려받기) in the device's Commands (명령) tab, pick the model and version, and press Send command (명령 보내기).
  3. 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 logs switched 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_model command, the device CLI and the local API.

Restart and exit

  • Quitting with q, ESC, Ctrl-C or SIGTERM lets the runtime settle the queue and close its database before exiting.
  • Restart (재시작) on the device detail page, or a restart command, 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 template
src/mlops_edge_demo/
cli.pycommand-line arguments, config loading, wiring
video.pydownload and cache a video URL
detector.pyYOLO output converted to the SDK inference contract (InferenceOutput)
edge.pyruns EdgeRuntime on its own thread; bridges reporting, frame collection and model swaps
player.pyOpenCV playback loop and status overlay

The 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).

Written for the platform as of 2026-10-02.

© MLOps Central Platform