Add Picture Streams architecture with postprocessing templates and stream test UI

Introduce Picture Stream abstraction that separates the capture pipeline into
composable layers: raw streams (display + capture engine + FPS) and processed
streams (source stream + postprocessing template). Devices reference a picture
stream instead of managing individual capture settings.

- Add PictureStream and PostprocessingTemplate data models and stores
- Add CRUD API endpoints for picture streams and postprocessing templates
- Add stream chain resolution in ProcessorManager for start_processing
- Add picture stream test endpoint with postprocessing preview support
- Add Stream Settings modal with border_width and interpolation_mode controls
- Add stream test modal with capture preview and performance metrics
- Add full frontend: Picture Streams tab, Processing Templates tab, stream
  selector on device cards, test buttons on stream cards
- Add localization keys for all new features (en, ru)
- Migrate existing devices to picture streams on startup

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-11 00:00:30 +03:00
parent 3db7ba4b0e
commit 493f14fba9
23 changed files with 2773 additions and 200 deletions

View File

@@ -30,7 +30,8 @@ class Device:
enabled: bool = True,
settings: Optional[ProcessingSettings] = None,
calibration: Optional[CalibrationConfig] = None,
capture_template_id: str = "tpl_mss_default",
capture_template_id: str = "",
picture_stream_id: str = "",
created_at: Optional[datetime] = None,
updated_at: Optional[datetime] = None,
):
@@ -44,7 +45,8 @@ class Device:
enabled: Whether device is enabled
settings: Processing settings
calibration: Calibration configuration
capture_template_id: ID of assigned capture template
capture_template_id: ID of assigned capture template (legacy, use picture_stream_id)
picture_stream_id: ID of assigned picture stream
created_at: Creation timestamp
updated_at: Last update timestamp
"""
@@ -56,6 +58,7 @@ class Device:
self.settings = settings or ProcessingSettings()
self.calibration = calibration or create_default_calibration(led_count)
self.capture_template_id = capture_template_id
self.picture_stream_id = picture_stream_id
self.created_at = created_at or datetime.utcnow()
self.updated_at = updated_at or datetime.utcnow()
@@ -84,6 +87,7 @@ class Device:
},
"calibration": calibration_to_dict(self.calibration),
"capture_template_id": self.capture_template_id,
"picture_stream_id": self.picture_stream_id,
"created_at": self.created_at.isoformat(),
"updated_at": self.updated_at.isoformat(),
}
@@ -121,11 +125,8 @@ class Device:
else create_default_calibration(data["led_count"])
)
# Migration: assign default MSS template if no template set
capture_template_id = data.get("capture_template_id")
if not capture_template_id:
capture_template_id = "tpl_mss_default"
logger.info(f"Migrating device {data['id']} to default MSS template")
capture_template_id = data.get("capture_template_id", "")
picture_stream_id = data.get("picture_stream_id", "")
return cls(
device_id=data["id"],
@@ -136,6 +137,7 @@ class Device:
settings=settings,
calibration=calibration,
capture_template_id=capture_template_id,
picture_stream_id=picture_stream_id,
created_at=datetime.fromisoformat(data.get("created_at", datetime.utcnow().isoformat())),
updated_at=datetime.fromisoformat(data.get("updated_at", datetime.utcnow().isoformat())),
)
@@ -217,7 +219,8 @@ class DeviceStore:
led_count: int,
settings: Optional[ProcessingSettings] = None,
calibration: Optional[CalibrationConfig] = None,
capture_template_id: str = "tpl_mss_default",
capture_template_id: str = "",
picture_stream_id: str = "",
) -> Device:
"""Create a new device.
@@ -247,6 +250,7 @@ class DeviceStore:
settings=settings,
calibration=calibration,
capture_template_id=capture_template_id,
picture_stream_id=picture_stream_id,
)
# Store
@@ -285,6 +289,7 @@ class DeviceStore:
settings: Optional[ProcessingSettings] = None,
calibration: Optional[CalibrationConfig] = None,
capture_template_id: Optional[str] = None,
picture_stream_id: Optional[str] = None,
) -> Device:
"""Update device.
@@ -331,6 +336,8 @@ class DeviceStore:
device.calibration = calibration
if capture_template_id is not None:
device.capture_template_id = capture_template_id
if picture_stream_id is not None:
device.picture_stream_id = picture_stream_id
device.updated_at = datetime.utcnow()