Read-time: full-article "Full story · ~N min" badge (Option B)
Replaces the gist-based read-time with the SOURCE article's full read time — the
contrast that sells the gist ("calm 1-min version here; ~10 min for the deep dive").
- goodnews/readtime.py: word_count_from_html (strips script/style/nav/header/
footer/form/button/aside furniture before counting) + source_read_minutes
(~225 wpm, 200-word floor, None when extraction looks failed/too thin).
- articles.source_words + read_checked_at columns (count only, never the body;
fits the privacy posture). Idempotent migration.
- enrich.fetch_source_words + enrich_read_times: a bounded, retry-guarded cycle
step (mirrors the image enrichers) that counts words for recent accepted
articles. Only ever writes a real count; never overwrites good with zero. Wired
into the cycle after recent-image enrichment.
- queries: source_words flows through _ARTICLE_COLUMNS; api exposes
source_read_minutes on Article (null when unknown).
- home3: News card shows "Full story · ~N min", hidden entirely when null (no
misleading "1 min").
- Tests: furniture stripping, threshold/rounding, enrich idempotency + no
zero-overwrite, API null handling. 412 backend.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+3
-1
@@ -36,7 +36,7 @@ from fastapi.responses import FileResponse, HTMLResponse, RedirectResponse
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from pydantic import BaseModel
|
||||
|
||||
from . import art, auth, bloom, daily, email_send, feeds, games, oauth_google, onthisday, publishing, queries, quote, share, sources, summarize, wotd
|
||||
from . import art, auth, bloom, daily, email_send, feeds, games, oauth_google, onthisday, publishing, queries, quote, readtime, share, sources, summarize, wotd
|
||||
from .localtime import local_today
|
||||
from .markup import reply_html_to_text, sanitize_reply_html
|
||||
from .db import connect
|
||||
@@ -322,6 +322,7 @@ class Article(BaseModel):
|
||||
paywalled: bool = False
|
||||
tags: list[str] = []
|
||||
summary: str | None = None # our own cached summary (present on the brief)
|
||||
source_read_minutes: int | None = None # ~minutes to read the FULL source article (null = unknown)
|
||||
# Subject geography (present on feed rows; absent/empty on the brief). breadth is
|
||||
# locality|regional|national|multinational|global|unknown; places are ISO codes.
|
||||
geo_breadth: str | None = None
|
||||
@@ -345,6 +346,7 @@ class Article(BaseModel):
|
||||
geo_confidence=row.get("geo_confidence"),
|
||||
geo_places=places,
|
||||
summary=row.get("summary"),
|
||||
source_read_minutes=readtime.source_read_minutes(row.get("source_words")),
|
||||
id=row["id"],
|
||||
title=row["title"],
|
||||
description=row.get("description"),
|
||||
|
||||
Reference in New Issue
Block a user