Daily Art backend: curated Met pool, daily cached pick, /api/art (prototype)
The engine for the /art room (design-independent; deploy held for Codex review).
- goodnews/art.py: harvest a curated pool of public-domain HIGHLIGHT artworks from the
Met (isHighlight+isPublicDomain+hasImages -> masterworks, never potsherds; CC0). Daily
deterministic pick from the least-recently-shown (no soon-repeats, same for everyone),
fetch metadata + download the image to OUR cache (data/art_cache) so the homepage never
waits on or hotlinks the museum. Bulletproof: bad object/image falls through candidates;
a failed day keeps the last piece (room never empty). Injectable HTTP for tests.
- Schema: art_pool + daily_art. /api/art/today (edge-cacheable) + /api/art/image/{id}
(served from cache, immutable). CLI `art [--harvest] [--force]` + a non-fatal cycle step.
- Tests (5, mocked HTTP) + verified live against the Met: harvested 1641 works,
picked/cached "Repose" by John White Alexander. 371 tests green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -13,6 +13,7 @@ from .games import generate_daily_puzzles
|
||||
from .localtime import local_today
|
||||
from .dedup import DEFAULT_THRESHOLD, DEFAULT_WINDOW_DAYS, cluster_duplicates, dedup as run_dedup
|
||||
from .geo import tag_articles as tag_geo
|
||||
from . import art
|
||||
from .enrich import enrich_brief_images, enrich_recent_images, enrich_summarized_images
|
||||
from .summarize import generate_summary, get_summary
|
||||
from .feeds import (
|
||||
@@ -135,6 +136,7 @@ def main() -> None:
|
||||
cycle_parser.add_argument("--no-dedup", action="store_true", help="Skip the embedding dedup step")
|
||||
cycle_parser.add_argument("--no-geo", action="store_true", help="Skip tagging article subject-geography")
|
||||
cycle_parser.add_argument("--geo-limit", type=int, default=60, help="Max articles to geo-tag per cycle")
|
||||
cycle_parser.add_argument("--no-art", action="store_true", help="Skip the Daily Art pick")
|
||||
cycle_parser.add_argument("--no-brief", action="store_true", help="Skip rebuilding today's brief")
|
||||
cycle_parser.add_argument("--no-review", action="store_true", help="Skip recomputing source review flags")
|
||||
cycle_parser.add_argument("--no-digest", action="store_true", help="Skip sending due daily digests")
|
||||
@@ -150,6 +152,10 @@ def main() -> None:
|
||||
)
|
||||
enrich_images_parser.add_argument("--limit", type=int, default=50, help="Max articles to fetch this batch")
|
||||
|
||||
art_parser = subparsers.add_parser("art", help="Daily Art: harvest the pool and/or pick today's cached piece")
|
||||
art_parser.add_argument("--harvest", action="store_true", help="(Re)harvest the curated museum pool")
|
||||
art_parser.add_argument("--force", action="store_true", help="Re-pick today's art even if already chosen")
|
||||
|
||||
geo_parser = subparsers.add_parser("geo", help="Tag article subject-geography (backfill / manual). Cycle-locked.")
|
||||
geo_parser.add_argument("--limit", type=int, default=200, help="Max articles to tag this batch")
|
||||
geo_parser.add_argument("--reclassify", action="store_true", help="Re-tag even rows already at the current geo version")
|
||||
@@ -307,6 +313,17 @@ def main() -> None:
|
||||
elif args.command == "enrich-images":
|
||||
found = enrich_summarized_images(conn, limit=args.limit)
|
||||
print(f"enrich-images: {found} new image(s) for summarized articles")
|
||||
elif args.command == "art":
|
||||
init_db(conn)
|
||||
if args.harvest:
|
||||
h = art.harvest_pool(conn)
|
||||
print(f"art harvest: found={h['found']} added={h['added']} pool={h['pool']} errors={h['errors']}")
|
||||
picked = art.pick_daily(conn, force=args.force)
|
||||
if picked:
|
||||
print(f"art pick: {picked['art_date']} -> #{picked['object_id']} "
|
||||
f"\"{picked['title']}\" — {picked['artist'] or 'Unknown'}")
|
||||
else:
|
||||
print("art pick: nothing fetched (kept the last piece)")
|
||||
elif args.command == "geo":
|
||||
init_db(conn)
|
||||
# Cycle-locked so a manual backfill can't contend with the scheduled cycle.
|
||||
@@ -534,6 +551,15 @@ def _run_cycle_locked(conn: sqlite3.Connection, args: argparse.Namespace) -> Non
|
||||
except Exception as exc:
|
||||
print(f"geo: skipped ({exc})")
|
||||
|
||||
# Daily Art: ensure the pool exists, then ensure today has a cached piece. No-ops
|
||||
# once the day is picked; non-fatal like every other step.
|
||||
if not args.no_art:
|
||||
try:
|
||||
a = art.run_daily(conn)
|
||||
print(f"art: pool={a['pool']} picked={a['picked_object']}")
|
||||
except Exception as exc:
|
||||
print(f"art: skipped ({exc})")
|
||||
|
||||
if not args.no_brief:
|
||||
today = local_today()
|
||||
try:
|
||||
|
||||
Reference in New Issue
Block a user