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:
@@ -16,8 +16,9 @@ logger = get_logger(__name__)
|
||||
class TemplateStore:
|
||||
"""Storage for capture templates.
|
||||
|
||||
Default templates for each available engine are created in memory at startup.
|
||||
Only user-created templates are persisted to the JSON file.
|
||||
All templates are persisted to the JSON file.
|
||||
On startup, if no templates exist, one is auto-created using the
|
||||
highest-priority available engine.
|
||||
"""
|
||||
|
||||
def __init__(self, file_path: str):
|
||||
@@ -28,34 +29,40 @@ class TemplateStore:
|
||||
"""
|
||||
self.file_path = Path(file_path)
|
||||
self._templates: Dict[str, CaptureTemplate] = {}
|
||||
self._ensure_defaults()
|
||||
self._load()
|
||||
self._ensure_initial_template()
|
||||
|
||||
def _ensure_defaults(self) -> None:
|
||||
"""Create default templates in memory for all available engines."""
|
||||
available = EngineRegistry.get_available_engines()
|
||||
def _ensure_initial_template(self) -> None:
|
||||
"""Auto-create a template if none exist, using the best available engine."""
|
||||
if self._templates:
|
||||
return
|
||||
|
||||
best_engine = EngineRegistry.get_best_available_engine()
|
||||
if not best_engine:
|
||||
logger.warning("No capture engines available, cannot create initial template")
|
||||
return
|
||||
|
||||
engine_class = EngineRegistry.get_engine(best_engine)
|
||||
default_config = engine_class.get_default_config()
|
||||
now = datetime.utcnow()
|
||||
template_id = f"tpl_{uuid.uuid4().hex[:8]}"
|
||||
|
||||
for engine_type in available:
|
||||
template_id = f"tpl_{engine_type}_default"
|
||||
engine_class = EngineRegistry.get_engine(engine_type)
|
||||
default_config = engine_class.get_default_config()
|
||||
template = CaptureTemplate(
|
||||
id=template_id,
|
||||
name=best_engine.upper(),
|
||||
engine_type=best_engine,
|
||||
engine_config=default_config,
|
||||
created_at=now,
|
||||
updated_at=now,
|
||||
description=f"Auto-created {best_engine.upper()} template",
|
||||
)
|
||||
|
||||
self._templates[template_id] = CaptureTemplate(
|
||||
id=template_id,
|
||||
name=engine_type.upper(),
|
||||
engine_type=engine_type,
|
||||
engine_config=default_config,
|
||||
is_default=True,
|
||||
created_at=now,
|
||||
updated_at=now,
|
||||
description=f"Default {engine_type} capture template",
|
||||
)
|
||||
|
||||
logger.info(f"Created {len(available)} default templates in memory")
|
||||
self._templates[template_id] = template
|
||||
self._save()
|
||||
logger.info(f"Auto-created initial template: {template.name} ({template_id}, engine={best_engine})")
|
||||
|
||||
def _load(self) -> None:
|
||||
"""Load user-created templates from file."""
|
||||
"""Load templates from file."""
|
||||
if not self.file_path.exists():
|
||||
return
|
||||
|
||||
@@ -66,9 +73,6 @@ class TemplateStore:
|
||||
templates_data = data.get("templates", {})
|
||||
loaded = 0
|
||||
for template_id, template_dict in templates_data.items():
|
||||
# Skip any default templates that may exist in old files
|
||||
if template_dict.get("is_default", False):
|
||||
continue
|
||||
try:
|
||||
template = CaptureTemplate.from_dict(template_dict)
|
||||
self._templates[template_id] = template
|
||||
@@ -80,26 +84,23 @@ class TemplateStore:
|
||||
)
|
||||
|
||||
if loaded > 0:
|
||||
logger.info(f"Loaded {loaded} user templates from storage")
|
||||
logger.info(f"Loaded {loaded} templates from storage")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to load templates from {self.file_path}: {e}")
|
||||
raise
|
||||
|
||||
total = len(self._templates)
|
||||
logger.info(f"Template store initialized with {total} templates")
|
||||
logger.info(f"Template store initialized with {len(self._templates)} templates")
|
||||
|
||||
def _save(self) -> None:
|
||||
"""Save only user-created templates to file."""
|
||||
"""Save all templates to file."""
|
||||
try:
|
||||
# Ensure directory exists
|
||||
self.file_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Only persist non-default templates
|
||||
templates_dict = {
|
||||
template_id: template.to_dict()
|
||||
for template_id, template in self._templates.items()
|
||||
if not template.is_default
|
||||
}
|
||||
|
||||
data = {
|
||||
@@ -162,7 +163,7 @@ class TemplateStore:
|
||||
"""
|
||||
# Check for duplicate name
|
||||
for template in self._templates.values():
|
||||
if template.name == name and not template.is_default:
|
||||
if template.name == name:
|
||||
raise ValueError(f"Template with name '{name}' already exists")
|
||||
|
||||
# Generate new ID
|
||||
@@ -175,7 +176,6 @@ class TemplateStore:
|
||||
name=name,
|
||||
engine_type=engine_type,
|
||||
engine_config=engine_config,
|
||||
is_default=False,
|
||||
created_at=now,
|
||||
updated_at=now,
|
||||
description=description,
|
||||
@@ -209,16 +209,13 @@ class TemplateStore:
|
||||
Updated template
|
||||
|
||||
Raises:
|
||||
ValueError: If template not found or is a default template
|
||||
ValueError: If template not found
|
||||
"""
|
||||
if template_id not in self._templates:
|
||||
raise ValueError(f"Template not found: {template_id}")
|
||||
|
||||
template = self._templates[template_id]
|
||||
|
||||
if template.is_default:
|
||||
raise ValueError("Cannot modify default templates")
|
||||
|
||||
# Update fields
|
||||
if name is not None:
|
||||
template.name = name
|
||||
@@ -244,16 +241,11 @@ class TemplateStore:
|
||||
template_id: Template ID
|
||||
|
||||
Raises:
|
||||
ValueError: If template not found or is a default template
|
||||
ValueError: If template not found
|
||||
"""
|
||||
if template_id not in self._templates:
|
||||
raise ValueError(f"Template not found: {template_id}")
|
||||
|
||||
template = self._templates[template_id]
|
||||
|
||||
if template.is_default:
|
||||
raise ValueError("Cannot delete default templates")
|
||||
|
||||
# Remove and save
|
||||
del self._templates[template_id]
|
||||
self._save()
|
||||
|
||||
Reference in New Issue
Block a user