a1090a5f4c
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).
145 lines
6.1 KiB
Python
145 lines
6.1 KiB
Python
"""Overlay primitives — 7 типов через pydantic discriminated union.
|
||
|
||
Все координаты **normalized** (0.0–1.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"),
|
||
]
|