Small joys: Quote of the Day + Word of the Day engines

- quote.py: curated public-domain quote pool (16 seeded, admin-grows), deterministic daily
  pick, lazy AI "what it means" explainer of the real quote (cached). No LLM-invented quotes.
- wotd.py: LLM proposes positive words → validated/enriched against dictionaryapi.dev (real
  definition, IPA, examples, audio) → audio clip cached to our origin (TTS fallback) →
  deterministic daily pick. Tops the pool up toward 30/day.
- db.py: quote_pool/daily_quote + wotd_pool/daily_wotd tables.
- api.py: /api/quote/today, /api/word/today, /api/word/audio/{word} (GET+HEAD).
- cli.py: cycle steps for both (under --no-joys), shared LLM client.
- tests: test_quote.py (6) + test_wotd.py (5). 393 backend tests green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
jay
2026-06-22 17:28:55 -04:00
parent a7da8362ab
commit 67d4bc32cb
7 changed files with 550 additions and 4 deletions
+36 -1
View File
@@ -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, email_send, feeds, games, oauth_google, onthisday, publishing, queries, share, sources, summarize
from . import art, auth, bloom, email_send, feeds, games, oauth_google, onthisday, publishing, queries, quote, share, sources, summarize, wotd
from .localtime import local_today
from .markup import reply_html_to_text, sanitize_reply_html
from .db import connect
@@ -2304,6 +2304,41 @@ def create_app() -> FastAPI:
"source": a["source"],
}
@app.get("/api/quote/today")
def quote_today(response: Response) -> dict:
with get_conn() as conn:
q = quote.get_today(conn)
if not q:
response.headers["Cache-Control"] = _PRIVATE
raise HTTPException(status_code=404, detail="No quote yet.")
response.headers["Cache-Control"] = _EDGE_FEED
return {"date": q["feature_date"], "text": q["text"], "author": q["author"],
"work": q["work"], "year": q["year"], "meaning": q["meaning"], "source": q["source"]}
@app.get("/api/word/today")
def word_today(response: Response) -> dict:
with get_conn() as conn:
w = wotd.get_today(conn)
if not w:
response.headers["Cache-Control"] = _PRIVATE
raise HTTPException(status_code=404, detail="No word yet.")
response.headers["Cache-Control"] = _EDGE_FEED
try:
examples = json.loads(w["examples"]) if w["examples"] else []
except (ValueError, TypeError):
examples = []
return {"date": w["feature_date"], "word": w["word"], "part_of_speech": w["part_of_speech"],
"phonetic": w["phonetic"], "definition": w["definition"], "examples": examples,
"audio_url": f"/api/word/audio/{w['word']}" if w["audio_file"] else None}
@app.api_route("/api/word/audio/{word}", methods=["GET", "HEAD"])
def word_audio(word: str) -> FileResponse:
matches = sorted(wotd.cache_dir().glob(f"{word.lower()}.*"))
matches = [m for m in matches if not m.name.startswith(".")]
if not matches:
raise HTTPException(status_code=404, detail="No audio.")
return FileResponse(str(matches[0]), headers={"Cache-Control": "public, max-age=31536000, immutable"})
@app.get("/api/replacement", response_model=Article | None)
def replacement(
exclude: str = Query("", description="comma-separated article ids already shown"),