WOTD #4/#5 content quality + Editorial Asymmetric /word page (CD)

Content quality ("LLM polishes, dictionary anchors"):
- New wotd._polish: rewrites the real dictionary gloss into ONE warm plain
  sentence + two clear everyday example sentences, grounded in the real
  definition (no invented meanings). Stored in new wotd_pool/daily_wotd columns
  gloss + usage, alongside the raw definition/examples which stay the anchor.
- harvest() polishes each new word; pick_daily() lazily polishes + caches back
  any older pooled word that lacks a gloss (client threaded through run_daily).
- Admin word-add polishes on insert; re-pick passes an LLM client so quote
  meaning / word gloss fill on a forced fresh pick.
- /api/word/today now prefers gloss + usage, falling back to the raw dictionary
  def/examples when polish is absent (so it's always safe).
- db._migrate adds gloss/usage to wotd_pool + daily_wotd (idempotent ALTER).

Frontend — /word redesigned to CD's "Editorial Asymmetric": faded oversized
initial bleeding off the right, vertical part-of-speech rail, big Newsreader
word, airy definition, left-ruled italic example sentences, outline Listen
button + date. (Uses our self-hosted Newsreader/Hanken stack rather than the
mockup's Google fonts; the made-up syllable respelling is omitted since we only
have real IPA.)

Tests: _polish parse/trim/cap, harvest stores gloss/usage, pick lazy-polishes
older words, admin gloss flows through to /api/word/today. 403 backend + 27 fe.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
jay
2026-06-23 06:08:14 -04:00
parent e5f3d942e2
commit cebbed58ab
6 changed files with 196 additions and 65 deletions
+14 -6
View File
@@ -2340,12 +2340,14 @@ def create_app() -> FastAPI:
response.headers["Cache-Control"] = _PRIVATE
raise HTTPException(status_code=404, detail="No word yet.")
response.headers["Cache-Control"] = _EDGE_FEED
# Prefer the LLM-polished gloss + everyday sentences; fall back to the raw dictionary.
raw_examples = w.get("usage") or w.get("examples")
try:
examples = json.loads(w["examples"]) if w["examples"] else []
examples = json.loads(raw_examples) if raw_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,
"phonetic": w["phonetic"], "definition": w.get("gloss") or 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"])
@@ -2423,10 +2425,13 @@ def create_app() -> FastAPI:
if not info:
raise HTTPException(status_code=400, detail="Word not found in dictionary.")
audio_file = wotd._cache_audio(info["audio_url"], info["word"])
conn.execute("INSERT OR IGNORE INTO wotd_pool (source, word, part_of_speech, phonetic, audio_file, audio_url, definition, examples) "
"VALUES ('admin',?,?,?,?,?,?,?)",
polished = wotd._polish(LocalModelClient.from_env(), info["word"], info["part_of_speech"], info["definition"])
gloss = polished["gloss"] if polished else None
usage = json.dumps(polished["examples"]) if polished else None
conn.execute("INSERT OR IGNORE INTO wotd_pool (source, word, part_of_speech, phonetic, audio_file, audio_url, definition, examples, gloss, usage) "
"VALUES ('admin',?,?,?,?,?,?,?,?,?)",
(info["word"], info["part_of_speech"], info["phonetic"], audio_file, info["audio_url"],
info["definition"], json.dumps(info["examples"])))
info["definition"], json.dumps(info["examples"]), gloss, usage))
else:
raise HTTPException(status_code=404, detail="Unknown joy.")
conn.commit()
@@ -2443,7 +2448,10 @@ def create_app() -> FastAPI:
f"SELECT pool_id FROM {_JOY_DAILY[kind]} WHERE feature_date=?", (local_today(),)
).fetchone()
avoid = cur["pool_id"] if cur else None # force a DIFFERENT item, not the same one
picked = mod.pick_daily(conn, force=True, avoid=avoid)
kwargs = {"force": True, "avoid": avoid}
if kind in ("quote", "word"): # these polish lazily (gloss / meaning)
kwargs["client"] = LocalModelClient.from_env()
picked = mod.pick_daily(conn, **kwargs)
return {"ok": True, "picked": bool(picked)}
@app.get("/api/replacement", response_model=Article | None)