Add static image picture stream type with auto-validating UI
Introduces a new "static_image" stream type that loads a frame from a URL or local file path, enabling LED testing with known images or displaying static content. Includes validate-image API endpoint, auto-validation on blur/enter/paste with caching, capture template names on stream cards, and conditional test stats display for single-frame results. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -12,11 +12,12 @@ class PictureStream:
|
||||
A picture stream is either:
|
||||
- "raw": captures from a display using a capture engine template at a target FPS
|
||||
- "processed": applies postprocessing to another picture stream
|
||||
- "static_image": returns a static frame from a URL or local file path
|
||||
"""
|
||||
|
||||
id: str
|
||||
name: str
|
||||
stream_type: str # "raw" or "processed"
|
||||
stream_type: str # "raw", "processed", or "static_image"
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
@@ -29,6 +30,9 @@ class PictureStream:
|
||||
source_stream_id: Optional[str] = None
|
||||
postprocessing_template_id: Optional[str] = None
|
||||
|
||||
# Static image fields (used when stream_type == "static_image")
|
||||
image_source: Optional[str] = None
|
||||
|
||||
description: Optional[str] = None
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
@@ -42,6 +46,7 @@ class PictureStream:
|
||||
"target_fps": self.target_fps,
|
||||
"source_stream_id": self.source_stream_id,
|
||||
"postprocessing_template_id": self.postprocessing_template_id,
|
||||
"image_source": self.image_source,
|
||||
"created_at": self.created_at.isoformat(),
|
||||
"updated_at": self.updated_at.isoformat(),
|
||||
"description": self.description,
|
||||
@@ -59,6 +64,7 @@ class PictureStream:
|
||||
target_fps=data.get("target_fps"),
|
||||
source_stream_id=data.get("source_stream_id"),
|
||||
postprocessing_template_id=data.get("postprocessing_template_id"),
|
||||
image_source=data.get("image_source"),
|
||||
created_at=datetime.fromisoformat(data["created_at"])
|
||||
if isinstance(data.get("created_at"), str)
|
||||
else data.get("created_at", datetime.utcnow()),
|
||||
|
||||
@@ -105,7 +105,7 @@ class PictureStreamStore:
|
||||
current_stream = self._streams.get(current_id)
|
||||
if not current_stream:
|
||||
break
|
||||
if current_stream.stream_type == "raw":
|
||||
if current_stream.stream_type != "processed":
|
||||
break
|
||||
current_id = current_stream.source_stream_id
|
||||
|
||||
@@ -134,24 +134,26 @@ class PictureStreamStore:
|
||||
target_fps: Optional[int] = None,
|
||||
source_stream_id: Optional[str] = None,
|
||||
postprocessing_template_id: Optional[str] = None,
|
||||
image_source: Optional[str] = None,
|
||||
description: Optional[str] = None,
|
||||
) -> PictureStream:
|
||||
"""Create a new picture stream.
|
||||
|
||||
Args:
|
||||
name: Stream name
|
||||
stream_type: "raw" or "processed"
|
||||
stream_type: "raw", "processed", or "static_image"
|
||||
display_index: Display index (raw streams)
|
||||
capture_template_id: Capture template ID (raw streams)
|
||||
target_fps: Target FPS (raw streams)
|
||||
source_stream_id: Source stream ID (processed streams)
|
||||
postprocessing_template_id: Postprocessing template ID (processed streams)
|
||||
image_source: URL or file path (static_image streams)
|
||||
description: Optional description
|
||||
|
||||
Raises:
|
||||
ValueError: If validation fails or cycle detected
|
||||
"""
|
||||
if stream_type not in ("raw", "processed"):
|
||||
if stream_type not in ("raw", "processed", "static_image"):
|
||||
raise ValueError(f"Invalid stream type: {stream_type}")
|
||||
|
||||
if stream_type == "raw":
|
||||
@@ -172,6 +174,9 @@ class PictureStreamStore:
|
||||
# Check for cycles
|
||||
if self._detect_cycle(source_stream_id):
|
||||
raise ValueError("Cycle detected in stream chain")
|
||||
elif stream_type == "static_image":
|
||||
if not image_source:
|
||||
raise ValueError("Static image streams require image_source")
|
||||
|
||||
# Check for duplicate name
|
||||
for stream in self._streams.values():
|
||||
@@ -190,6 +195,7 @@ class PictureStreamStore:
|
||||
target_fps=target_fps,
|
||||
source_stream_id=source_stream_id,
|
||||
postprocessing_template_id=postprocessing_template_id,
|
||||
image_source=image_source,
|
||||
created_at=now,
|
||||
updated_at=now,
|
||||
description=description,
|
||||
@@ -210,6 +216,7 @@ class PictureStreamStore:
|
||||
target_fps: Optional[int] = None,
|
||||
source_stream_id: Optional[str] = None,
|
||||
postprocessing_template_id: Optional[str] = None,
|
||||
image_source: Optional[str] = None,
|
||||
description: Optional[str] = None,
|
||||
) -> PictureStream:
|
||||
"""Update an existing picture stream.
|
||||
@@ -241,6 +248,8 @@ class PictureStreamStore:
|
||||
stream.source_stream_id = source_stream_id
|
||||
if postprocessing_template_id is not None:
|
||||
stream.postprocessing_template_id = postprocessing_template_id
|
||||
if image_source is not None:
|
||||
stream.image_source = image_source
|
||||
if description is not None:
|
||||
stream.description = description
|
||||
|
||||
@@ -289,9 +298,9 @@ class PictureStreamStore:
|
||||
return False
|
||||
|
||||
def resolve_stream_chain(self, stream_id: str) -> dict:
|
||||
"""Resolve a stream chain to get the final raw stream and collected postprocessing templates.
|
||||
"""Resolve a stream chain to get the terminal stream and collected postprocessing templates.
|
||||
|
||||
Walks the chain from the given stream to the root raw stream,
|
||||
Walks the chain from the given stream to a terminal stream (raw or static_image),
|
||||
collecting postprocessing template IDs along the way.
|
||||
|
||||
Args:
|
||||
@@ -299,7 +308,7 @@ class PictureStreamStore:
|
||||
|
||||
Returns:
|
||||
Dict with:
|
||||
- raw_stream: The root raw PictureStream
|
||||
- raw_stream: The terminal PictureStream (raw or static_image)
|
||||
- postprocessing_template_ids: List of PP template IDs (in chain order)
|
||||
|
||||
Raises:
|
||||
@@ -316,7 +325,7 @@ class PictureStreamStore:
|
||||
|
||||
stream = self.get_stream(current_id)
|
||||
|
||||
if stream.stream_type == "raw":
|
||||
if stream.stream_type != "processed":
|
||||
return {
|
||||
"raw_stream": stream,
|
||||
"postprocessing_template_ids": postprocessing_template_ids,
|
||||
|
||||
Reference in New Issue
Block a user