Images phase 1: attention-triggered og:image coverage

Tie image enrichment to attention (per review): when an article earns a summary
(i.e. a reader reached it), best-effort fetch a real og:image if it lacks one —
never blanket-fetch every ingested article. Adds:

* enrich_article_image() — single-article fetch, leaves existing images alone,
  retries an imageless article only after 7 days, stamps image_checked_at.
* generate_summary() calls it after caching (wrapped; never breaks summaries).
* enrich_summarized_images() + `goodnews enrich-images` CLI — slow background
  backfill of already-summarized, accepted, imageless articles.
* Quality gate: extend the generic-image skip list with data:/tracking-pixel/
  spacer markers (on top of the existing logo/placeholder + unbranded-BBC logic).

This is coverage only; display (editorial rhythm, tile treatment) comes next.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
jay
2026-06-07 12:30:11 -04:00
parent 9813af40ed
commit 403749e26f
4 changed files with 143 additions and 1 deletions
+68
View File
@@ -50,6 +50,13 @@ _GENERIC_IMAGE_MARKERS = (
"/placeholder",
"share-default",
"social-default",
# tracking pixels / spacers / data-URIs — never a real share image
"data:image",
"/pixel",
"1x1",
"spacer",
"/blank.",
"transparent.",
)
@@ -207,3 +214,64 @@ def enrich_brief_images(
found += 1
conn.commit()
return found
def enrich_article_image(
conn: sqlite3.Connection, article_id: int, fetch=fetch_og_image, retry_days: int = 7
) -> bool:
"""Attention-triggered: fetch an og:image for ONE article that lacks one.
Called when an article earns a summary (i.e. it's actually being read), so we
only spend a fetch on articles a reader has reached. Leaves an existing image
alone; retries a still-imageless article only after `retry_days`. Returns True
if a new image was stored. Best-effort — never raises.
"""
row = conn.execute(
"""
SELECT id, canonical_url FROM articles
WHERE id = ?
AND (image_url IS NULL OR image_url = '')
AND (image_checked_at IS NULL OR image_checked_at < datetime('now', ?))
""",
(article_id, f"-{retry_days} days"),
).fetchone()
if not row:
return False # has an image already, or checked too recently
try:
image = fetch(row["canonical_url"])
except Exception:
image = None
conn.execute(
"UPDATE articles SET image_url = COALESCE(?, image_url), image_checked_at = CURRENT_TIMESTAMP "
"WHERE id = ?",
(image, article_id),
)
conn.commit()
return bool(image)
def enrich_summarized_images(
conn: sqlite3.Connection, fetch=fetch_og_image, limit: int = 50, retry_days: int = 7
) -> int:
"""Slow backfill: give already-summarized, accepted articles an image if they
lack one. Run in modest batches so we never blast publishers. Returns count
of newly-found images.
"""
rows = conn.execute(
"""
SELECT a.id FROM articles a
JOIN article_summaries m ON m.article_id = a.id
JOIN article_scores s ON s.article_id = a.id
WHERE s.accepted = 1 AND a.duplicate_of IS NULL
AND (a.image_url IS NULL OR a.image_url = '')
AND (a.image_checked_at IS NULL OR a.image_checked_at < datetime('now', ?))
ORDER BY a.id DESC
LIMIT ?
""",
(f"-{retry_days} days", limit),
).fetchall()
found = 0
for row in rows:
if enrich_article_image(conn, row["id"], fetch=fetch, retry_days=retry_days):
found += 1
return found