Introduce ColorStripSource as first-class entity

Extracts color processing and calibration out of WledPictureTarget into a
new PictureColorStripSource entity, enabling multiple LED targets to share
one capture/processing pipeline.

New entities & processing:
- storage/color_strip_source.py: ColorStripSource + PictureColorStripSource models
- storage/color_strip_store.py: JSON-backed CRUD store (prefix css_)
- core/processing/color_strip_stream.py: ColorStripStream ABC + PictureColorStripStream (runs border-extract → map → smooth → brightness/sat/gamma in background thread)
- core/processing/color_strip_stream_manager.py: ref-counted shared stream manager

Modified storage/processing:
- WledPictureTarget simplified to device_id + color_strip_source_id + standby_interval + state_check_interval
- Device model: calibration field removed
- WledTargetProcessor: acquires ColorStripStream from manager instead of running its own pipeline
- ProcessorManager: wires ColorStripStreamManager into TargetContext

API layer:
- New routes: GET/POST/PUT/DELETE /api/v1/color-strip-sources, PUT calibration/test
- Removed calibration endpoints from /devices
- Updated /picture-targets CRUD for new target structure

Frontend:
- New color-strips.js module with CSS editor modal and card rendering
- Calibration modal extended with CSS mode (css-id hidden field + device picker)
- targets.js: Color Strip Sources section added to LED tab; target editor/card updated
- app.js: imports and window globals for CSS + showCSSCalibration
- en.json / ru.json: color_strip.* and targets.section.color_strips keys added

Data migration runs at startup: existing WledPictureTargets are converted to
reference a new PictureColorStripSource created from their old settings.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-20 15:49:47 +03:00
parent c4e0257389
commit 7de3546b14
33 changed files with 2325 additions and 814 deletions

View File

