A Hacker News story as audio
An action reads a pinned Hacker News story, vision reads the page it links, the model writes three sentences and the node's speech model reads them out.
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.
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
bashAPI=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
tsimport { writeFileSync } from "node:fs";const API = "https://beta-api.b3os.org";const headers = { authorization: `Bearer ${process.env.B3OS_API_KEY}`, "content-type": "application/json" };const post = (path: string, body: unknown) => fetch(`${API}${path}`, { method: "POST", headers, body: JSON.stringify(body) });const id = 50319510;const item: any = await (await post("/v1/actions/call", { ref: "io.b3os/hacker-news/get-item@1", input: { id } })).json();const story = item.output;const page: any = await (await post("/v1/vision/scrape", { url: story.url, formats: ["markdown"] })).json();const chat: any = await ( await post("/v1/infer/chat/completions", { 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: `Title: ${story.title}\n\n${page.markdown.slice(0, 6000)}` }, ], })).json();const speech = await post("/v1/infer/audio/speech", { model: "kokoro-82m", input: chat.choices[0].message.content, voice: "alloy", response_format: "mp3",});writeFileSync("brief.mp3", Buffer.from(await speech.arrayBuffer()));
pythonimport osimport requestsAPI = "https://beta-api.b3os.org"s = requests.Session()s.headers["Authorization"] = f"Bearer {os.environ['B3OS_API_KEY']}"ID = 50319510story = s.post(f"{API}/v1/actions/call", json={ "ref": "io.b3os/hacker-news/get-item@1", "input": {"id": ID},}).json()["output"]page = s.post(f"{API}/v1/vision/scrape", json={"url": story["url"], "formats": ["markdown"]}).json()chat = s.post(f"{API}/v1/infer/chat/completions", json={ "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": f"Title: {story['title']}\n\n{page['markdown'][:6000]}"}, ],}).json()speech = s.post(f"{API}/v1/infer/audio/speech", json={ "model": "kokoro-82m", "input": chat["choices"][0]["message"]["content"], "voice": "alloy", "response_format": "mp3",})with open("brief.mp3", "wb") as f: f.write(speech.content)
The remote B3OS MCP server has the action, scrape and chat tools but no speech tool yet, so the agent writes the summary over MCP and the audio is one HTTP call (the curl tab's last command). Ask your agent:
textGet Hacker News item 50319510 with the hacker-news get-item action. Scrape the page it links as Markdown andsummarize it in three plain sentences for listening, at most 70 words.
The agent calls actions_call, vision_scrape and infer_chat.
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
json{ "story_title": "HackerTube: YouTube built from videos recommended by HN", "story_url": "https://karpathy.bearblog.dev/hacker-tube/", "story_points": 1009, "scrape_status": 200, "scrape_tier": "cache", "markdown_chars": 2373, "model": "b3os/qwen3.5-9b", "summary": "The author built HackerTube, a YouTube clone that displays videos recommended by Hacker News comments instead of standard algorithms. This project filtered 40,000 comments to find 6,454 high-quality videos spanning 150 topics from 2007 to 2026. The creator used an AI agent to handle most of the development, requiring only minor manual tasks like setting up a domain and API keys.", "audio_type": "audio/mpeg", "audio_bytes": 394413, "audio": "https://beta.b3os.org/gallery/recipes/news-audio-brief/173672af3d386a44.mp3", "scrape_ms": 65.8, "summary_ms": 544.2, "speech_ms": 301.2, "total_ms": 1269.8}
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
.