13 Commits

Author SHA1 Message Date
gx f5ea2e3005 controller: placeholder_renderer — dynamic PNG generation для per-cell labels
При 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>
2026-05-25 12:01:06 +01:00
gx 6081e33e5a controller: PipelineMonitor — auto-restore overlay state после pipeline restart
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>
2026-05-24 07:35:27 +01:00
gx 976aed52e9 controller: FrigateBridge bbox throttling — rate-limit + delta filter
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>
2026-05-22 09:38:00 +01:00
gx 4cd2b4ba8f controller: auto-layout debounce (hysteresis) + revert mpp_main dispatch
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>
2026-05-21 07:19:43 +01:00
gx 48b24a04dd controller: overlay broadcast ко всем cuda_grid + auto_hysteresis_sec
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>
2026-05-21 06:54:04 +01:00
gx 9080004d48 controller: auto-layout v2 — mpp при 2+ active с dynamic main
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>
2026-05-21 06:41:11 +01:00
gx a7b1d9b1d9 controller: auto-layout selector (motion + priority)
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>
2026-05-21 06:31:27 +01:00
gx d807cd2c23 controller: Phase 5b+5c+6 — multi-audio + intercom ducking + dynamic overlays
5b — audio source switching:
  AudioSourceCfg list + audio_filter_target в InstanceCfg
  CommandDispatcher._audio_set → ZMQ astreamselect@as map <index>
  REST: GET /audio/{inst}, POST /audio/{inst}/set
  MQTT: cuda_grid/cmd/<inst>/audio/set <source_name>

5c — intercom ducking:
  music_volume_target / intercom_volume_target / music_ducked_volume в InstanceCfg
  CommandDispatcher._intercom_set → 2× ZMQ volume@music/@intercom commands
  REST: POST /intercom/{inst}/start (music↓ + intercom↑) + /end (restore)
  MQTT: cuda_grid/cmd/<inst>/intercom/start|end

6 — dynamic overlays (charts/chats):
  dynamic_overlays.py: ChartCfg/ChatCfg + DynamicRenderer
  PIL rendering: line chart + scrolling text list
  Async loops пишут PNG в icon_dir + invalidate filter cache via reload_icon ZMQ
  MQTT subscriptions для real data (charts: numeric topic, chats: text topic)
  Demo: chart sine wave если data_topic=null
  Wired в __main__.py + mqtt_loop dispatch

+ ZMQ client asyncio.Lock — REQ socket strict send/recv pattern требует
  serialize requests (overlay/audio/intercom concurrent ломали "Operation
  cannot be accomplished in current state")
+ Pillow в Dockerfile (для PIL render)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 21:55:33 +01:00
gx a576b4ec51 controller: border defaults → 1px (idle и motion)
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>
2026-05-20 20:54:09 +01:00
gx e877a254ce controller: fix MQTT topic для motion — frigate/<cam>/motion/state (не /motion)
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>
2026-05-20 20:50:20 +01:00
gx 26e9f30990 controller: FrigateBridge cell borders с idle/motion state machine
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>
2026-05-20 20:43:34 +01:00
gx 96e6048b64 controller: Phase 4b end-to-end working — wire format fix + FrigateBridge auto-overlay
Fixed pipeline (live verified с ffmpeg-vf-cuda-grid:phase4b-icon):

zmq_client.send_command — wrap args в single-quotes
  FFmpeg's zmq filter parses через av_get_token (libavfilter/f_zmq.c) — берёт
  ОДИН token как arg. Без quotes filter получает только id, parse fail.

dispatch._serialize_overlay_to_zmq — translation pydantic → filter wire:
  color "#RRGGBB"     → r=N g=N b=N
  opacity 0..1.0      → opacity 0..255
  border_only + width → thickness (0=filled, иначе width)
  dim_factor 0..1.0   → amount 0..255
  text/icon_name      → URL-encoded (%20 для space)

frigate_bridge — Phase 4b auto-rendering:
  motion ON       → add RectOverlay (orange border весь cell)
  motion OFF      → remove
  event new/update → add RectOverlay (bbox) + TextOverlay (label + score%)
  event end       → remove оба
  Bbox px → normalized через configurable camera_width/height (default 1920x1080).
  Опциональные flags motion_indicator/bbox_overlay в mapping.
  Constructor принимает dispatcher для dispatch.handle("overlay.add"/"overlay.remove").

mqtt_loop._handle_message — await frigate_bridge.handle_message (теперь async).
__main__.py — передаёт dispatcher в FrigateBridge constructor.

Verified end-to-end через test pipeline:
  pydantic Overlay → _serialize_overlay_to_zmq → send_command quoted →
  ZMQ → filter parse_overlay_args → CUDA kernel render
4× add_overlay (rect border, text, dim, rect filled) — все 4 visible в output frame.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 19:19:47 +01:00
gx a1090a5f4c controller: Phase 4a — overlay infrastructure (data models + API + Frigate bridge skeleton)
Phase 4a deliverable (no filter rendering yet — это Phase 4b).
End-to-end pipeline: HA/HTTP/MQTT → controller → ZMQ → FFmpeg (logged).

Modules:
- overlays.py — 7 discriminated union types через pydantic:
  rect, text, icon, image, dim, graph, chat. Normalized coords (0.0-1.0),
  optional cell binding, z_order, opacity, visible.
- state.py — overlay storage per instance (CRUD: add/remove/update/get/clear)
- dispatch.py — overlay.add/remove/clear actions:
  - parses JSON payload в Overlay через TypeAdapter
  - serializes to ZMQ string: "<id> <type> <full-json>"
  - sends via FFmpeg process_command (filter will парсить в Phase 4b)
  - updates state + publishes events (overlay_added, overlay_removed, overlays_cleared)
- http_api.py — REST endpoints:
  - POST /overlay/{inst}/add (body = Overlay JSON, returns id)
  - GET /overlay/{inst} — list all
  - DELETE /overlay/{inst}/{id} — single
  - DELETE /overlay/{inst} — clear all
  - PATCH /overlay/{inst}/{id} — update
- mqtt_loop.py — already subscribes cuda_grid/cmd/<inst>/+/+; teper handles
  overlay/add (JSON payload), overlay/remove (id), overlay/clear
- frigate_bridge.py — FrigateBridge skeleton:
  - subscribe frigate/+/motion + frigate/events
  - mapping camera_name → target_instance + cell index
  - Phase 4a: log received events (rendering в Phase 4b)
- config.py — frigate: optional section
- examples/controller.yaml — frigate mappings для 4 наших камер

State management:
- ControllerState.add/remove/update/get/clear_overlay (asyncio.Lock guarded)
- InstanceState.overlays: dict[str, Overlay]
- IDs generated via uuid4()[:8]

Phase 4a limitations:
- Filter side ничего не рендерит (just logs ZMQ commands)
- Frigate bridge принимает events но не auto-generates overlays
- HA Discovery не имеет overlay-specific entities (overlays через REST API)

Phase 4b: filter-side AVFrame side data + CUDA kernels (rect first, NPP-based,
потом text via freetype atlas, потом icon sprite blit).
2026-05-19 22:03:20 +01:00