feat: manage saved thumbnail plans and select plans for tasks
This commit is contained in:
@@ -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,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",
|
||||
|
||||
Reference in New Issue
Block a user