Render curated date-folder photos with title date and place captions
This commit is contained in:
@@ -830,6 +830,23 @@
|
||||
"title": "Enabled",
|
||||
"default": true
|
||||
},
|
||||
"catalog_limit": {
|
||||
"type": "integer",
|
||||
"maximum": 100000.0,
|
||||
"minimum": 1.0,
|
||||
"title": "Catalog Limit",
|
||||
"default": 10000
|
||||
},
|
||||
"organized_only": {
|
||||
"type": "boolean",
|
||||
"title": "Organized Only",
|
||||
"default": false
|
||||
},
|
||||
"folder_caption": {
|
||||
"type": "boolean",
|
||||
"title": "Folder Caption",
|
||||
"default": false
|
||||
},
|
||||
"credential_env": {
|
||||
"anyOf": [
|
||||
{
|
||||
@@ -884,6 +901,23 @@
|
||||
"title": "Enabled",
|
||||
"default": true
|
||||
},
|
||||
"catalog_limit": {
|
||||
"type": "integer",
|
||||
"maximum": 100000.0,
|
||||
"minimum": 1.0,
|
||||
"title": "Catalog Limit",
|
||||
"default": 10000
|
||||
},
|
||||
"organized_only": {
|
||||
"type": "boolean",
|
||||
"title": "Organized Only",
|
||||
"default": false
|
||||
},
|
||||
"folder_caption": {
|
||||
"type": "boolean",
|
||||
"title": "Folder Caption",
|
||||
"default": false
|
||||
},
|
||||
"credential_env": {
|
||||
"anyOf": [
|
||||
{
|
||||
@@ -920,6 +954,20 @@
|
||||
"name": {
|
||||
"type": "string",
|
||||
"title": "Name"
|
||||
},
|
||||
"caption": {
|
||||
"anyOf": [
|
||||
{
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"title": "Caption"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
from io import BytesIO
|
||||
|
||||
import pytest
|
||||
from PIL import Image
|
||||
|
||||
from pigway_frame_flow.providers.photo_names import folder_caption, path_caption
|
||||
from pigway_frame_flow.providers.photo_sources import PhotoSourceInput
|
||||
from pigway_frame_flow.renderers.photo_caption import render_captioned_photo
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"name,title,day,place",
|
||||
[
|
||||
("20141207 婚礼@示例酒店", "婚礼", "2014-12-07", "示例酒店"),
|
||||
("2026-10-05 家庭出游 @ 示例地点", "家庭出游", "2026-10-05", "示例地点"),
|
||||
("2026.10.05_周末@家", "周末", "2026-10-05", "家"),
|
||||
("2026年10月05日 国庆假期", "国庆假期", "2026-10-05", ""),
|
||||
("20041015 军训合影", "军训合影", "2004-10-15", ""),
|
||||
("20241005 主题@地点@补充说明", "主题", "2024-10-05", "地点@补充说明"),
|
||||
],
|
||||
)
|
||||
def test_folder_three_line_caption(name, title, day, place):
|
||||
assert folder_caption(name) == {"title": title, "date": day, "place": place}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"name",
|
||||
[
|
||||
"MobileBackup",
|
||||
"20260230 无效日期",
|
||||
"20261301 无效月份",
|
||||
"20261005123 数字不是日期",
|
||||
],
|
||||
)
|
||||
def test_non_date_folders_are_not_curated(name):
|
||||
assert folder_caption(name) is None
|
||||
|
||||
|
||||
def test_child_folder_inherits_date_and_location():
|
||||
assert path_caption("20141207 婚礼@示例酒店/精选/IMG_001.jpg") == {
|
||||
"title": "婚礼",
|
||||
"date": "2014-12-07",
|
||||
"place": "示例酒店",
|
||||
}
|
||||
assert path_caption("MobileBackup/20260101.jpg") is None
|
||||
|
||||
|
||||
@pytest.mark.parametrize("profile", ["color", "grayscale"])
|
||||
def test_native_caption_layout_has_three_separate_lines(service, profile):
|
||||
out = BytesIO()
|
||||
Image.new("RGB", (1600, 900), "blue").save(out, "JPEG")
|
||||
result = render_captioned_photo(
|
||||
out.getvalue(),
|
||||
folder_caption("20141207 婚礼@示例酒店"),
|
||||
service.renderer.font,
|
||||
profile=profile,
|
||||
)
|
||||
with Image.open(BytesIO(result)) as image:
|
||||
assert image.size == (1200, 1600)
|
||||
for y in [1320, 1410, 1480]:
|
||||
darkest = image.crop((50, y, 1150, y + 65)).convert("L").getextrema()[0]
|
||||
assert darkest < 50, "Caption row is missing"
|
||||
pixel = image.getpixel((600, 640))
|
||||
assert (
|
||||
len(set(pixel)) <= 2
|
||||
if profile == "grayscale"
|
||||
else pixel[2] > pixel[0] + 100
|
||||
)
|
||||
|
||||
|
||||
def test_source_filters_unorganized_and_reports_caption(client, service, settings):
|
||||
root = settings.data_dir.parent / "photos"
|
||||
settings.photo_root = root
|
||||
for folder in ["20261005 秋游@金山/精选", "MobileBackup", "20260230 无效日期"]:
|
||||
path = root / folder / "photo.jpg"
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
Image.new("RGB", (100, 100), "red").save(path)
|
||||
source = PhotoSourceInput(
|
||||
name="Curated",
|
||||
kind="local",
|
||||
location=".",
|
||||
organized_only=True,
|
||||
folder_caption=True,
|
||||
catalog_limit=100000,
|
||||
)
|
||||
service.photo_sources.save("curated", source)
|
||||
r = client.get("/api/v1/photo-sources/curated/images")
|
||||
assert r.status_code == 200
|
||||
data = r.json()
|
||||
assert data["total"] == 1 and data["catalog_limit"] == 100000
|
||||
assert data["items"][0]["caption"] == {
|
||||
"title": "秋游",
|
||||
"date": "2026-10-05",
|
||||
"place": "金山",
|
||||
}
|
||||
accepted = client.post(
|
||||
"/api/v1/devices/living-room/upload",
|
||||
json={"type": "photo_source", "source_id": "curated"},
|
||||
)
|
||||
assert accepted.status_code == 202 and service.process_one()
|
||||
op = client.get("/api/v1/operations/" + accepted.json()["operation_id"]).json()
|
||||
assert op["status"] == "COMPLETED"
|
||||
with Image.open(BytesIO(client.get(op["result"]["url"]).content)) as image:
|
||||
assert image.size == (1200, 1600)
|
||||
|
||||
|
||||
def test_curated_catalog_includes_photos_beyond_old_ten_thousand_limit(
|
||||
service, settings
|
||||
):
|
||||
root = settings.data_dir.parent / "many-photos"
|
||||
settings.photo_root = root
|
||||
folder = root / "20261005 旅行@示例地点"
|
||||
folder.mkdir(parents=True)
|
||||
for index in range(10001):
|
||||
(folder / f"{index:05}.jpg").write_bytes(b"catalog metadata fixture")
|
||||
service.photo_sources.save(
|
||||
"all",
|
||||
PhotoSourceInput(
|
||||
name="All curated",
|
||||
kind="local",
|
||||
location=".",
|
||||
organized_only=True,
|
||||
folder_caption=True,
|
||||
catalog_limit=100000,
|
||||
),
|
||||
)
|
||||
assert len(service.photo_sources.catalog("all")) == 10001
|
||||
Reference in New Issue
Block a user