feat: manage saved thumbnail plans and select plans for tasks
Community checks / verify (push) Skipped
Verify Frame Flow / verify (push) Failing after 5m58s

This commit is contained in:
渡口浪人
2026-10-23 14:47:27 +08:00
parent d94031eb50
commit 7178235022
17 changed files with 1736 additions and 37 deletions
+14 -2
View File
@@ -67,6 +67,10 @@ class DisplayService:
def validate(self, request, writing=True):
self.registry.get(request.device_id)
if request.options.thumbnail_plan_id:
from pigway_frame_flow.display.thumbnail_plans import get_plan
get_plan(self, request.options.thumbnail_plan_id)
if isinstance(request.content, FutureContent):
raise FlowError(
E.UNSUPPORTED_CAPABILITY, "Content type is unsupported", 422
@@ -778,6 +782,10 @@ class DisplayService:
):
self.reconcile(pending["id"], acknowledge=True)
request = DisplayRequest.model_validate(payload)
if request.options.thumbnail_plan_id:
from pigway_frame_flow.display.thumbnail_plans import get_plan
get_plan(self, request.options.thumbnail_plan_id)
content = request.content
self.update(id, "RENDERING")
asset = None
@@ -830,11 +838,15 @@ class DisplayService:
else:
driver.display(image_id)
result = {"image_id": image_id}
if asset:
if asset and request.options.generate_thumbnail:
from pigway_frame_flow.display.wrist_thumbnail import publish
thumbnail = publish(
self, device_id, asset["asset_id"], source_image.get("data")
self,
device_id,
asset["asset_id"],
source_image.get("data"),
plan_id=request.options.thumbnail_plan_id,
)
if thumbnail:
result["wrist_thumbnail"] = thumbnail
@@ -0,0 +1,45 @@
"""Saved thumbnail rendering parameters; source pixels are never decorated."""
from pydantic import Field
from pigway_frame_flow.errors import ErrorCode as E
from pigway_frame_flow.errors import FlowError
from pigway_frame_flow.models import StrictModel
from pigway_frame_flow.renderers.images import convert
from pigway_frame_flow.storage.database import encode
class ThumbnailPlan(StrictModel):
name: str = Field(min_length=1, max_length=120)
width: int = Field(default=410, ge=16, le=2048)
height: int = Field(default=502, ge=16, le=2048)
fit: str = Field(default="cover", pattern="^(cover|contain)$")
center_x: float = Field(default=0.5, ge=0, le=1, allow_inf_nan=False)
center_y: float = Field(default=0.5, ge=0, le=1, allow_inf_nan=False)
quality: int = Field(default=85, ge=40, le=95)
def get_plan(service, plan_id):
row = service.db.one(
"SELECT payload FROM app_config WHERE key=?", ("thumbnail-plan:" + plan_id,)
)
if not row:
raise FlowError(E.NOT_FOUND, "Thumbnail plan not found", 404)
return ThumbnailPlan.model_validate_json(row["payload"])
def render(data, plan):
return convert(
data,
plan.fit,
size=(plan.width, plan.height),
centering=(plan.center_x, plan.center_y),
quality=plan.quality,
)
def save_plan(service, plan_id, plan):
service.db.execute(
"INSERT OR REPLACE INTO app_config VALUES(?,?)",
("thumbnail-plan:" + plan_id, encode(plan.model_dump())),
)
+14 -6
View File
@@ -14,7 +14,7 @@ logger = logging.getLogger(__name__)
SIZE = (410, 502)
def publish(service, device_id, asset_id, data=None):
def publish(service, device_id, asset_id, data=None, plan_id=None):
# Templates and old gallery assets have no verified undecorated source.
row = service.db.one(
"SELECT payload FROM asset_sources WHERE asset_id=?", (asset_id,)
@@ -23,25 +23,33 @@ def publish(service, device_id, asset_id, data=None):
if source.get("type") != "photo_source":
return None
try:
from pigway_frame_flow.display.thumbnail_plans import get_plan, render
plan = get_plan(service, plan_id) if plan_id else None
size = (plan.width, plan.height) if plan else SIZE
if data is None:
data, _, _ = service.photo_sources.read(
source["source_id"], source["item_id"], target_size=SIZE
source["source_id"], source["item_id"], target_size=size
)
rendered = convert(data, "cover", "color", size=SIZE)
rendered = (
render(data, plan) if plan else convert(data, "cover", "color", size=SIZE)
)
asset = service.assets.put(rendered, "watch.jpg", "cache")
now = datetime.now(ZoneInfo(service.settings.timezone))
result = {
"asset_id": asset["asset_id"],
"url": asset["url"],
"width": SIZE[0],
"height": SIZE[1],
"width": size[0],
"height": size[1],
"date": now.date().isoformat(),
"timezone": service.settings.timezone,
"generated_at": now.isoformat(),
"sha256": hashlib.sha256(rendered).hexdigest(),
"bytes": len(rendered),
"source": source,
"fit": "cover",
"fit": plan.fit if plan else "cover",
"plan_id": plan_id,
"plan": plan.model_dump() if plan else None,
"source_rendition": "preview"
if service.photo_sources.get(source["source_id"]).kind == "synology-photos"
else "source",