Four calls with one key: io.b3os/hacker-news/get-item reads one story by id (no key needed), vision/scrape turns the page it links into Markdown, b3os/qwen3.5-9b writes three sentences meant to be heard, and infer/audio/speech reads them with kokoro-82m. The model and the speech model run on the same node as the scrape, so the only trips across the internet are to Hacker News's API and to the story's site.

The tabs read story 50319510, the one the published run below is pinned to: a person read it before it was pinned, because the summary is spoken in the node's voice on this page. To brief today's top story, take the id from io.b3os/hacker-news/get-front-page first ({"hits_per_page": 5}): each of its stories has an id, which get-item takes as a number.

Check

Verified 2026-11-01: this recipe's last run passed against https://beta-api.b3os.org, label live-recorded (recorded from production), from commit 771a9508. The recipe harness runs it again, and a failed run takes this page out of the cookbook.

Run it

bash
API=https://beta-api.b3os.orgAUTH="Authorization: Bearer $B3OS_API_KEY"ID=50319510URL=$(curl -s "$API/v1/actions/call" -H "$AUTH" -H "Content-Type: application/json" \ -d "$(jq -n --argjson id "$ID" '{ref: "io.b3os/hacker-news/get-item@1", input: {id: $id}}')" \ | jq -r '.output.url')curl -s "$API/v1/vision/scrape" -H "$AUTH" -H "Content-Type: application/json" \ -d "$(jq -n --arg url "$URL" '{url: $url, formats: ["markdown"]}')" | jq -r '.markdown[0:6000]' > page.mdjq -n --rawfile page page.md '{model: "b3os/qwen3.5-9b", temperature: 0, max_tokens: 160, chat_template_kwargs: {enable_thinking: false}, messages: [{role: "system", content: "Summarize the page for someone listening, in three plain sentences and at most 70 words. Use only the page. No lists, no URLs, no markup."}, {role: "user", content: $page}]}' \| curl -s "$API/v1/infer/chat/completions" -H "$AUTH" -H "Content-Type: application/json" -d @- \| jq -r '.choices[0].message.content' > summary.txtjq -n --rawfile text summary.txt '{model: "kokoro-82m", input: $text, voice: "alloy", response_format: "mp3"}' \| curl -s "$API/v1/infer/audio/speech" -H "$AUTH" -H "Content-Type: application/json" -d @- -o brief.mp3

The player below is the last published run's, spoken by the node in production. The recipe harness publishes a run of this recipe only while the story's id, title and link and the sha256 of the Markdown the model read (page_sha256) match the published run's (recipe.json input and pins); a run whose page changed waits for a person to read it (node examples/recipes/_harness/run.mjs --review). The summary is still the model's reading of someone else's page; check it against the page before you quote it.

Owner: Docs. Products: Docs, Actions and triggers, Vision, Infer.

Output of the last green run

bash
$ node run.mjs# exit 0

The outputs this recipe publishes (its recipe.json publish.outputs), with workspace identifiers redacted. The run's stdout, stderr and other outputs are not published.

Captured 2026-11-01 from commit 771a9508 , label live-recorded (recorded from production). Case recipes/news-audio-brief .

Ask a question... ⌘I