narrative: precomputed plain-English day summaries #102
No reviewers
Labels
No labels
Compat/Breaking
Kind/Bug
Kind/Documentation
Kind/Enhancement
Kind/Feature
Kind/Security
Kind/Testing
Priority
Critical
Priority
High
Priority
Low
Priority
Medium
Reviewed
Confirmed
Reviewed
Duplicate
Reviewed
Invalid
Reviewed
Won't Fix
Status
Abandoned
Status
Blocked
Status
Need More Info
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference: Jinemi/thermograph#102
Loading…
Reference in a new issue
No description provided.
Delete branch "feat/grade-narrative"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Adds
GET /api/v2/narrative— a one-sentence, plain-English summary of how unusual a given day was at a location, generated by a local LLM on the desktop and served from cache.Shape
Generation and serving are fully decoupled:
scripts/generate_narratives.py(batch, runs wherever it can reach Ollama) reads graded days from the API, has the model phrase them, and writes rows.api/narrative_routes.py(request path) reads one row and returns it. It never calls the model, and a test replacesnarrator.generatewith a bomb so it can't start.So the public endpoint has no dependency on the inference host being reachable, powered on, or holding a GPU. A miss is 200 with
narrative: null,status: "pending"— not a 404; "no sentence yet" is a normal state and a frontend should render the page without it.Grounding
The model is given tier labels and percentile directions only — never a temperature, never a rainfall figure. It cannot misreport a reading it was never shown.
_sanitizethen rejects output carrying units, a forecast, or advice, strips lead-ins/<think>blocks, and caps length at a sentence boundary. Rejecting is always safe: nothing is stored and the endpoint servesnull.Fact lines state direction explicitly:
An earlier revision wrote
82nd percentile (High)and the model read it backwards, describing a warm night as a cool one — a factual inversion no output filter can detect, since nothing about the sentence looks wrong. Percentages mirrorgrading.pct_ordinal's rounding and 1..99 clamp so a narrative can't disagree with the number printed beside it.A consequence worth naming: narratives are unit-free, which sidesteps
F_COUNTRIESentirely and keeps one cached row per (cell, date) rather than a °C and a °F variant.Storage
New
narrativetable in the derived store,LOGGEDon Postgres unlike its twoUNLOGGEDsiblings — losing it costs an LLM pass over the whole grid, not a recompute from parquet. Same token-as-validity rule asderived: the token isNARRATIVE_VER:model, so a prompt or model change orphans old text instead of serving it. It deliberately does not track the climate sync stamps; that would invalidate every row every ~4h, which isn't affordable against one desktop GPU and buys nothing.Verified
Operational note
The desktop's RX 7800 XT is currently absent from the PCI bus, so Ollama has run 100% CPU since a Jul 20 reboot. Generation still works (~7-20s/city, so a ~1000-city pass is hours rather than minutes); it speeds up on its own when the card returns, with no code change. Nothing in this PR depends on the GPU.
Adds /api/v2/narrative, serving a one-sentence summary of how unusual a given day was at a location. Generation is a batch job (scripts/generate_narratives.py) that talks to a local Ollama; the endpoint only ever reads the cache, so no request path depends on the inference host being reachable or powered on. A miss answers 200 with narrative:null and status "pending" rather than 404. The model is handed tier labels and percentile directions only, never a temperature or rainfall figure, so it has nothing to misreport; the sanitizer then rejects any output that carries units, a forecast or advice. Fact lines state direction ("warmer than 82% of nights at this time of year here") because the earlier "82nd percentile (High)" form was read backwards and phrased a warm night as a cool one. Narratives are unit-free as a result, which also keeps one cached row per cell/date instead of a per-country degree variant. Storage is a new `narrative` table in the derived store, LOGGED on Postgres unlike its two UNLOGGED siblings: losing it costs an LLM pass over the whole grid, not a recompute from parquet.View command line instructions
Checkout
From your project repository, check out a new branch and test the changes.