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:
+36
-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, 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"),
|
||||
|
||||
Reference in New Issue
Block a user