Commit Graph

28 Commits

Author SHA1 Message Date
gx e773066867 controller: browser_overlays + cookies field (Playwright session auth)
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>
2026-05-22 09:33:50 +01:00
gx 450cee3556 controller: ${VAR} env interpolation в YAML config (для secrets)
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>
2026-05-22 05:46:03 +01:00
gx c287caf7c1 controller: browser_overlays — resize screenshot к target w_px×h_px
Без 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>
2026-05-22 05:38:45 +01:00
gx fc40b5c402 controller: PREDEFINED_LAYOUTS += main_with_strip
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>
2026-05-21 23:35:06 +01:00
gx bd9dcd2441 controller: browser_overlays + extra_http_headers — auth для protected dashboards
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>
2026-05-21 23:22:24 +01:00
gx 19ddaf2dde controller: browser-rendered overlays — Grafana/chat/любой HTML
MVP Phase 8 feature: headless Chromium snapshot URL → PNG → existing icon
overlay infrastructure. Use case — Grafana dashboard, web chat, любой
HTML widget с transparent background поверх video composite.

Архитектура:
  BrowserRenderer launches один shared Chromium instance, per-dashboard
  page. Loop:
    page.reload (для свежих данных — Grafana auto-refresh не triggers
                 в headless mode без user interaction)
    page.add_style_tag (re-inject transparent CSS — reload teryает styles)
    page.wait_for_selector (если selector задан)
    page.screenshot(omit_background=True) или locator(selector).screenshot
    save PNG к icon_dir
    dispatcher._reload_icon → filter re-reads atlas

Config (dynamic_overlays.dashboards в controller.yaml):
  id, target_instance, cell, url, x, y, w_px, h_px,
  refresh_sec (default 2.0s, min 0.5s — frequent reload bad см. stutter memory),
  inject_css (default — transparent background + zero margins),
  selector (optional CSS selector — экспортить только конкретный element),
  viewport_w/h (override page viewport, default = w_px/h_px;
                полезно при selector чтобы page layout не collapsed),
  wait_until, page_timeout_ms, opacity, z_order.

Dockerfile:
  + pip install playwright
  + playwright install --with-deps chromium (~170MB Chromium + ~150MB system libs)

Если dashboards не нужны — можно убрать оба install'а (playwright
import lazy, BrowserRenderer.start() graceful no-op без playwright).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 23:09:50 +01:00
gx 48eb62bddc controller: UI-toggle audio output + astreamselect decouple (Phase 7 Вариант 2)
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>
2026-05-21 20:44:50 +01:00
gx e2764160b6 controller: Phase 7 dispatch — set_layout + cell_map (вместо streamselect)
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>
2026-05-21 20:11:04 +01:00
gx 155038aabb controller: stream watchdog (Phase 1 resilience, issue #3)
StreamWatchdog (watchdog.py) — polls mediamtx /v3/paths/list каждые N sec.
Если ожидаемый path missing > threshold → emit MQTT event stream_lost +
показывает text overlay 'OFFLINE'. При восстановлении — stream_restored +
remove overlay.

Config:
  watchdog:
    enabled: true
    mediamtx_api_url: http://cuda-grid-mediamtx:9997
    poll_interval_sec: 5.0
    lost_threshold_sec: 15.0
    paths:
      - mediamtx_path: live-audio
        instance: tv_grid
        label: Audio
        overlay_when_lost: true

httpx добавлен в Dockerfile.

Сегодняшний incident (audio sidecar потерял connection с mediamtx →
pipeline restart loop) — watchdog обнаружит missing live-audio через
15 sec + покажет TV-side warning. Manual restart audio sidecar still
needed (watchdog auto-restart — Phase 2).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 10:07:12 +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 c9c5b93ef8 controller: GET /layouts/{instance} + UI fetch dynamic layout list
UI loadLayouts() теперь fetches /layouts/{inst} — берёт actual layout_map
из config'а (не hardcoded), показывает только existing layouts.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 06:04:48 +01:00
gx d7b3e34c6b controller: snapshot history + layout_map + UI grid
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>
2026-05-21 05:45:11 +01:00
gx d90c139dce controller: Phase 6+ — Web UI mini dashboard
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>
2026-05-21 05:27:27 +01:00
gx d8674e599d controller: Phase 5d — audio_zmq_endpoint для split-process architecture
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>
2026-05-20 23:15:13 +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 c396a47f4a controller: switch wire format JSON → key=val URL-encoded (matches filter)
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>
2026-05-19 22:43:35 +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
gx 37232ae1b9 controller: Phase 3 — Python sidecar skeleton (MQTT + ZMQ + HTTP + HA Discovery)
cuda-grid-controller (Python 3.11+) — control plane между HA/MQTT/HTTP
и FFmpeg's vf_cuda_grid filter через ZMQ.

Modules (~700 LOC Python):
- config.py — Pydantic schema (broker, instances[], ha_discovery, http, log) + YAML loader
- layouts.py — registry известных layouts (sync с vf_cuda_grid.c Phase 2)
- ha_discovery.py — HA MQTT Discovery payloads (select.layout, sensor.current_layout,
  binary_sensor.online per instance + global device entry)
- zmq_client.py — async ZMQ REQ socket к FFmpeg zmq filter
  (target command args → reply parsing)
- state.py — in-memory ControllerState (active_layout per instance, asyncio.Lock)
- mqtt_loop.py — aiomqtt async loop: subscribe cuda_grid/cmd/<inst>/+/+,
  publish cuda_grid/state/* (retained) + cuda_grid/event/*, LWT, HA status reconnect
- dispatch.py — CommandDispatcher: layout.set action → ZMQ send_command + state update + events
- http_api.py — FastAPI: /health, /layouts, /state, POST /layout/{inst}/set
- __main__.py — typer CLI, asyncio.gather(mqtt_loop, uvicorn.server)

Examples + Dockerfile:
- examples/controller.yaml — 2 instances (livingroom_tv, public_stream)
- Dockerfile — python:3.11-slim, ENTRYPOINT cuda-grid-controller
- README — overview, usage, FFmpeg side filter graph

End-to-end flow ready:
  HA dashboard → MQTT → controller → ZMQ → FFmpeg process_command → layout switch
  ↓
  state публикуется обратно в MQTT → HA UI обновляется

Phase 3 deliverable per gx/vf-cuda-grid#1. Phase 4 = overlays (rect/text/icon).
2026-05-19 21:52:11 +01:00
gx 06be41d245 readme: project overview + architecture diagram + phase table 2026-05-19 20:37:41 +01:00
gx 8a6afa53b3 initial: README + design document (architect-reviewed)
Design document (1124 строки) от ai-systems-architect — покрывает:
- High-level architecture (filter + sidecar + protocols)
- Component design + CUDA composition algorithm
- Layout DSL + dynamic creation
- Overlay system (7 types — rect/text/icon/image/dim/graph/chat)
- Control plane (ZMQ/MQTT/HTTP/HA Discovery, commands IN + events OUT)
- Audio orchestration (domofon ducking use case)
- Multi-instance behaviour (shared inputs, per-screen layout)
- Library choice — Python (FastAPI + asyncio)
- 6 phases implementation plan
- Migration path для cctv-processor (closes gx/cctv#22 Phase 4)
- Overlap analysis с gx/cctv#24 (superseded by cuda-grid-controller)

README — short описание + use cases + architecture diagram + phase table.

Implementation начнётся после ratification design'а и Phase 1 issue.
2026-05-19 20:36:47 +01:00
gx bf51325ca0 Initial commit 2026-05-19 20:35:31 +01:00