Daily Art engine: museum-guide blurb (grounded LLM) + extracted palette
- daily_art gains blurb + palette columns (idempotent migration). - art._palette: Pillow median-cut to ~5 hex colors from the cached image (best- effort → [] on any failure). art._blurb: a warm 2-3 sentence "what you're looking at" note grounded in the Met catalogue (title/artist/bio/date/medium/ classification/culture/tags). Prompt leans on context/significance and the title+tags for subject — explicitly NOT asserting literal composition (figure counts/poses) it can't see, since the model can't view the image. Markdown stripped from the output. - pick_daily generates both (client optional → blurb skipped when absent); cycle + art CLI pass an LLM client. /api/art/today exposes blurb + palette. - Backfilled the last 3 days on host (Veteran / Magnolia Vase / Bierstadt). - scripts/art_blurb_palette_backfill.py for in-place backfill (no re-pick). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -44,6 +44,58 @@ def test_harvest_dedupes_into_pool(conn):
|
||||
assert art.harvest_pool(conn)["added"] == 0 # idempotent
|
||||
|
||||
|
||||
def test_palette_extracts_hex_colors(tmp_path):
|
||||
from PIL import Image
|
||||
p = tmp_path / "img.png"
|
||||
im = Image.new("RGB", (60, 60), (200, 30, 30)) # mostly red...
|
||||
for x in range(60):
|
||||
for y in range(30):
|
||||
im.putpixel((x, y), (30, 150, 70)) # ...top half green
|
||||
im.save(p)
|
||||
cols = art._palette(p, n=3)
|
||||
assert 1 <= len(cols) <= 3
|
||||
assert all(c.startswith("#") and len(c) == 7 for c in cols)
|
||||
|
||||
|
||||
def test_palette_bad_image_is_empty(tmp_path):
|
||||
p = tmp_path / "bad.jpg"
|
||||
p.write_bytes(b"\xff\xd8\xff" + b"x" * 500) # not a decodable image
|
||||
assert art._palette(p) == []
|
||||
|
||||
|
||||
class _FakeClient:
|
||||
def __init__(self, text="A quiet wheat field at dusk."):
|
||||
self.text, self.seen = text, None
|
||||
def chat_text(self, messages):
|
||||
self.seen = messages
|
||||
return self.text
|
||||
|
||||
|
||||
def test_blurb_grounds_in_metadata_and_cleans():
|
||||
c = _FakeClient(" A returning soldier in a golden field. \n")
|
||||
out = art._blurb(c, {"title": "The Veteran", "artistDisplayName": "Homer",
|
||||
"medium": "Oil on canvas", "tags": [{"term": "wheat"}, {"term": "scythe"}]})
|
||||
assert out == "A returning soldier in a golden field."
|
||||
user = c.seen[-1]["content"]
|
||||
assert "Homer" in user and "Oil on canvas" in user and "wheat" in user # catalogue facts fed in
|
||||
|
||||
|
||||
def test_blurb_none_on_error_or_empty():
|
||||
class Bad:
|
||||
def chat_text(self, m): raise RuntimeError("down")
|
||||
assert art._blurb(Bad(), {"title": "X"}) is None
|
||||
assert art._blurb(_FakeClient(" "), {"title": "X"}) is None
|
||||
|
||||
|
||||
def test_pick_stores_blurb_and_palette(conn):
|
||||
art.harvest_pool(conn)
|
||||
a = art.pick_daily(conn, art_date="2026-06-21", client=_FakeClient("A quiet masterwork."))
|
||||
assert a["blurb"] == "A quiet masterwork."
|
||||
assert a["palette"] == "[]" # fixture image isn't decodable → empty palette, stored as JSON
|
||||
b = art.pick_daily(conn, art_date="2026-06-22") # no client → no blurb, pick still succeeds
|
||||
assert b["blurb"] is None
|
||||
|
||||
|
||||
def test_pick_caches_image_metadata_and_marks_shown(conn):
|
||||
art.harvest_pool(conn)
|
||||
a = art.pick_daily(conn, art_date="2026-06-21")
|
||||
|
||||
@@ -23,6 +23,8 @@ def client(tmp_path, monkeypatch):
|
||||
"VALUES ('2026-06-21','met',10154,'Lander''s Peak','Bierstadt','1863','Oil','Paintings',"
|
||||
"'Gift','https://met/10154','10154.jpg','https://met/full.jpg',1)"
|
||||
)
|
||||
c.execute("UPDATE daily_art SET blurb=?, palette=? WHERE object_id=10154",
|
||||
("A luminous western vista.", '["#7fb4cf", "#c79a3c"]'))
|
||||
c.commit(); c.close()
|
||||
cache.mkdir(parents=True, exist_ok=True)
|
||||
(cache / "10154.jpg").write_bytes(b"\xff\xd8\xff" + b"x" * 5000) # web-large display copy
|
||||
@@ -43,3 +45,5 @@ def test_today_exposes_full_res_url(client):
|
||||
assert a["image_url"] == "/api/art/image/10154"
|
||||
assert a["image_url_large"] == "/api/art/image/10154?size=full"
|
||||
assert a["license"] == "Public Domain (CC0)" and a["museum"] == "The Met"
|
||||
assert a["blurb"] == "A luminous western vista."
|
||||
assert a["palette"] == ["#7fb4cf", "#c79a3c"] # parsed from stored JSON
|
||||
|
||||
Reference in New Issue
Block a user