@@ -1,26 +1,11 @@
"""Picture target schemas (CRUD, processing state, settings, metrics)."""
"""Picture target schemas (CRUD, processing state, metrics)."""
from datetime import datetime
from typing import Dict, List, Literal, Optional
from typing import Dict, List, Optional
from pydantic import BaseModel, Field
from wled_controller.core.processing.processing_settings import DEFAULT_STATE_CHECK_INTERVAL
class ProcessingSettings(BaseModel):
"""Processing settings for a picture target."""
display_index: int = Field(default=0, description="Display to capture", ge=0)
fps: int = Field(default=30, description="Target frames per second", ge=10, le=90)
interpolation_mode: str = Field(default="average", description="LED color interpolation mode (average, median, dominant)")
brightness: float = Field(default=1.0, description="Global brightness (0.0-1.0)", ge=0.0, le=1.0)
smoothing: float = Field(default=0.3, description="Temporal smoothing factor (0.0=none, 1.0=full)", ge=0.0, le=1.0)
standby_interval: float = Field(default=1.0, description="Seconds between keepalive sends when screen is static (0.5-5.0)", ge=0.5, le=5.0)
state_check_interval: int = Field(
default=DEFAULT_STATE_CHECK_INTERVAL, ge=5, le=600,
description="Seconds between WLED health checks"
)
DEFAULT_STATE_CHECK_INTERVAL = 30 # seconds between health checks
class KeyColorRectangleSchema(BaseModel):
@@ -65,9 +50,13 @@ class PictureTargetCreate(BaseModel):
name: str = Field(description="Target name", min_length=1, max_length=100)
target_type: str = Field(default="led", description="Target type (led, key_colors)")
# LED target fields
device_id: str = Field(default="", description="LED device ID")
picture_source_id: str = Field(default="", description="Picture source ID")
settings: Optional[ProcessingSettings] = Field(None, description="Processing settings (for wled targets)")
color_strip_source_id: str = Field(default="", description="Color strip source ID")
standby_interval: float = Field(default=1.0, description="Keepalive send interval when screen is static (0.5-5.0s)", ge=0.5, le=5.0)
state_check_interval: int = Field(default=DEFAULT_STATE_CHECK_INTERVAL, description="Device health check interval (5-600s)", ge=5, le=600)
# KC target fields
picture_source_id: str = Field(default="", description="Picture source ID (for key_colors 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)
@@ -76,9 +65,13 @@ class PictureTargetUpdate(BaseModel):
"""Request to update a picture target."""
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 (for wled targets)")
# LED target fields
device_id: Optional[str] = Field(None, description="LED device ID")
color_strip_source_id: Optional[str] = Field(None, description="Color strip source ID")
standby_interval: Optional[float] = Field(None, description="Keepalive interval (0.5-5.0s)", ge=0.5, le=5.0)
state_check_interval: Optional[int] = Field(None, description="Health check interval (5-600s)", ge=5, le=600)
# KC target fields
picture_source_id: Optional[str] = Field(None, description="Picture source ID (for key_colors 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)
@@ -89,10 +82,14 @@ class PictureTargetResponse(BaseModel):
id: str = Field(description="Target ID")
name: str = Field(description="Target name")
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: Optional[ProcessingSettings] = Field(None, description="Processing settings (wled)")
key_colors_settings: Optional[KeyColorsSettingsSchema] = Field(None, description="Key colors settings (key_colors)")
# LED target fields
device_id: str = Field(default="", description="LED device ID")
color_strip_source_id: str = Field(default="", description="Color strip source ID")
standby_interval: float = Field(default=1.0, description="Keepalive interval (s)")
state_check_interval: int = Field(default=DEFAULT_STATE_CHECK_INTERVAL, description="Health check interval (s)")
# KC target fields
picture_source_id: str = Field(default="", description="Picture source ID (key_colors)")
key_colors_settings: Optional[KeyColorsSettingsSchema] = Field(None, description="Key colors settings")
description: Optional[str] = Field(None, description="Description")
created_at: datetime = Field(description="Creation timestamp")
updated_at: datetime = Field(description="Last update timestamp")
@@ -110,21 +107,18 @@ class TargetProcessingState(BaseModel):
target_id: str = Field(description="Target ID")
device_id: Optional[str] = Field(None, description="Device ID")
color_strip_source_id: Optional[str] = Field(None, description="Color strip source ID")
processing: bool = Field(description="Whether processing is active")
fps_actual: Optional[float] = Field(None, description="Actual FPS achieved")
fps_potential: Optional[float] = Field(None, description="Potential FPS (processing speed without throttle)")
fps_target: int = Field(default=0, description="Target FPS")
fps_target: Optional[int] = Field(None, description="Target FPS")
frames_skipped: Optional[int] = Field(None, description="Frames skipped (no screen change)")
frames_keepalive: Optional[int] = Field(None, description="Keepalive frames sent during standby")
fps_current: Optional[int] = Field(None, description="Frames sent in the last second")
timing_extract_ms: Optional[float] = Field(None, description="Border extraction time (ms)")
timing_map_leds_ms: Optional[float] = Field(None, description="LED mapping time (ms)")
timing_smooth_ms: Optional[float] = Field(None, description="Smoothing time (ms)")
timing_send_ms: Optional[float] = Field(None, description="DDP send time (ms)")
timing_total_ms: Optional[float] = Field(None, description="Total processing time (ms)")
timing_calc_colors_ms: Optional[float] = Field(None, description="Color calculation time (ms, KC targets)")
timing_broadcast_ms: Optional[float] = Field(None, description="WebSocket broadcast time (ms, KC targets)")
display_index: int = Field(default=0, description="Current display index")
display_index: Optional[int] = Field(None, description="Current display index")
overlay_active: bool = Field(default=False, description="Whether visualization overlay is active")
last_update: Optional[datetime] = Field(None, description="Last successful update")
errors: List[str] = Field(default_factory=list, description="Recent errors")
@@ -147,7 +141,7 @@ class TargetMetricsResponse(BaseModel):
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")
fps_target: Optional[int] = Field(None, description="Target FPS")
uptime_seconds: float = Field(description="Processing uptime in seconds")
frames_processed: int = Field(description="Total frames processed")
errors_count: int = Field(description="Total error count")