Files
vf-cuda-grid/controller/cuda_grid_controller/placeholder_renderer.py
T
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

80 lines
2.9 KiB
Python
Raw 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.
"""Placeholder PNG generator — рендерит per-cell offline placeholder
("CAMERA LABEL — НЕТ СИГНАЛА") при controller startup.
Filter cuda_grid looks up "<placeholder_icon>_<pad>.png" first (per-cell),
fallback к "<placeholder_icon>.png" generic. Этот module генерирует
per-cell PNG с camera label из FrigateBridge config — user меняет label
в yaml, controller restarts, PNGs регенерируются.
Поддерживает UTF-8 (Russian text) через PIL freetype — недоступно в filter's
basic FT_Get_Char_Index path (single-byte only).
"""
from __future__ import annotations
from pathlib import Path
import structlog
from PIL import Image, ImageDraw, ImageFont
log = structlog.get_logger()
_FONT_PATH = "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf"
def _font(size: int) -> ImageFont.FreeTypeFont | ImageFont.ImageFont:
try:
return ImageFont.truetype(_FONT_PATH, size=size)
except OSError:
return ImageFont.load_default()
def render_placeholder(
label: str,
width: int = 480,
height: int = 120,
) -> Image.Image:
"""2-line transparent PNG: camera label (top, yellow) + 'НЕТ СИГНАЛА' (bottom, gray).
Transparent bg — filter заполняет cell black, потом blit'ит этот PNG поверх."""
img = Image.new("RGBA", (width, height), (0, 0, 0, 0))
draw = ImageDraw.Draw(img)
label_font = _font(36)
sub_font = _font(28)
bbox_l = draw.textbbox((0, 0), label, font=label_font)
lw, lh = bbox_l[2] - bbox_l[0], bbox_l[3] - bbox_l[1]
label_y = height // 2 - lh - 4
draw.text(((width - lw) // 2, label_y),
label, fill=(255, 230, 100, 255), font=label_font)
sub_text = "НЕТ СИГНАЛА"
bbox_s = draw.textbbox((0, 0), sub_text, font=sub_font)
sw, sh = bbox_s[2] - bbox_s[0], bbox_s[3] - bbox_s[1]
sub_y = height // 2 + 4
draw.text(((width - sw) // 2, sub_y),
sub_text, fill=(220, 220, 220, 230), font=sub_font)
return img
def generate_placeholders(icon_dir: Path, labels: dict[int, str],
base_name: str = "offline") -> None:
"""Generates <base>_<pad>.png для each (pad → label) entry.
Also generic <base>.png (fallback когда pad index без custom label)."""
icon_dir.mkdir(parents=True, exist_ok=True)
for pad, label in labels.items():
img = render_placeholder(label)
path = icon_dir / f"{base_name}_{pad}.png"
tmp = path.with_suffix(".png.tmp")
img.save(tmp, "PNG")
tmp.replace(path)
log.info("placeholder.rendered", pad=pad, label=label, path=str(path))
img = render_placeholder("СИГНАЛ ПОТЕРЯН")
path = icon_dir / f"{base_name}.png"
tmp = path.with_suffix(".png.tmp")
img.save(tmp, "PNG")
tmp.replace(path)
log.info("placeholder.rendered_generic", path=str(path))