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:
@@ -0,0 +1,58 @@
|
||||
from goodnews.db import connect, init_db
|
||||
from goodnews import enrich
|
||||
|
||||
|
||||
def _setup(tmp_path):
|
||||
c = connect(str(tmp_path / "t.db")); init_db(c)
|
||||
c.execute("INSERT INTO sources (id,name,feed_url) VALUES (1,'S','http://s/f')")
|
||||
return c
|
||||
|
||||
|
||||
def _add(c, aid, *, image=None, accepted=1, dup=None, summarized=True):
|
||||
c.execute(
|
||||
"INSERT INTO articles (id,source_id,canonical_url,title,url_hash,image_url,duplicate_of,published_at) "
|
||||
"VALUES (?,1,?,?,?,?,?,datetime('now'))",
|
||||
(aid, f"http://s/{aid}", f"T{aid}", f"h{aid}", image, dup),
|
||||
)
|
||||
c.execute("INSERT INTO article_scores (article_id,accepted) VALUES (?,?)", (aid, accepted))
|
||||
if summarized:
|
||||
c.execute("INSERT INTO article_summaries (article_id,summary) VALUES (?, 's')", (aid,))
|
||||
c.commit()
|
||||
|
||||
|
||||
def test_enrich_article_image_fills_missing(tmp_path):
|
||||
c = _setup(tmp_path); _add(c, 1, image=None)
|
||||
got = enrich.enrich_article_image(c, 1, fetch=lambda url: "http://img/og.jpg")
|
||||
assert got is True
|
||||
assert c.execute("SELECT image_url FROM articles WHERE id=1").fetchone()[0] == "http://img/og.jpg"
|
||||
# image_checked_at is stamped so we don't re-fetch endlessly
|
||||
assert c.execute("SELECT image_checked_at FROM articles WHERE id=1").fetchone()[0] is not None
|
||||
|
||||
|
||||
def test_enrich_skips_when_image_present(tmp_path):
|
||||
c = _setup(tmp_path); _add(c, 1, image="http://existing/img.jpg")
|
||||
calls = []
|
||||
got = enrich.enrich_article_image(c, 1, fetch=lambda url: calls.append(url) or "http://new.jpg")
|
||||
assert got is False and calls == [] # never fetched
|
||||
assert c.execute("SELECT image_url FROM articles WHERE id=1").fetchone()[0] == "http://existing/img.jpg"
|
||||
|
||||
|
||||
def test_enrich_records_checked_even_when_none_found(tmp_path):
|
||||
c = _setup(tmp_path); _add(c, 1, image=None)
|
||||
got = enrich.enrich_article_image(c, 1, fetch=lambda url: None)
|
||||
assert got is False
|
||||
# checked_at stamped → won't retry until the retry window passes
|
||||
assert c.execute("SELECT image_checked_at FROM articles WHERE id=1").fetchone()[0] is not None
|
||||
again = enrich.enrich_article_image(c, 1, fetch=lambda url: "http://late.jpg", retry_days=7)
|
||||
assert again is False # still within retry window
|
||||
|
||||
|
||||
def test_backfill_only_targets_summarized_accepted_imageless(tmp_path):
|
||||
c = _setup(tmp_path)
|
||||
_add(c, 1, image=None, summarized=True, accepted=1) # eligible
|
||||
_add(c, 2, image=None, summarized=False, accepted=1) # no summary → skip
|
||||
_add(c, 3, image=None, summarized=True, accepted=0) # rejected → skip
|
||||
_add(c, 4, image="http://has.jpg", summarized=True) # has image → skip
|
||||
n = enrich.enrich_summarized_images(c, fetch=lambda url: "http://og.jpg", limit=50)
|
||||
assert n == 1
|
||||
assert c.execute("SELECT image_url FROM articles WHERE id=2").fetchone()[0] is None
|
||||
Reference in New Issue
Block a user