Add Key Colors target type for extracting colors from screen regions

Introduce a new "key_colors" target type alongside WLED targets, enabling
real-time color extraction from configurable screen rectangles with
average/median/dominant modes, temporal smoothing, and WebSocket streaming.

- Split WledPictureTarget into its own module, add KeyColorsPictureTarget
- Add KC target lifecycle to ProcessorManager (register, start/stop, processing loop)
- Extend API routes and schemas for KC targets (CRUD, settings, state, metrics, colors)
- Add WebSocket endpoint for real-time color updates with auth
- Add KC sub-tab in Targets UI with editor modal and live color swatches
- Add EN and RU translations for all key colors strings

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-12 16:43:09 +03:00
parent 3d2393e474
commit 5f9bc9a37e
13 changed files with 1525 additions and 111 deletions

View File

@@ -35,14 +35,51 @@ class ProcessingSettings(BaseModel):
)
class KeyColorRectangleSchema(BaseModel):
"""A named rectangle for key color extraction (relative coords 0.0-1.0)."""
name: str = Field(description="Rectangle name", min_length=1, max_length=50)
x: float = Field(default=0.0, description="Left edge (0.0-1.0)", ge=0.0, le=1.0)
y: float = Field(default=0.0, description="Top edge (0.0-1.0)", ge=0.0, le=1.0)
width: float = Field(default=1.0, description="Width (0.0-1.0)", gt=0.0, le=1.0)
height: float = Field(default=1.0, description="Height (0.0-1.0)", gt=0.0, le=1.0)
class KeyColorsSettingsSchema(BaseModel):
"""Settings for key colors extraction."""
fps: int = Field(default=10, description="Extraction rate (1-60)", ge=1, le=60)
interpolation_mode: str = Field(default="average", description="Color mode (average, median, dominant)")
smoothing: float = Field(default=0.3, description="Temporal smoothing (0.0-1.0)", ge=0.0, le=1.0)
rectangles: List[KeyColorRectangleSchema] = Field(default_factory=list, description="Rectangles to extract colors from")
class ExtractedColorResponse(BaseModel):
"""A single extracted color."""
r: int = Field(description="Red (0-255)")
g: int = Field(description="Green (0-255)")
b: int = Field(description="Blue (0-255)")
hex: str = Field(description="Hex color (#rrggbb)")
class KeyColorsResponse(BaseModel):
"""Extracted key colors for a target."""
target_id: str = Field(description="Target ID")
colors: Dict[str, ExtractedColorResponse] = Field(description="Rectangle name -> color")
timestamp: Optional[datetime] = Field(None, description="Extraction timestamp")
class PictureTargetCreate(BaseModel):
"""Request to create a picture target."""
name: str = Field(description="Target name", min_length=1, max_length=100)
target_type: str = Field(default="wled", description="Target type (wled)")
target_type: str = Field(default="wled", description="Target type (wled, key_colors)")
device_id: str = Field(default="", description="WLED device ID")
picture_source_id: str = Field(default="", description="Picture source ID")
settings: Optional[ProcessingSettings] = Field(None, description="Processing settings")
settings: Optional[ProcessingSettings] = Field(None, description="Processing settings (for wled targets)")
key_colors_settings: Optional[KeyColorsSettingsSchema] = Field(None, description="Key colors settings (for key_colors targets)")
description: Optional[str] = Field(None, description="Optional description", max_length=500)
@@ -52,7 +89,8 @@ class PictureTargetUpdate(BaseModel):
name: Optional[str] = Field(None, description="Target name", min_length=1, max_length=100)
device_id: Optional[str] = Field(None, description="WLED device ID")
picture_source_id: Optional[str] = Field(None, description="Picture source ID")
settings: Optional[ProcessingSettings] = Field(None, description="Processing settings")
settings: Optional[ProcessingSettings] = Field(None, description="Processing settings (for wled targets)")
key_colors_settings: Optional[KeyColorsSettingsSchema] = Field(None, description="Key colors settings (for key_colors targets)")
description: Optional[str] = Field(None, description="Optional description", max_length=500)
@@ -64,7 +102,8 @@ class PictureTargetResponse(BaseModel):
target_type: str = Field(description="Target type")
device_id: str = Field(default="", description="WLED device ID")
picture_source_id: str = Field(default="", description="Picture source ID")
settings: ProcessingSettings = Field(description="Processing settings")
settings: Optional[ProcessingSettings] = Field(None, description="Processing settings (wled)")
key_colors_settings: Optional[KeyColorsSettingsSchema] = Field(None, description="Key colors settings (key_colors)")
description: Optional[str] = Field(None, description="Description")
created_at: datetime = Field(description="Creation timestamp")
updated_at: datetime = Field(description="Last update timestamp")
@@ -81,11 +120,11 @@ class TargetProcessingState(BaseModel):
"""Processing state for a picture target."""
target_id: str = Field(description="Target ID")
device_id: str = Field(description="Device ID")
device_id: Optional[str] = Field(None, description="Device ID")
processing: bool = Field(description="Whether processing is active")
fps_actual: Optional[float] = Field(None, description="Actual FPS achieved")
fps_target: int = Field(description="Target FPS")
display_index: int = Field(description="Current display index")
fps_target: int = Field(default=0, description="Target FPS")
display_index: int = Field(default=0, description="Current display index")
last_update: Optional[datetime] = Field(None, description="Last successful update")
errors: List[str] = Field(default_factory=list, description="Recent errors")
wled_online: bool = Field(default=False, description="Whether WLED device is reachable")
@@ -103,7 +142,7 @@ class TargetMetricsResponse(BaseModel):
"""Target metrics response."""
target_id: str = Field(description="Target ID")
device_id: str = Field(description="Device ID")
device_id: Optional[str] = Field(None, description="Device ID")
processing: bool = Field(description="Whether processing is active")
fps_actual: Optional[float] = Field(None, description="Actual FPS")
fps_target: int = Field(description="Target FPS")