Metro AI App Recipe — DLSPS + WebRTC + Mosquitto + Node-RED + Grafana + Nginx
Build an end-to-end {{OBJECT}}-analytics stack on Intel hardware in
./{{STACK_DIR}}/ with Docker Compose. Vertical-agnostic: the same
seven-container topology (Nginx, DLSPS, Mosquitto, Node-RED, Grafana, MediaMTX,
Coturn) serves any DL Streamer / OpenVINO CV pipeline — only the model, class
filter, alert rule, dashboard, and topics differ. Follows the open-edge-platform
Metro Vision AI App Recipe
MediaMTX + Coturn + WebRTC path, streamlined (no Prometheus/OTel).
Scenescape is off by default (opt-in multi-camera analysis). Metadata flows
DLSPS→MQTT→Node-RED→Grafana; video is decoupled (DLSPS overlays via
gvawatermark, WHIP-pushes each source to MediaMTX, ENABLE_WEBRTC=true,
per-source peer-id) — see architecture below.
When to use this skill
Use when building an object-detection, classification, object-counting, or
zone/line-crossing alerting pipeline for any vertical (smart city/ITS, retail,
industrial, logistics, healthcare, or a custom OpenVINO/ONNX model). Optionally
adds a Scenescape multi-camera path, or a lightweight demo/PoC single app when
no full stack is needed.
Not for: non-Intel or cloud-only deployments, Prometheus/OpenTelemetry
metrics stacks, or training/exporting models.
Supported verticals & use-cases
| Vertical |
Example use-cases (each = one invoking prompt) |
| Smart city / ITS |
person/vehicle detection, ANPR, smart-parking, wrong-way |
| Retail |
customer counting, queue-length, shelf out-of-stock, dwell-time |
| Industrial / logistics |
surface-defect, PPE compliance, zone intrusion, forklift tracking |
| Healthcare / facilities |
fall detection, hand-hygiene, occupancy, perimeter intrusion |
| Custom |
any OpenVINO IR / ONNX detector + optional classifier |
The invoking prompt maps its vertical to concrete {{OBJECT}},
{{PIPELINE_NAME}}, {{DEFAULT_MODEL}}, {{DEFAULT_RULE}}, {{DASHBOARD_SLUG}}
— nothing else changes.
How to use this skill
- Read this file end-to-end.
- Ask Question 0 (mode) first. If demo, branch to
Demo/PoC mode + load
references/DEMO_POC.md, skip questions 1–7; else
(production, default) continue.
- Ask the 7 questions in ONE batched message (defaults in brackets); accept
go/defaults/empty. Question 7 selects the Scenescape path.
- Run parameter validation (below); refuse to proceed on any failure.
- Load reference file(s) on demand — not all up front (per the Reference
files table): Scenescape only when
{{SCENESCAPE}}=yes; PIPELINE for
GPU/NPU, RTSP//dev/video, or classifier; NODE_RED for </<=/>= rules
or a non-empty {{CLASS_FILTER_IDS}}.
- Verify against completion criteria before declaring success (record
throughput/latency vs
benchmark.md); validate_env.sh is step 0 of
install.sh.
Reference files (load on demand)
| File |
Load when authoring |
references/PIPELINE.md |
DLSPS config.json, GPU/NPU variants (_gpu/_npu + group_add), input sources (file/RTSP/device), gvaclassify wiring, REST launcher, watchdog |
references/PROXY_UI.md |
nginx.conf proxy (WHEP/WHIP + WebRTC-TCP), Grafana iframe panels, dashboard provisioning, Mosquitto |
references/NODE_RED.md |
flows.json, MQTT wildcard, gva_meta probe, alert flow |
references/INSTALL.md |
file layout, .env, validate_env.sh + rules, install.sh, docker-compose.yml volumes |
references/TESTS.md |
conftest.py, test_webrtc_stream.py, assertion contracts |
references/SCENESCAPE.md |
{{SCENESCAPE}}=yes only — multi-camera scene-fusion via scenescape-setup skill |
references/DEMO_POC.md |
{{MODE}}=demo only — lightweight single-app path (DL Streamer or OpenVINO); no full stack |
Parameters (from invoking prompt)
| Param |
Purpose |
{{MODE}} |
demo | production (default production). demo = single-app path (DEMO_POC); rows below are production-only |
{{OBJECT}} |
class label in dashboard/alerts (e.g. person, vehicle, defect, fall); any MQTT/Grafana-safe string |
{{STACK_DIR}} |
e.g. person-detect-stack, ppe-compliance-stack, anpr-stack |
{{DEFAULT_MODEL}}, {{OTHER_MODELS}} |
allowed model options |
{{PIPELINE_NAME}} |
canonical DLSPS pipeline name (e.g. yolov11s); variants <name>/_gpu/_npu; topic {{DETECTIONS_TOPIC_PREFIX}}_X/<name> |
{{CLASSIFIER}} |
secondary model or none; if set, also {{CLASSIFIER_URL}} + {{CLASSIFIER_XML}} |
{{CLASS_FILTER_IDS}} |
JSON array of class IDs to keep ([]=all). Filtered in Node-RED |
{{DEFAULT_RULE}} |
e.g. count>2 in 10s; parses to {{RULE_OP}}∈>,>=,<,<=, {{RULE_N}}, {{RULE_WINDOW_S}} (see NODE_RED) |
{{RULE_SCOPE}} |
per-source | aggregate (default per-source) |
{{ALERT_TOPIC}} |
e.g. alerts/{{OBJECT}} |
{{DETECTIONS_TOPIC_PREFIX}} |
e.g. object_detection (per-source _1, _2, …) |
{{COUNT_TOPIC}} |
e.g. stats/{{OBJECT}}_count |
{{LABEL_RULE_NOTE}} |
model-specific classification note for Node-RED |
{{DASHBOARD_SLUG}} |
e.g. smart-parking |
{{NUM_SOURCES}} |
default 4 |
{{SCENESCAPE}} |
yes | no (default no). yes = multi-camera path (SCENESCAPE) |
{{SCENE_NAME}} |
(Scenescape only) scene name, e.g. intersection-1 |
{{CAMERA_IDS}} |
(Scenescape only) unique IDs (no /), one per input stream, in input order |
{{TURN_USER}}, {{TURN_PASS}} |
Coturn / MediaMTX ICE credentials (default turnuser / a generated secret) |
Questions (single batched prompt)
Question 0 — Mode [production]: demo (single-app PoC) or production
(full stack). If demo, STOP and follow Demo/PoC mode; skip
questions 1–7 (they apply to production only).
- Model [
{{DEFAULT_MODEL}}] (also: {{OTHER_MODELS}})
- Classifier [
{{CLASSIFIER}}] (or none)
- Device [CPU] (GPU, NPU, AUTO)
- Inputs [{{NUM_SOURCES}}× sample-video] (or RTSP URLs /
/dev/videoN / local
paths); sets INPUT_TYPE. RTSP/device are continuous → no sample-video
download, no file:// watchdog (see PIPELINE).
- Node-RED rule [
{{DEFAULT_RULE}}, {{RULE_SCOPE}}]
- Alert channel [MQTT
{{ALERT_TOPIC}}]
- Scenescape multi-camera spatial analysis? [
{{SCENESCAPE}}, default no]
(if yes, also collect {{SCENE_NAME}} + one unique {{CAMERA_IDS}} per
input stream → references/SCENESCAPE.md)
Parameter validation (enforce BEFORE install.sh runs)
Ship validate_env.sh and call it as step 0 of install.sh; reject on any
failure. The script body and full validation rules table (MODE, HOST_IP,
NUM_SOURCES, DEVICE, PIPELINE_NAME, topics, TURN creds, inputs, Scenescape
params, …) are in references/INSTALL.md.
Reference architecture
Single Compose network app_network. Nginx publishes 80/443; Coturn also
publishes 3478/udp (WebRTC TURN). Nginx routes: /api/→DLSPS REST,
/grafana/→Grafana, /nodered/→Node-RED, /mediamtx/<pid>/→WHEP iframe,
/<pid>/whep|whip→signalling, /webrtc/→MediaMTX TCP (ICE 8189). Data:
DLSPS→MQTT→Mosquitto→Node-RED→Grafana; DLSPS→WHIP→MediaMTX (peer-id
{{DETECTIONS_TOPIC_PREFIX}}_N, ICE/TURN via Coturn); Grafana embeds
<iframe src="/mediamtx/{{DETECTIONS_TOPIC_PREFIX}}_N/">.
Demo/PoC mode
When Question 0 selects demo, do not build the full stack (no Compose
topology, no MediaMTX/Coturn/Node-RED/Grafana/Nginx, no Scenescape). Produce one
lightweight app proving a model runs on Intel hardware. Two sub-paths (ask which):
- DL Streamer app — a simple DL Streamer / GStreamer pipeline; delegate to
the
dlstreamer-coding-agent skill.
- OpenVINO app — a minimal Python script (load →
compile_model → infer →
post-process); no dedicated skill, follow the OpenVINO 2026 docs.
Full guidance + lightweight criteria are in
references/DEMO_POC.md — load only on this branch;
production criteria (1–11) do not apply in demo mode.
Scenescape spatial-analysis path (optional, {{SCENESCAPE}}=yes)
When Question 7 selects Scenescape, branch: keep the DLSPS detection pipeline
but replace the MediaMTX/WebRTC + Node-RED + Grafana-MQTT tail with an Intel®
Scenescape multi-camera scene-fusion stack. Do not re-implement by hand —
delegate to scenescape-setup, passing {{SCENE_NAME}} + {{CAMERA_IDS}}.
Architecture, images, validation, criteria in
references/SCENESCAPE.md; load only on this branch.
Default ({{SCENESCAPE}}=no) path is unchanged.
Images — pin to the latest available tag (never :latest)
Resolve each image to the newest published stable tag on Docker Hub (query
the repo's tags API with ordering=last_updated), pin it, and ignore
*-weekly pre-releases. Grafana is the one exception: keep it pinned to
11.5.4 (the MQTT datasource plugin only works on that tag).
intel/dlstreamer-pipeline-server:2026.1.0-ubuntu24
eclipse-mosquitto:2.1.2-alpine
nodered/node-red:5.0.4
nginx:1.31.3-alpine
bluenviron/mediamtx:1.20.0 (WebRTC: WHIP in, WHEP out)
coturn/coturn:4.17.0 (ICE/TURN)
grafana/grafana:11.5.4 (pinned — do not upgrade) with GF_INSTALL_PLUGINS="grafana-mqtt-datasource 1.3.3,yesoreyeram-infinity-datasource 3.11.1" — a bad plugin version kills the container → Nginx 502
intel/dlstreamer:2026.1.0-ubuntu24 (one-shot in install.sh: model download + INT8 quantize + TLS cert)
Layout (flat)
Generate a flat {{STACK_DIR}}/: README.md, docker-compose.yml, .env,
validate_env.sh, install.sh, sample_*.sh/update_dashboard.sh, a src/
tree (dlstreamer-pipeline-server/, mosquitto/, node-red/, grafana/,
nginx/), and tests/. Full annotated tree in
references/INSTALL.md.
README.md (required content)
The generated README.md MUST document, at minimum:
- Architecture —
DLSPS → MQTT (Mosquitto) → Node-RED → Grafana + decoupled
video DLSPS WHIP → MediaMTX → browser WHEP (Grafana <iframe>), Coturn
ICE/TURN, behind the Nginx TLS proxy; include the ASCII diagram + seven
containers.
- Quick start —
./install.sh → docker compose up -d →
./sample_start.sh <cpu|gpu|npu>, plus stop/status scripts.
- Access URLs + credentials — dashboard
https://<HOST_IP>/grafana/
(Grafana login admin/admin, change on first login), DLSPS REST
https://<HOST_IP>/api/pipelines, WHEP
https://<HOST_IP>/mediamtx/{{DETECTIONS_TOPIC_PREFIX}}_N/.
- Configuration — key
.env values (HOST_IP, GIDs, TURN creds) + how to
swap the video source for RTSP.
Template variable substitution
Every {{VAR}} MUST be substituted with its concrete value BEFORE writing the
file — a literal {{...}} left in nginx.conf, config.json, flows.json,
the dashboard JSON, or a test file is a syntax error.
Execution guardrails
- Hard timeouts: model dl+INT8 300 s; video dl 120 s/file;
compose pull 300 s;
compose up -d 120 s + 180 s healthy; each pytest 60 s.
- Max 2 retries per step, then STOP and print last 30 log lines from the failing
container. Never loop.
- Before
compose up: ss -ltn shows :80/:443 free, ss -lun shows
:3478 free (Coturn TURN).
- Bypass host proxy for all localhost/LAN curl — corporate proxies route
https://localhost/... through an unreachable proxy (→ Could not resolve host / 502). Every curl in sample_*.sh MUST use --noproxy '*' + --cacert src/nginx/ssl/server.crt (generated by install.sh); tests set NO_PROXY=*
in conftest.py.
- Test WebRTC signalling:
curl --cacert src/nginx/ssl/server.crt --noproxy '*' -sf -o /dev/null -w '%{http_code}' https://<HOST>/mediamtx/{{DETECTIONS_TOPIC_PREFIX}}_1/ (expect 200; stream exists only after sample_start.sh).
- Test MQTT:
docker run --rm --network <project>_app_network eclipse-mosquitto:2.1.2-alpine mosquitto_sub -h broker -t '#' -v (image tag 2.1.2-alpine is an intentional pin — never :latest).
- pytest venv at
./.venv inside stack dir (python -m venv .venv) — system
pip is PEP-668 blocked; /tmp may be noexec.
Optional external skills
If available, invoke; otherwise write files from the reference templates.
dlstreamer-coding-agent — pipeline JSON (+ demo/PoC DL Streamer app when {{MODE}}=demo)
dlsps-user — DLSPS deploy/config/REST; default production path (PIPELINE)
model-download — OMZ model IR
scenescape-setup — only when {{SCENESCAPE}}=yes (SCENESCAPE)
Reference implementation
The upstream
smart-parking/src/
recipe uses the same path — consult it for config.json, mosquitto.conf,
nginx.conf, datasources.yml, dashboards.yml, flows.json shapes; drop
prometheus/otel-collector/metrics-manager.
Completion criteria (all must pass)
When {{SCENESCAPE}}=yes, criteria 3–11 are superseded by the Scenescape
criteria in references/SCENESCAPE.md; 1–2 still apply.
./install.sh succeeds: .env populated; INT8 model + optional classifier
IR under src/dlstreamer-pipeline-server/models/…; videos downloaded; TLS
cert with SAN.
./validate_env.sh cpu exits 0 with a valid .env;
HOST_IP=127.0.0.1 ./validate_env.sh cpu exits non-zero.
docker compose up -d → all containers running/healthy (incl.
mediamtx-server, coturn).
curl --cacert src/nginx/ssl/server.crt https://localhost/api/pipelines/status returns 3 variants.
./sample_start.sh <cpu|gpu|npu> launches {{NUM_SOURCES}} pipelines; none
QUEUED, all RUNNING.
- Detections arrive on
{{DETECTIONS_TOPIC_PREFIX}}_1..{{NUM_SOURCES}}/{{PIPELINE_NAME}}
(or _gpu/_npu) within 30 s.
curl --cacert src/nginx/ssl/server.crt https://localhost/mediamtx/{{DETECTIONS_TOPIC_PREFIX}}_1/
returns 200 (WHEP HTML) once pipelines run; MediaMTX logs show the WHIP
publisher connected per peer-id.
- Node-RED publishes JSON
{{ALERT_TOPIC}} and scalar {{COUNT_TOPIC}} /
stats/alert_active / stats/alert_total per {{RULE_SCOPE}};
mosquitto_sub -t '{{COUNT_TOPIC}}/#' -C 1 MUST parse as int() (JSON breaks
Grafana plotting).
- Grafana at
https://localhost/grafana (admin/admin) shows live {{OBJECT}}
counts + alert data and {{NUM_SOURCES}} <iframe> WebRTC panels
(GF_SECURITY_ALLOW_EMBEDDING=true). MQTT datasource health
(/grafana/api/datasources/uid/mqtt_ds/health) MUST return "MQTT Connected" — broker in jsonData.uri (tcp://broker:1883), not url:.
Blank-panel / redirect-loop fixes: references/PROXY_UI.md.
pytest -q tests/ passes; pytest --collect-only -q tests/ | tail -1
reports ≥ 9 tests collected (no empty stubs).
- Watchdog continuity (file:// sources): after
video-length + 30 s,
/api/pipelines/status shows {{NUM_SOURCES}} RUNNING (COMPLETED
history is fine), WebRTC re-establishes, MQTT still flowing
(>{{NUM_SOURCES}} RUNNING = missing watchdog dedup guard).
Final summary — surface the proof (don't just name files)
Graders see only your final message + tool names, not file contents. An
expectation counts as met only if you state it and quote the one decisive
line in your closing summary — walk every completion criterion plus the
proof-point checklist (topology, WebRTC, pinned tags incl. grafana:11.5.4,
MQTT/class filter, no literal {{...}}, validate_env.sh step 0, cert
subjectAltName, curl --noproxy '*' + --cacert, inputs/watchdog, parsed
rule, classifier, GPU group_add, Scenescape) detailed in
references/INSTALL.md → Final-summary proof points.
A claim with no quoted evidence is treated as unmet.
1---2name: metro-ai-app-recipe3description: Stand up a complete, ready-to-run computer-vision analytics stack on Intel hardware with one Docker Compose command — point it at your video sources and an OpenVINO/ONNX model to get live annotated WebRTC video plus real-time detection dashboards and alerts (object detection, classification, counting, or zone/line-crossing) for any vertical, with no glue code. See "When to use this skill" for the full component list, trigger conditions, and boundaries.4license: Apache-2.05---67# Metro AI App Recipe — DLSPS + WebRTC + Mosquitto + Node-RED + Grafana + Nginx89Build an end-to-end `{{OBJECT}}`-analytics stack on Intel hardware in10`./{{STACK_DIR}}/` with Docker Compose. **Vertical-agnostic:** the same11seven-container topology (Nginx, DLSPS, Mosquitto, Node-RED, Grafana, MediaMTX,12Coturn) serves any DL Streamer / OpenVINO CV pipeline — only the model, class13filter, alert rule, dashboard, and topics differ. Follows the open-edge-platform14[Metro Vision AI App Recipe](https://github.com/open-edge-platform/edge-ai-suites/tree/main/metro-ai-suite/metro-vision-ai-app-recipe)15**MediaMTX + Coturn + WebRTC** path, streamlined (**no Prometheus/OTel**).16Scenescape is **off by default** (opt-in multi-camera analysis). Metadata flows17DLSPS→MQTT→Node-RED→Grafana; video is decoupled (DLSPS overlays via18`gvawatermark`, WHIP-pushes each source to MediaMTX, `ENABLE_WEBRTC=true`,19per-source `peer-id`) — see architecture below.2021## When to use this skill2223**Use when** building an object-detection, classification, object-counting, or24zone/line-crossing alerting pipeline for any vertical (smart city/ITS, retail,25industrial, logistics, healthcare, or a custom OpenVINO/ONNX model). Optionally26adds a Scenescape multi-camera path, or a lightweight demo/PoC single app when27no full stack is needed.2829**Not for:** non-Intel or cloud-only deployments, Prometheus/OpenTelemetry30metrics stacks, or training/exporting models.3132## Supported verticals & use-cases3334| Vertical | Example use-cases (each = one invoking prompt) |35|---|---|36| Smart city / ITS | person/vehicle detection, ANPR, smart-parking, wrong-way |37| Retail | customer counting, queue-length, shelf out-of-stock, dwell-time |38| Industrial / logistics | surface-defect, PPE compliance, zone intrusion, forklift tracking |39| Healthcare / facilities | fall detection, hand-hygiene, occupancy, perimeter intrusion |40| Custom | any OpenVINO IR / ONNX detector + optional classifier |4142The invoking prompt maps its vertical to concrete `{{OBJECT}}`,43`{{PIPELINE_NAME}}`, `{{DEFAULT_MODEL}}`, `{{DEFAULT_RULE}}`, `{{DASHBOARD_SLUG}}`44— nothing else changes.4546## How to use this skill47481. Read this file end-to-end.492. Ask **Question 0 (mode)** first. If **demo**, branch to50 [Demo/PoC mode](#demopoc-mode) + load51 [`references/DEMO_POC.md`](references/DEMO_POC.md), skip questions 1–7; else52 (**production**, default) continue.533. Ask the 7 questions in ONE batched message (defaults in brackets); accept54 `go`/`defaults`/empty. Question 7 selects the **Scenescape** path.554. Run parameter validation (below); refuse to proceed on any failure.565. Load reference file(s) on demand — **not all up front** (per the *Reference57 files* table): Scenescape only when `{{SCENESCAPE}}=yes`; PIPELINE for58 GPU/NPU, RTSP/`/dev/video`, or classifier; NODE_RED for `<`/`<=`/`>=` rules59 or a non-empty `{{CLASS_FILTER_IDS}}`.606. Verify against completion criteria before declaring success (record61 throughput/latency vs `benchmark.md`); `validate_env.sh` is **step 0 of62 `install.sh`**.6364## Reference files (load on demand)6566| File | Load when authoring |67|---|---|68| [`references/PIPELINE.md`](references/PIPELINE.md) | DLSPS `config.json`, GPU/NPU variants (`_gpu`/`_npu` + `group_add`), input sources (file/RTSP/device), `gvaclassify` wiring, REST launcher, watchdog |69| [`references/PROXY_UI.md`](references/PROXY_UI.md) | `nginx.conf` proxy (WHEP/WHIP + WebRTC-TCP), Grafana iframe panels, dashboard provisioning, Mosquitto |70| [`references/NODE_RED.md`](references/NODE_RED.md) | `flows.json`, MQTT wildcard, `gva_meta` probe, alert flow |71| [`references/INSTALL.md`](references/INSTALL.md) | file layout, `.env`, `validate_env.sh` + rules, `install.sh`, `docker-compose.yml` volumes |72| [`references/TESTS.md`](references/TESTS.md) | `conftest.py`, `test_webrtc_stream.py`, assertion contracts |73| [`references/SCENESCAPE.md`](references/SCENESCAPE.md) | **`{{SCENESCAPE}}=yes` only** — multi-camera scene-fusion via `scenescape-setup` skill |74| [`references/DEMO_POC.md`](references/DEMO_POC.md) | **`{{MODE}}=demo` only** — lightweight single-app path (DL Streamer or OpenVINO); no full stack |7576## Parameters (from invoking prompt)7778| Param | Purpose |79|---|---|80| `{{MODE}}` | `demo` \| `production` (default `production`). `demo` = single-app path ([DEMO_POC](references/DEMO_POC.md)); rows below are `production`-only |81| `{{OBJECT}}` | class label in dashboard/alerts (e.g. `person`, `vehicle`, `defect`, `fall`); any MQTT/Grafana-safe string |82| `{{STACK_DIR}}` | e.g. `person-detect-stack`, `ppe-compliance-stack`, `anpr-stack` |83| `{{DEFAULT_MODEL}}`, `{{OTHER_MODELS}}` | allowed model options |84| `{{PIPELINE_NAME}}` | canonical DLSPS pipeline `name` (e.g. `yolov11s`); variants `<name>`/`_gpu`/`_npu`; topic `{{DETECTIONS_TOPIC_PREFIX}}_X/<name>` |85| `{{CLASSIFIER}}` | secondary model or `none`; if set, also `{{CLASSIFIER_URL}}` + `{{CLASSIFIER_XML}}` |86| `{{CLASS_FILTER_IDS}}` | JSON array of class IDs to keep (`[]`=all). Filtered in Node-RED |87| `{{DEFAULT_RULE}}` | e.g. `count>2 in 10s`; parses to `{{RULE_OP}}`∈`>`,`>=`,`<`,`<=`, `{{RULE_N}}`, `{{RULE_WINDOW_S}}` (see [NODE_RED](references/NODE_RED.md)) |88| `{{RULE_SCOPE}}` | `per-source` \| `aggregate` (default `per-source`) |89| `{{ALERT_TOPIC}}` | e.g. `alerts/{{OBJECT}}` |90| `{{DETECTIONS_TOPIC_PREFIX}}` | e.g. `object_detection` (per-source `_1`, `_2`, …) |91| `{{COUNT_TOPIC}}` | e.g. `stats/{{OBJECT}}_count` |92| `{{LABEL_RULE_NOTE}}` | model-specific classification note for Node-RED |93| `{{DASHBOARD_SLUG}}` | e.g. `smart-parking` |94| `{{NUM_SOURCES}}` | default `4` |95| `{{SCENESCAPE}}` | `yes` \| `no` (default `no`). `yes` = multi-camera path ([SCENESCAPE](references/SCENESCAPE.md)) |96| `{{SCENE_NAME}}` | (Scenescape only) scene name, e.g. `intersection-1` |97| `{{CAMERA_IDS}}` | (Scenescape only) unique IDs (no `/`), one per input stream, in input order |98| `{{TURN_USER}}`, `{{TURN_PASS}}` | Coturn / MediaMTX ICE credentials (default `turnuser` / a generated secret) |99100## Questions (single batched prompt)101102**Question 0 — Mode** [`production`]: `demo` (single-app PoC) or `production`103(full stack). If `demo`, STOP and follow [Demo/PoC mode](#demopoc-mode); skip104questions 1–7 (they apply to `production` only).1051061. Model [`{{DEFAULT_MODEL}}`] (also: `{{OTHER_MODELS}}`)1072. Classifier [`{{CLASSIFIER}}`] (or `none`)1083. Device [CPU] (GPU, NPU, AUTO)1094. Inputs [{{NUM_SOURCES}}× sample-video] (or RTSP URLs / `/dev/videoN` / local110 paths); sets `INPUT_TYPE`. RTSP/device are **continuous** → no sample-video111 download, no file:// watchdog (see [PIPELINE](references/PIPELINE.md)).1125. Node-RED rule [`{{DEFAULT_RULE}}`, `{{RULE_SCOPE}}`]1136. Alert channel [MQTT `{{ALERT_TOPIC}}`]1147. Scenescape multi-camera spatial analysis? [`{{SCENESCAPE}}`, default `no`]115 (if `yes`, also collect `{{SCENE_NAME}}` + one unique `{{CAMERA_IDS}}` per116 input stream → [`references/SCENESCAPE.md`](references/SCENESCAPE.md))117118## Parameter validation (enforce BEFORE `install.sh` runs)119120Ship `validate_env.sh` and call it as step 0 of `install.sh`; reject on any121failure. The script body and full **validation rules table** (`MODE`, `HOST_IP`,122`NUM_SOURCES`, `DEVICE`, `PIPELINE_NAME`, topics, TURN creds, inputs, Scenescape123params, …) are in [`references/INSTALL.md`](references/INSTALL.md).124125## Reference architecture126127Single Compose network `app_network`. Nginx publishes 80/443; **Coturn also128publishes `3478/udp`** (WebRTC TURN). Nginx routes: `/api/`→DLSPS REST,129`/grafana/`→Grafana, `/nodered/`→Node-RED, `/mediamtx/<pid>/`→WHEP iframe,130`/<pid>/whep|whip`→signalling, `/webrtc/`→MediaMTX TCP (ICE 8189). Data:131DLSPS→MQTT→Mosquitto→Node-RED→Grafana; DLSPS→WHIP→MediaMTX (peer-id132`{{DETECTIONS_TOPIC_PREFIX}}_N`, ICE/TURN via Coturn); Grafana embeds133`<iframe src="/mediamtx/{{DETECTIONS_TOPIC_PREFIX}}_N/">`.134135## Demo/PoC mode136137When Question 0 selects `demo`, **do not build the full stack** (no Compose138topology, no MediaMTX/Coturn/Node-RED/Grafana/Nginx, no Scenescape). Produce one139lightweight app proving a model runs on Intel hardware. Two sub-paths (ask which):140141- **DL Streamer app** — a simple DL Streamer / GStreamer pipeline; delegate to142 the `dlstreamer-coding-agent` skill.143- **OpenVINO app** — a minimal Python script (load → `compile_model` → infer →144 post-process); no dedicated skill, follow the OpenVINO 2026 docs.145146Full guidance + lightweight criteria are in147[`references/DEMO_POC.md`](references/DEMO_POC.md) — load only on this branch;148production criteria (1–11) do **not** apply in demo mode.149150## Scenescape spatial-analysis path (optional, `{{SCENESCAPE}}=yes`)151152When Question 7 selects Scenescape, **branch**: keep the DLSPS detection pipeline153but replace the MediaMTX/WebRTC + Node-RED + Grafana-MQTT tail with an Intel®154Scenescape multi-camera scene-fusion stack. **Do not re-implement by hand** —155delegate to `scenescape-setup`, passing `{{SCENE_NAME}}` + `{{CAMERA_IDS}}`.156Architecture, images, validation, criteria in157[`references/SCENESCAPE.md`](references/SCENESCAPE.md); load only on this branch.158Default (`{{SCENESCAPE}}=no`) path is unchanged.159160## Images — pin to the latest available tag (never `:latest`)161162Resolve each image to the **newest published stable tag on Docker Hub** (query163the repo's `tags` API with `ordering=last_updated`), pin it, and ignore164`*-weekly` pre-releases. **Grafana is the one exception:** keep it pinned to165`11.5.4` (the MQTT datasource plugin only works on that tag).166167- `intel/dlstreamer-pipeline-server:2026.1.0-ubuntu24`168- `eclipse-mosquitto:2.1.2-alpine`169- `nodered/node-red:5.0.4`170- `nginx:1.31.3-alpine`171- `bluenviron/mediamtx:1.20.0` (WebRTC: WHIP in, WHEP out)172- `coturn/coturn:4.17.0` (ICE/TURN)173- `grafana/grafana:11.5.4` (**pinned — do not upgrade**) with `GF_INSTALL_PLUGINS="grafana-mqtt-datasource 1.3.3,yesoreyeram-infinity-datasource 3.11.1"` — a bad plugin version kills the container → Nginx 502174- `intel/dlstreamer:2026.1.0-ubuntu24` (one-shot in `install.sh`: model download + INT8 quantize + TLS cert)175176## Layout (flat)177178Generate a flat `{{STACK_DIR}}/`: `README.md`, `docker-compose.yml`, `.env`,179`validate_env.sh`, `install.sh`, `sample_*.sh`/`update_dashboard.sh`, a `src/`180tree (`dlstreamer-pipeline-server/`, `mosquitto/`, `node-red/`, `grafana/`,181`nginx/`), and `tests/`. Full annotated tree in182[`references/INSTALL.md`](references/INSTALL.md).183184### `README.md` (required content)185186The generated `README.md` MUST document, at minimum:187188- **Architecture** — `DLSPS → MQTT (Mosquitto) → Node-RED → Grafana` + decoupled189 video `DLSPS WHIP → MediaMTX → browser WHEP (Grafana <iframe>)`, Coturn190 ICE/TURN, behind the Nginx TLS proxy; include the ASCII diagram + seven191 containers.192- **Quick start** — `./install.sh` → `docker compose up -d` →193 `./sample_start.sh <cpu|gpu|npu>`, plus stop/status scripts.194- **Access URLs + credentials** — dashboard `https://<HOST_IP>/grafana/`195 (**Grafana login `admin`/`admin`**, change on first login), DLSPS REST196 `https://<HOST_IP>/api/pipelines`, WHEP197 `https://<HOST_IP>/mediamtx/{{DETECTIONS_TOPIC_PREFIX}}_N/`.198- **Configuration** — key `.env` values (`HOST_IP`, GIDs, TURN creds) + how to199 swap the video source for RTSP.200201## Template variable substitution202203Every `{{VAR}}` MUST be substituted with its concrete value BEFORE writing the204file — a literal `{{...}}` left in `nginx.conf`, `config.json`, `flows.json`,205the dashboard JSON, or a test file is a syntax error.206207## Execution guardrails208209- Hard timeouts: model dl+INT8 300 s; video dl 120 s/file; `compose pull` 300 s;210 `compose up -d` 120 s + 180 s healthy; each pytest 60 s.211- Max 2 retries per step, then STOP and print last 30 log lines from the failing212 container. Never loop.213- Before `compose up`: `ss -ltn` shows `:80`/`:443` free, `ss -lun` shows214 `:3478` free (Coturn TURN).215- **Bypass host proxy for all localhost/LAN curl** — corporate proxies route216 `https://localhost/...` through an unreachable proxy (→ `Could not resolve217 host` / 502). Every curl in `sample_*.sh` MUST use `--noproxy '*'` + `--cacert218 src/nginx/ssl/server.crt` (generated by `install.sh`); tests set `NO_PROXY=*`219 in `conftest.py`.220- Test WebRTC signalling: `curl --cacert src/nginx/ssl/server.crt --noproxy '*' -sf -o /dev/null -w '%{http_code}' https://<HOST>/mediamtx/{{DETECTIONS_TOPIC_PREFIX}}_1/` (expect `200`; stream exists only after `sample_start.sh`).221- Test MQTT: `docker run --rm --network <project>_app_network eclipse-mosquitto:2.1.2-alpine mosquitto_sub -h broker -t '#' -v` (image tag `2.1.2-alpine` is an intentional pin — never `:latest`).222- pytest venv at `./.venv` inside stack dir (`python -m venv .venv`) — system223 pip is PEP-668 blocked; `/tmp` may be `noexec`.224225## Optional external skills226227If available, invoke; otherwise write files from the reference templates.228- `dlstreamer-coding-agent` — pipeline JSON (+ demo/PoC DL Streamer app when `{{MODE}}=demo`)229- `dlsps-user` — DLSPS deploy/config/REST; default `production` path ([PIPELINE](references/PIPELINE.md))230- `model-download` — OMZ model IR231- `scenescape-setup` — **only when `{{SCENESCAPE}}=yes`** ([SCENESCAPE](references/SCENESCAPE.md))232233## Reference implementation234235The upstream236[`smart-parking/src/`](https://github.com/open-edge-platform/edge-ai-suites/tree/main/metro-ai-suite/metro-vision-ai-app-recipe/smart-parking/src)237recipe uses the same path — consult it for `config.json`, `mosquitto.conf`,238`nginx.conf`, `datasources.yml`, `dashboards.yml`, `flows.json` shapes; drop239`prometheus`/`otel-collector`/`metrics-manager`.240241## Completion criteria (all must pass)242243> When `{{SCENESCAPE}}=yes`, criteria 3–11 are **superseded** by the Scenescape244> criteria in [`references/SCENESCAPE.md`](references/SCENESCAPE.md); 1–2 still apply.2452461. `./install.sh` succeeds: `.env` populated; INT8 model + optional classifier247 IR under `src/dlstreamer-pipeline-server/models/…`; videos downloaded; TLS248 cert with SAN.2492. `./validate_env.sh cpu` exits 0 with a valid `.env`;250 `HOST_IP=127.0.0.1 ./validate_env.sh cpu` exits non-zero.2513. `docker compose up -d` → all containers `running`/`healthy` (incl.252 `mediamtx-server`, `coturn`).2534. `curl --cacert src/nginx/ssl/server.crt https://localhost/api/pipelines/status` returns 3 variants.2545. `./sample_start.sh <cpu|gpu|npu>` launches `{{NUM_SOURCES}}` pipelines; none255 `QUEUED`, all `RUNNING`.2566. Detections arrive on257 `{{DETECTIONS_TOPIC_PREFIX}}_1..{{NUM_SOURCES}}/{{PIPELINE_NAME}}`258 (or `_gpu`/`_npu`) within 30 s.2597. `curl --cacert src/nginx/ssl/server.crt https://localhost/mediamtx/{{DETECTIONS_TOPIC_PREFIX}}_1/`260 returns 200 (WHEP HTML) once pipelines run; MediaMTX logs show the WHIP261 publisher connected per `peer-id`.2628. Node-RED publishes JSON `{{ALERT_TOPIC}}` and **scalar** `{{COUNT_TOPIC}}` /263 `stats/alert_active` / `stats/alert_total` per `{{RULE_SCOPE}}`;264 `mosquitto_sub -t '{{COUNT_TOPIC}}/#' -C 1` MUST parse as `int()` (JSON breaks265 Grafana plotting).2669. Grafana at `https://localhost/grafana` (admin/admin) shows live {{OBJECT}}267 counts + alert data and `{{NUM_SOURCES}}` `<iframe>` WebRTC panels268 (`GF_SECURITY_ALLOW_EMBEDDING=true`). MQTT datasource health269 (`/grafana/api/datasources/uid/mqtt_ds/health`) MUST return `"MQTT270 Connected"` — broker in **`jsonData.uri`** (`tcp://broker:1883`), not `url:`.271 Blank-panel / redirect-loop fixes: [`references/PROXY_UI.md`](references/PROXY_UI.md).27210. `pytest -q tests/` passes; `pytest --collect-only -q tests/ | tail -1`273 reports ≥ 9 tests collected (no empty stubs).27411. **Watchdog continuity** (file:// sources): after `video-length + 30 s`,275 `/api/pipelines/status` shows `{{NUM_SOURCES}}` `RUNNING` (`COMPLETED`276 history is fine), WebRTC re-establishes, MQTT still flowing277 (`>{{NUM_SOURCES}}` `RUNNING` = missing watchdog dedup guard).278279## Final summary — surface the proof (don't just name files)280281Graders see only your **final message** + tool *names*, not file contents. An282expectation counts as met only if you **state it and quote the one decisive283line** in your closing summary — walk every completion criterion plus the284proof-point checklist (topology, WebRTC, pinned tags incl. `grafana:11.5.4`,285MQTT/class filter, no literal `{{...}}`, `validate_env.sh` step 0, cert286`subjectAltName`, curl `--noproxy '*'` + `--cacert`, inputs/watchdog, parsed287rule, classifier, GPU `group_add`, Scenescape) detailed in288[`references/INSTALL.md`](references/INSTALL.md) → *Final-summary proof points*.289A claim with no quoted evidence is treated as unmet.