> ## Documentation Index
> Fetch the complete documentation index at: https://docs.beta.b3os.org/llms.txt
> Use this file to discover all available pages before exploring further.

# 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

**curl**

```bash
API=https://beta-api.b3os.org
AUTH="Authorization: Bearer $B3OS_API_KEY"

ID=50319510

URL=$(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.md

jq -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.txt

jq -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
```
**TypeScript**

```ts
import { 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()));
```
**Python**

```python
import os
import requests

API = "https://beta-api.b3os.org"
s = requests.Session()
s.headers["Authorization"] = f"Bearer {os.environ['B3OS_API_KEY']}"

ID = 50319510
story = 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)
```
**MCP**

The remote [B3OS MCP server](/sdks/mcp) 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:

```text
Get Hacker News item 50319510 with the hacker-news get-item action. Scrape the page it links as Markdown and
summarize 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

**Command**

```bash
$ node run.mjs
# exit 0
```
**Outputs**

```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`.