dynamic_overlays.ChatCfg.template (опц.) — Jinja2 шаблон применяется к
incoming MQTT payload. Доступны переменные payload (raw str) и payload_json
(parsed JSON dict если valid). Используется для извлечения отдельного поля
из JSON, например temperature из z2m sensor.
Применение в controller.yaml:
chats:
- id: temp_outside
source_topic: "zigbee2mqtt/Температура на улице"
template: "{{ '%+.1f' | format(payload_json.temperature | float) }}°C"
Если template не задан — payload используется как есть (backwards compat
с alerts_chat и подобными).
UI editor static/index.html: для icon/text overlays вместо коробки показываем
точечный маркер 14×14 (синий circle). Причина: filter render_overlay_icon
использует native PNG dimensions, frontend не знает реальный размер
(w_px/h_px хранятся в ChatCfg на server-side). Показ size=10% как было —
вводил в заблуждение. Resize handle тоже скрыт для marker'ов.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Поверх preview (image от /snapshot endpoint) добавлен editor layer с
drag-and-drop созданием / перемещением / resize / удалением overlay'ев
через HTTP API (POST/PATCH/DELETE /overlay/{instance}[/{id}]).
Toolbar: draw mode toggle, type (rect-border / rect-fill / dim / text),
color picker, opacity slider. Drag по preview → создание нового overlay
с правильными нормализованными координатами. Клик на overlay → выбор +
drag перемещение, нижний-правый handle = resize, красный × delete.
Cell-bound overlays (Frigate motion borders, dynamic_overlays grafana
panel) не отрисовываются в editor — они уже физически в видео output
и заполняли весь editor area, делая cursor:not-allowed везде. Только
absolute (cell=null) overlays редактируемы.
Preview: HLS отключён.
mediamtx в LL-HLS режиме отдаёт пустые audio fragments
(passthrough-remuxer: "No samples for initPTS at playlist time",
"Duration parsed from mp4 should be greater than zero"). hls.js застревает
с readyState=1, buffered=0. lowLatencyMode:false на клиенте не помог —
mediamtx сам делит на video1_stream.m3u8 + audio2_stream.m3u8 на server
side. Diagnosed через Playwright + hls.js debug logs.
Заменено на <img> с polling /snapshot/{instance} (~700ms request, де-факто
~5s из-за времени ffmpeg RTSP+PNG, но достаточно как reference для
overlay positioning).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Bug #1: _check_stall возвращался early если alive=False (pipeline ZMQ dead).
Но pipeline может hung без exit'а (ffmpeg process жив но encoder deadlock):
ZMQ не отвечает (alive=False), при этом encoder не emit'ит frames в
mediamtx. Wrapper script не retry'ит (process не exited), никто не
поднимает алерт. Fix: stall check работает ВНЕ зависимости от alive.
Bug #2: _check_stall возвращался early если /live publish session
отсутствует в mediamtx /v3/rtspsessions/list. Pipeline мог никогда
не подключиться (или TCP push session дропнулся). Treat as "frozen
at 0 bytes" — stall alert fire'ится через N polls.
Bug #3 (bonus): _check_instance логировал "lost" но не emit'ил MQTT
event. HA не видела алертов. Fix: добавлен on_event call для pipeline_lost
и pipeline_restored (paired с уже существующими pipeline_stalled /
pipeline_unstalled).
Verified на сегодняшнем incident: pipeline encoder hung 4 hours,
никто не реагировал. После fix monitor emit'ит lost+stalled события
через 12 секунд → MQTT-listener (HA automation) может сделать
docker restart cuda-grid-pipeline.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
При startup controller iterate'т FrigateBridge mappings и рендерит:
- offline_<cell>.png с camera label из placeholder_label (или frigate_camera
как fallback) — для каждого pad
- offline.png generic — для cells без mapping
Filter cuda_grid looks up "offline_<pad>.png" first per-cell, fallback к
"offline.png". User меняет yaml placeholder_label → restart controller →
PNGs регенерируются. Никаких ручных PNG manipulations.
Russian text supported (PIL + freetype, vs single-byte path в filter).
Pattern:
frigate.mappings:
- frigate_camera: parking_overview
placeholder_label: Парковка
cell: 0
...
При stale input на cell 0 → filter blits offline_0.png с "Парковка — НЕТ
СИГНАЛА" (текст желтый, transparent bg — filter сам fill'ит black за иконкой).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Resilience improvement — раньше pipeline mог hung без exit (NVENC stuck,
output broken pipe), Docker restart policy не triggered. Никакой alert.
Now: poll mediamtx /v3/rtspsessions/list каждые N sec, track publish session
inboundBytes. Не растёт 3 polls (~9 sec) → emit MQTT 'pipeline_stalled' event
(через dispatcher.on_event = mqtt.publish_event). User / Home Assistant
automation решает что делать (restart container, notify).
Wired:
pipeline_monitor.on_event = mqtt.publish_event # __main__.py
Bytes started growing again → emit 'pipeline_unstalled'.
Alert single-shot: пока stalled flag set, no dup alerts. Reset когда
bytes counter растёт.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Bug: restore триггерился сразу после detected "ZMQ ping success", но
filter graph внутри ffmpeg ещё в startup phase — accepts ZMQ commands
но overlay add не fully apply'ются. Часть overlays lost silently.
Симптом: monitor logs "restore_done overlays=6", но pipeline ZMQ probe
показывает filter имеет только 2-3 overlays. Grafana / alerts_chat /
frigate event overlays отсутствуют → user sees пустые cells / strip.
Fix: asyncio.sleep(2.0) перед re-pushing layout + overlays. Pipeline
filter всегда успевает finish init за 2 sec (verified повторными
restart tests).
Cost: 2-sec задержка между detect + visible restore. Acceptable —
total recovery time pipeline crash → fully visible на TV ≈ 5-7 sec.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Pipeline filter state (overlays, layout, cell_map, audio) живёт в RAM
ffmpeg process. При recreate container (compose up, OOM, NVENC crash,
config change) state lost — controller'у нужно re-push.
Раньше user'у приходилось вручную:
curl POST /layout/.../set
docker restart cuda-grid-controller # для browser/dynamic re-register
Теперь автоматизировано:
PipelineMonitor polls ZMQ каждые 3 sec (no-op set_layout).
On timeout/error → mark instance lost.
First success after lost → trigger restore:
1. set_layout к state.active_layout
2. set_audio_output_enabled к state.audio_output_enabled
3. re-push все overlays из state.overlays
4. browser/dynamic/frigate hooks: mark_all_unregistered() —
их loops автоматически re-add на next iteration
Verified test: docker restart cuda-grid-pipeline → 10 sec downtime →
monitor logs lost+restored+restore_done с count=6 overlays.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Frigate publishes 5-10 detection events/sec при motion → каждое emit ZMQ
overlay update к pipeline (mutex acquire, atlas rebuild). Это блокирует
render thread и вызывает TV stutter (см. cuda-grid-filter-stutter memory
2026-05-21 диагностика).
Throttle config:
bbox_min_interval_sec: 0.25 # max 4 updates/sec на event_id
bbox_delta_threshold: 0.02 # skip если все coords changed < 2% camera dim
State в self._event_bbox_last: event_id → (timestamp, nx, ny, nw, nh).
Cleanup в _remove_event_overlay (event end).
С default 0.25s + 0.02 threshold:
5-10 ev/sec → ~2-4 ev/sec applied (rate-limit), плюс stationary objects
не апдейтятся вообще (delta filter). Render-thread load на bbox flow
снижается 60-80%.
Эффект — можно вернуть bbox_overlay=true в controller.yaml без risk
TV stutter. Diagnostic-disable из 2026-05-21 теперь не нужен.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Use case: Grafana с session cookie (login flow вместо Bearer token),
admin UIs без API token endpoint.
Config:
dashboards:
- id: my_widget
cookies:
- name: session_id
value: abc123
url: https://example.com/
Каждый element passed к page.context.add_cookies. Playwright spec format:
{name, value, url} OR {name, value, domain, path, ...}.
Multi-dashboard verified рабочий (caвая dashboard = own Page в shared browser).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Config.from_yaml теперь recursively expands ${VAR} и ${VAR:-default}
в string values через os.environ. Позволяет хранить tokens / passwords
в gitignored .env, passed контейнеру через compose env section:
controller.yaml:
extra_http_headers:
Authorization: "Bearer ${GRAFANA_TOKEN}"
.env (gitignored):
GRAFANA_TOKEN=glsa_xxx
docker-compose.override.yml controller:
environment:
GRAFANA_TOKEN: "${GRAFANA_TOKEN:-}" # compose interpolates от .env
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Без resize element screenshot возвращался в native dims (e.g. Grafana panel
790×258), не fitting configured overlay slot (1280×180 = info strip). Result
— overflow + gap.
С resize (LANCZOS) PNG exactly w_px×h_px, fills slot полностью. Trade-off:
если aspect не совпадает (panel 3:1 → strip 7:1), будет растяжение
horizontal. User responsibility tune w_px/h_px к нужным пропорциям.
Verified: GPU/VRAM panel 790×258 → 1280×180 readable, lines + labels OK.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Sync с ffmpeg-fresh n7.1-vf-cuda-grid-phase7 cd1839f. Layout validation
(http_api set_layout, dispatch _set_layout) теперь accept'ит main_with_strip.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Use case: Grafana service account token, basic auth, custom UA. Headers
устанавливаются через page.set_extra_http_headers перед первым goto.
Пример:
dashboards:
- id: grafana_gpu
url: https://chat2.goldix.org/grafana/d/UID/dash
extra_http_headers:
Authorization: "Bearer glsa_xxxxxxxxxxxx"
Без этого Grafana с auth gate показывает empty login page (см. test
2026-05-22 ночью: page.screenshot вернул только footer "Documentation/
Support/Community" — content area empty потому что unauthorized).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Web UI checkbox в Audio source карточке: «вывод аудио в стрим». Off →
TV получает silence + downstream audio chain isolated от live-audio RTSP
(нестабильный sidecar поток больше не блокирует video pipeline).
Изменения:
state.InstanceState: + audio_output_enabled (default True)
config.InstanceCfg:
+ audio_output_volume_target (default volume@output_audio)
+ audio_input_select_target (default astreamselect@audio_input)
dispatch.set_audio_output_enabled(enabled):
enabled=False → astreamselect map=1 (anullsrc) + volume=0
enabled=True → astreamselect map=0 (live-audio) + volume=1
Двойная команда: select decouples upstream, volume гарантирует тишину
на случай если в anullsrc что-то не так.
http_api: + GET /audio-output/{instance}, POST /audio-output/{instance}
static/index.html: + checkbox в Audio source header + loadAudioOut/toggleAudioOut
ZMQ smoke test OK. HTTP roundtrip OK.
Сопутствующий pipeline change: docker-compose.phase7.yml — amix заменён
на astreamselect@audio_input (см. localhost-infra commit).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Sync с n7.1-vf-cuda-grid-phase7 filter rework (один cuda_grid + native
runtime layout switching). Изменения:
dispatch._set_layout:
target: layout_filter_target (default cuda_grid@cg, был streamselect@layout)
command: "set_layout <name>" (был "map <index>")
validation: PREDEFINED_LAYOUTS (был inst.layout_map)
dispatch.set_main_cam:
target: main_cam_filter_target (default cuda_grid@cg, был streamselect@main_cam)
command: "cell_map <cell> <pad>" × max_cells
cell 0 = main camera; cells 1..N-1 = identity rotation остальных pads
(исключая main чтобы не дублировать в preview cells)
dispatch.set_mpp_main:
alias к set_main_cam — main_plus_preview тоже использует cell 0 как main.
config.InstanceCfg:
layout_filter_target / main_cam_filter_target / mpp_main_filter_target
default = cuda_grid@cg (Phase 7 single filter)
new max_cells: int = 4 (соответствует filter max_cells option)
layout_map deprecated — оставлен для UI subset visibility (HTTP /layouts/{inst})
http_api.set_layout:
validation против PREDEFINED_LAYOUTS вместо layout_map
Frigate bridge не меняется — set_main_cam подпись та же (instance, cam_index).
Compile-test OK. Pipeline image: gx/cuda-grid-pipeline:phase7. Controller
image rebuild требуется для deploy.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Hysteresis (FrigateBridgeCfg.auto_hysteresis_sec, default 3.0):
_update_auto_layout schedules debounced apply через asyncio.Task.
Каждый новый state event cancels pending timer (reset). Apply только
когда state стабилен N sec — short motion blips не дёргают layout.
Реверт set_mpp_main dispatch (mpp main fixed = parking):
Earlier attempt с streamselect@mpp_main + split=4 крашил pipeline
("Error while filtering: Invalid argument" — too complex filter graph).
Rolled back в infra; controller dispatch соответственно не пытается
set_mpp_main больше.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Issue: при layout switch overlays исчезали — каждый cuda_grid instance
(quad/single/mpp) имеет свой overlay state. add_overlay шёл только в один
target (cuda_grid@cg) → quad имел overlays, single/mpp без.
Fix:
InstanceCfg.overlay_filter_targets: list[str] (e.g. [cuda_grid@cg,
cuda_grid@cg_s, cuda_grid@cg_m]) — fallback к [filter_target] если empty.
Dispatcher._overlay_broadcast(cmd, arg) — sends к каждому target.
_overlay_add/remove/clear + _reload_icon now use broadcast.
Auto-layout debounce/hysteresis:
FrigateBridgeCfg.auto_hysteresis_sec (default 3.0)
_update_auto_layout schedules debounced apply через asyncio.Task;
каждый новый state event cancels pending timer (reset). Apply только
когда state стабилен N sec — short motion blips не дёргают layout.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Logic update в FrigateBridge._update_auto_layout:
0 active → quad
1 active → single, main_cam = active
2+ active → main_plus_preview, mpp_main = highest priority active
Dispatcher.set_mpp_main — ZMQ streamselect@mpp_main map <index>
Config.mpp_main_filter_target = "streamselect@mpp_main"
При каждом auto-layout change controller отправляет 3 ZMQ:
streamselect@main_cam map N (single layout main)
streamselect@mpp_main map N (mpp layout main, может быть тот же N)
streamselect@layout map L (final layout selector)
Preview cells в mpp остаются fixed mapping (cell1=cam1/front_yard, cell2=cam2/gate_lpr,
cell3=cam3/back_yard). Если main_cam = cam1/2/3 — preview slot этой cam visible duplicate.
Acceptable v2 trade-off (user warned).
Live verified: 4+ active cameras → mpp, gate_lpr в main slot (priority=10 highest).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
FrigateCameraMapping +priority +main_cam_index — для auto-layout decision.
FrigateBridgeCfg.auto_layout flag — toggle через REST.
Logic (FrigateBridge._update_auto_layout):
0 active cameras → quad (default overview)
1+ active → single, main_cam = highest priority active
Equal priority → first active wins (deterministic)
Dispatcher.set_main_cam — ZMQ streamselect@main_cam map <index>
Config.main_cam_filter_target = "streamselect@main_cam"
REST:
GET /auto-layout/{instance} — current toggle state
POST /auto-layout/{instance} — { enabled: bool }
при включении сразу применяет
UI:
+ checkbox "auto" в Layout card — toggleAuto() hits POST /auto-layout
Live verified: enable → immediately picks layout=single, main=gate_lpr
(priority=10, highest active). Visual confirms gate_lpr full screen.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
UI loadLayouts() теперь fetches /layouts/{inst} — берёт actual layout_map
из config'а (не hardcoded), показывает только existing layouts.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
snapshot_history.py — async periodic capture per instance:
interval_sec / keep_last (FIFO eviction) / dir configurable
GET /snapshots/{instance}?limit=N → list metadata
GET /snapshots/{instance}/{filename} → image bytes
Persisted в /var/lib/cuda-grid/snapshots/{instance}/<ts>.png
layout_map / layout_filter_target в InstanceCfg — для будущей runtime switch
архитектуры (через streamselect либо filter rework — выбор за Phase 7).
Текущий _set_layout dispatches к layout_filter_target c index из map.
UI:
+ Layout buttons (quad/single/main_plus_preview placeholder)
+ Snapshot history grid с thumbnails (loaded /snapshots/{inst}?limit=24)
+ "Reload history" button
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Static HTML/JS dashboard в cuda_grid_controller/static/index.html, mounted
на /ui и / FastAPI endpoints. Vanilla JS + HLS.js (CDN) для video player.
Controls:
Audio source — buttons из /audio/{instance} list, switch через POST
Intercom — Start (music↓) / End (restore)
Snapshot — opens PNG в new tab
Manual overlay — form для rect/text/dim/icon types
Chat — placeholder (info-toast про mosquitto_pub)
State — refresh каждые 2 sec, shows layout/overlays_count + raw list
Player consumes HLS на http://server:8888/live/index.m3u8 (mediamtx).
TV не нужен — браузер на любом устройстве в LAN.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
InstanceCfg.audio_zmq_endpoint (optional) — ZMQ адрес отдельного audio
sidecar ffmpeg. dispatcher._audio_set + _intercom_set теперь используют
_audio_client(inst) — separate ZMQ socket к sidecar.
Fallback: если audio_zmq_endpoint=null → реверт к video pipeline zmq
(Phase 5a single-source behaviour).
Зачем: multi-source audio chain в одном pipeline с video блокирует
video pacing (см. feedback_audio-chain-blocks-video). Split разделяет
A/V на 2 ffmpeg процесса; audio sidecar publish'нет к mediamtx, video
pipeline consume'нет audio как RTSP source + remux к combined output.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
User feedback: 4px motion border слишком жирная, к тому же много false motion
от Frigate (зоны/чувствительность будут tune'иться позже). Уменьшаем default
до 1px чтобы borders не мешали visually. Width конфигурируется (1..16).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Frigate publishes на /motion/state ("ON"/"OFF") — а bare /motion это SET-topic
для control. Subscribe pattern был неправильный → bridge не получал motion events
→ red border не загорался при motion.
Live verified после fix: real motion от parking/gate_lpr/back_yard cameras
триггерит upsert cell_<N>_border с motion theme (red #FF0000, 4px, opacity 1.0).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Permanent 1-2px рамка вокруг каждой cell для visual разделения.
При motion ON → рамка светится красным (config'и BorderTheme).
Logic:
_ensure_borders() Lazy init 4 borders (id="cell_N_border") при первом event
с idle стилем (#808080 width=2 opacity=0.4)
_set_border_state(motion=bool) Upsert тот же overlay с motion (#FF0000 width=4 opacity=1.0)
или idle styling
_cell_states set'у активных cams per cell ("inst:cell" → set(cam_names))
— border ON если хоть один cam имеет motion, OFF только когда
все cams cleared
BorderTheme:
idle_color/width/opacity subtle разделитель
motion_color/width/opacity alarm подсветка
configurable через cfg.frigate.border_theme
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Filter использует sscanf("%s") который stops on whitespace — нужно
URL-encode string values (text="hello world" → text=hello%20world).
Filter inline decode'ит %xx.
Также:
tools/smoke_test_overlays.sh — integration test script (manual run
утром когда GPU свободна; сейчас прод-сервисы заняли всю VRAM)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>