Files
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

145 lines
6.1 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Overlay primitives — 7 типов через pydantic discriminated union.
Все координаты **normalized** (0.01.0 относительно cell или output frame).
Color — hex RGB string + alpha как float.
Phase 4a: data models + state. Rendering — Phase 4b (filter-side CUDA kernels).
"""
from __future__ import annotations
import uuid
from typing import Annotated, Literal, Union
from pydantic import BaseModel, Field
# ─── Common ──────────────────────────────────────────────────────────────
class OverlayBase(BaseModel):
"""Общие поля всех overlay'ев."""
id: str = Field(default_factory=lambda: str(uuid.uuid4())[:8])
cell: int | None = Field(
default=None,
description="Привязка к cell layout (0..N-1). None = относительно "
"всего output frame.",
)
z_order: int = Field(default=0, description="Higher = поверх. Default 0.")
opacity: float = Field(default=1.0, ge=0.0, le=1.0)
visible: bool = True
# ─── Rect ────────────────────────────────────────────────────────────────
class RectOverlay(OverlayBase):
type: Literal["rect"] = "rect"
x: float = Field(ge=0.0, le=1.0)
y: float = Field(ge=0.0, le=1.0)
w: float = Field(gt=0.0, le=1.0)
h: float = Field(gt=0.0, le=1.0)
color: str = Field(default="#FF0000", description="HEX RGB e.g. #FF0000")
border_only: bool = Field(default=False, description="Если true — только рамка")
border_width: int = Field(default=2, ge=1, le=64)
# ─── Text ────────────────────────────────────────────────────────────────
class TextOverlay(OverlayBase):
type: Literal["text"] = "text"
x: float = Field(ge=0.0, le=1.0)
y: float = Field(ge=0.0, le=1.0)
text: str
font_size: int = Field(default=24, ge=8, le=256)
color: str = "#FFFFFF"
bg_color: str | None = Field(
default="#000000", description="Background — None = transparent"
)
bg_opacity: float = Field(default=0.5, ge=0.0, le=1.0)
# ─── Icon ────────────────────────────────────────────────────────────────
class IconOverlay(OverlayBase):
type: Literal["icon"] = "icon"
name: str = Field(description="Имя из preloaded sprite sheet, e.g. 'warning', 'person'")
x: float = Field(ge=0.0, le=1.0)
y: float = Field(ge=0.0, le=1.0)
size: float = Field(default=0.05, gt=0.0, le=1.0, description="Размер относительно frame")
tint: str | None = Field(default=None, description="HEX RGB — None = original color")
# ─── Image (любой PNG/JPG как texture) ──────────────────────────────────
class ImageOverlay(OverlayBase):
type: Literal["image"] = "image"
url: str = Field(description="file://path или http://... PNG/JPG")
x: float = Field(ge=0.0, le=1.0)
y: float = Field(ge=0.0, le=1.0)
w: float = Field(gt=0.0, le=1.0)
h: float = Field(gt=0.0, le=1.0)
# ─── Dim / privacy mask ──────────────────────────────────────────────────
class DimOverlay(OverlayBase):
"""Затемнение области — используется как privacy mask либо out-of-zone dim."""
type: Literal["dim"] = "dim"
x: float = Field(ge=0.0, le=1.0)
y: float = Field(ge=0.0, le=1.0)
w: float = Field(gt=0.0, le=1.0)
h: float = Field(gt=0.0, le=1.0)
color: str = "#000000"
dim_factor: float = Field(default=0.8, ge=0.0, le=1.0, description="0=без затемнения, 1=полное")
# ─── Graph / chart ───────────────────────────────────────────────────────
class GraphOverlay(OverlayBase):
"""Live chart — controller рендерит CPU-side (Cairo) и uploads texture."""
type: Literal["graph"] = "graph"
x: float = Field(ge=0.0, le=1.0)
y: float = Field(ge=0.0, le=1.0)
w: float = Field(gt=0.0, le=1.0)
h: float = Field(gt=0.0, le=1.0)
data_topic: str = Field(description="MQTT topic с time-series данными")
chart_type: Literal["line", "bar", "histogram"] = "line"
style: dict = Field(default_factory=dict, description="Цвета, axis, тp.")
refresh_hz: float = Field(default=1.0, gt=0.0, le=10.0)
# ─── Chat / scrolling text ───────────────────────────────────────────────
class ChatOverlay(OverlayBase):
"""Scrolling text — notifications, alerts, etc."""
type: Literal["chat"] = "chat"
x: float = Field(ge=0.0, le=1.0)
y: float = Field(ge=0.0, le=1.0)
w: float = Field(gt=0.0, le=1.0)
h: float = Field(gt=0.0, le=1.0)
source_topic: str = Field(description="MQTT topic для новых сообщений (newline-separated)")
font_size: int = Field(default=20, ge=8, le=128)
color: str = "#FFFFFF"
bg_opacity: float = Field(default=0.6, ge=0.0, le=1.0)
max_messages: int = Field(default=10, ge=1, le=100)
scroll_speed_px_s: int = Field(default=30, ge=0, le=1000)
# ─── Discriminated union ─────────────────────────────────────────────────
Overlay = Annotated[
Union[
RectOverlay,
TextOverlay,
IconOverlay,
ImageOverlay,
DimOverlay,
GraphOverlay,
ChatOverlay,
],
Field(discriminator="type"),
]