live · one briefing, every evening

Evening news briefing

Vesper · Python · local qwen3:14b · scheduled 5 p.m. · Pydantic

Built for:
One person whose inbox had turned into a dozen overlapping AI feeds and four homegrown briefing bots — and who wanted to read one thing in the evening, not thirty.
Not built for:
A team newsroom. Vesper ranks the day against one person’s projects and writes for one reader; the relevance model is personal on purpose.

By the evening my inbox held a dozen newsletters, several feeds, and the output of four briefing bots I’d built earlier — all overlapping, none synthesized. Vesper collects all of it, ranks it against what I’m actually working on, and hands a local model the survivors to explain in plain language. What lands is one email: tonight’s news, in tiers, with a short list of what to actually do about it.

§ I

The problem

The cost of staying informed isn’t finding the news — there’s too much of it — it’s the synthesis nobody does for you. Twelve sources tell you about the same model launch in twelve voices, and none of them tell you whether it matters to the thing on your desk. Worse, I’d built the part of the problem myself: a morning brief, a video scout, an inbox triage, each emailing me separately.

So Vesper is an aggregator that sits on top of the others. It eats their output and everyone else’s, dedupes the overlap, ranks what’s left against my projects, and replaces the whole flood with one synthesized read. The feeders still run; their emails are suppressed so only Vesper’s lands.

§ II

Decisions

  1. local synthesis

    The ranking and the writing run on a local model — qwen3:14b through Ollama, on the same machine that collects the feeds. The judgment about what matters to my work, and the words that explain it, never leave the box. A produce step can add narration and illustration through cloud APIs when I want a richer page, but the thinking is local and free.

  2. explain it plainly

    Every surviving item gets three things from the model: a plain-language explanation with an analogy, an importance tier, and one honest line about whether it touches anything I’m building — or the exact words “No direct relevance.” The tiers are forced to spread, so a quiet day reads like a quiet day. It’s synthesis, not a link dump.

  3. one email

    The whole point is de-flooding, so the output is exactly one message a day. The feeders it aggregates are flagged to stay silent; their work flows into Vesper instead of into my inbox. Dozens of notifications collapse into a single evening briefing with a short, verb-led action list at the top.

§ III

System

Six stages. Collection fans in from many source types; ranking and dedup cut the volume down; a local model synthesizes the survivors into tiered stories and an action list; a produce step renders the page; one email goes out.

Collect36 feedsRankdedup @0.85Synthesizeqwen3:14bProducepageServelocalhostDeliverone emailRSS · Reddit · Gmail label · the four feeder bots
FIGURE 1. Many sources in, one email out. The accented stages are the local model that does the thinking and the single message that replaces the flood. The four earlier briefing bots feed in here instead of mailing me separately.
Stack — current pins.
LayerImplementationPurpose
ScheduleWindows Task · 5 p.m.One run, one email, daily
Collect36 feeds · 9 source typesRSS · Reddit · Gmail label · feeders
RankKeyword relevance + dedup @0.85Cut the volume to what matters
SynthesizeOllama · qwen3:14b (local)Explain · tier · project impact
ProduceHTML page · optional narrationSkippable, illustrated presentation
DeliverSMTP · one messageReplaces dozens of feeder emails
vesper/synthesize.pypython · local-model output schema
# Each surviving item is handed to a local qwen3:14b with a
# strict contract: explain it like I'm twelve (with an analogy),
# assign an importance tier, and name the ONE project it touches
# — or say so plainly. Pydantic validates every reply.
class Story(BaseModel):
    item: Item
    explanation: str   # plain-language, with an analogy
    tier: int          # 1 = big news … 3 = quiet; tiers spread
    impact: str        # the project it touches, or "No direct relevance."

class Brief(BaseModel):
    stories: list[Story]
    action_list: list[str]   # 3–6 items, each starts with a verb
    weeded_out: list[Item]   # what was collected but cut
story.jsonone synthesized item
{
  "explanation": "A new open model that runs on a normal
   GPU — like getting a sports-car engine that fits a
   sedan. You no longer have to rent the cloud to use it.",
  "tier": 2,
  "impact": "Companion — a smaller local model to test
   against the current Ollama backend.",
  "_then": "rolled into the day's 3–6 verb-led actions"
}
FIGURE. One item, synthesized by the local model into the shape the email is built from: a plain explanation, a forced tier, and an honest impact line tied to a real project. Everything the rank step cut is kept in weeded_out, so nothing is silently dropped.
§ IV

Where it stands

Live, on a daily 5 p.m. schedule. It collects from thirty-six feeds across nine source types, ranks a couple thousand raw items down to the handful worth reading, and ships one briefing. The whole synthesis runs locally; the only cloud is an optional polish layer for narration and illustration when I want the richer page. It went from empty repo to running pipeline in two days — and it’s the one that finally made my own inbox quiet.

Acknowledgments

Vesper stands on Ollama and qwen3:14b for local synthesis, Pydantic for a strict contract with a model that wants to ramble, feedparser and the Gmail API for collection, and the idea that the right number of evening emails is one.

← Index