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

@@ -16,6 +16,8 @@ from wled_controller.config import get_config
from wled_controller.core.processor_manager import ProcessorManager
from wled_controller.storage import DeviceStore
from wled_controller.storage.template_store import TemplateStore
from wled_controller.storage.postprocessing_template_store import PostprocessingTemplateStore
from wled_controller.storage.picture_stream_store import PictureStreamStore
from wled_controller.utils import setup_logging, get_logger
# Initialize logging
@@ -28,7 +30,65 @@ config = get_config()
# Initialize storage and processing
device_store = DeviceStore(config.storage.devices_file)
template_store = TemplateStore(config.storage.templates_file)
processor_manager = ProcessorManager()
pp_template_store = PostprocessingTemplateStore(config.storage.postprocessing_templates_file)
picture_stream_store = PictureStreamStore(config.storage.picture_streams_file)
# Assign first available template to devices with missing/invalid template
all_templates = template_store.get_all_templates()
if all_templates:
valid_ids = {t.id for t in all_templates}
for device in device_store.get_all_devices():
if not device.capture_template_id or device.capture_template_id not in valid_ids:
old_id = device.capture_template_id
device_store.update_device(device.id, capture_template_id=all_templates[0].id)
logger.info(
f"Assigned template '{all_templates[0].name}' to device '{device.name}' "
f"(was '{old_id}')"
)
# Migrate devices without picture_stream_id: create streams from legacy settings
for device in device_store.get_all_devices():
if not device.picture_stream_id:
try:
# Create a raw stream from the device's current capture settings
raw_stream = picture_stream_store.create_stream(
name=f"{device.name} - Raw",
stream_type="raw",
display_index=device.settings.display_index,
capture_template_id=device.capture_template_id,
target_fps=device.settings.fps,
description=f"Auto-migrated from device '{device.name}'",
)
# Create a processed stream with the first PP template
pp_templates = pp_template_store.get_all_templates()
if pp_templates:
processed_stream = picture_stream_store.create_stream(
name=f"{device.name} - Processed",
stream_type="processed",
source_stream_id=raw_stream.id,
postprocessing_template_id=pp_templates[0].id,
description=f"Auto-migrated from device '{device.name}'",
)
device_store.update_device(device.id, picture_stream_id=processed_stream.id)
logger.info(
f"Migrated device '{device.name}': created raw stream '{raw_stream.id}' "
f"+ processed stream '{processed_stream.id}'"
)
else:
# No PP templates, assign raw stream directly
device_store.update_device(device.id, picture_stream_id=raw_stream.id)
logger.info(
f"Migrated device '{device.name}': created raw stream '{raw_stream.id}'"
)
except Exception as e:
logger.error(f"Failed to migrate device '{device.name}': {e}")
processor_manager = ProcessorManager(
picture_stream_store=picture_stream_store,
capture_template_store=template_store,
pp_template_store=pp_template_store,
)
@asynccontextmanager
@@ -61,7 +121,11 @@ async def lifespan(app: FastAPI):
logger.info("All API requests require valid Bearer token authentication")
# Initialize API dependencies
init_dependencies(device_store, template_store, processor_manager)
init_dependencies(
device_store, template_store, processor_manager,
pp_template_store=pp_template_store,
picture_stream_store=picture_stream_store,
)
# Load existing devices into processor manager
devices = device_store.get_all_devices()
@@ -74,6 +138,7 @@ async def lifespan(app: FastAPI):
settings=device.settings,
calibration=device.calibration,
capture_template_id=device.capture_template_id,
picture_stream_id=device.picture_stream_id,
)
logger.info(f"Loaded device: {device.name} ({device.id})")
except Exception as e: