# 朗读
Source: https://blume.ndjp.net/docs/configuration/narration/
English: https://useblume.dev/docs/configuration/narration

朗读会在每个页面的描述下方加一个 **收听本页**播放器。读者按下播放，页面就从标题开始逐句朗读，被读到的句子会高亮并保持在视野内。它需要手动启用：

```ts blume.config.ts lineNumbers
export default defineConfig({
  narration: true,
});
```

设为 `true` 时，页面使用读者设备自带的[浏览器语音](#browser-voices)朗读：无需密钥，无需构建步骤，在任何托管平台上都能工作，静态的或非静态的都可以。加上一个 `provider` 则改用[生成的语音](#generated-voices)。

## 它朗读哪些内容 [#what-it-reads]

朗读会按顺序读出页面标题、描述、各级标题、段落、列表项和卡片文字。有些组件前面会加一句简短的语音提示，让听者知道接下来是什么内容：

| 组件 | 提示 |
| --- | --- |
| [提示框](/docs/content/syntax/) | 「注意。」「提示。」「警告。」等等 |
| [Steps](/docs/content/components/) | 「第 1 步。」「第 2 步。」 |
| [Tabs](/docs/content/components/) | 「macOS 标签页。」每个标签页都会被朗读，不只是当前打开的那个 |
| [Accordion](/docs/content/components/) 和 [Expandable](/docs/content/components/) | 「可展开区块。」 |

当朗读遇到折叠起来的区块或未显示的标签页时，会把它展开，这样高亮总能落在读者看得见的文字上。

代码块、表格、图片、视频、图表、公式、类型展示、文件树和实时组件预览都会被跳过 —— 它们读出来没有意义。

只有正文足够多、值得一听的页面才会出现播放器，大约 50 词以上。很短的页面，以及大部分由代码或 API 字段构成的页面，都不会出现播放器。

## 收听 [#listening]

页面播放时，播放器会固定在页头下方，包含播放与暂停、上一句与下一句、进度条、已读时间与页面总长度的对比，以及 0.8× 到 2× 的倍速控制。这个倍速会被记住，下一页继续沿用。

页面会自动滚动，让正在朗读的句子保持在视野内。如果读者自己滚走了，跟随就会停止而音频继续播放；滚回那个句子，或者按下 **跟随朗读**，就会重新跟上。打开另一个页面会停止朗读。

## 浏览器语音 [#browser-voices]

`narration: true` 使用 [Web Speech API](https://developer.mozilla.org/docs/Web/API/SpeechSynthesis) 和读者设备上的语音来朗读页面，并按页面语言挑选一个合适的声音。如果浏览器没有该语言的声音，播放器会保持隐藏，而不是用错误的口音把页面念出来。

这不需要你提供任何东西，也不产生任何费用，但效果取决于设备：macOS、iOS 和 Windows 上的系统语音都不错，而某些 Linux 浏览器自带的语音很少，甚至一个都没有。

## 生成的语音 [#generated-voices]

传入一个 `provider`，就能在构建时用神经语音模型生成音频。这个 provider 就是 assistant 使用的同一个 [`gateway()`](/docs/configuration/assistant/)适配器，只是指向一个语音模型：

```ts blume.config.ts lineNumbers
import { defineConfig } from "blume";
import { gateway } from "blume/ai";

export default defineConfig({
  narration: {
    provider: gateway({
      model: "openai/tts-1-hd",
      voice: "alloy",
    }),
  },
});
```

`blume build` 会完全按播放器的方式读取每个构建好的页面，把它拆成句子，并通过 [Vercel AI Gateway](https://vercel.com/docs/ai-gateway) 为每个句子生成一段音频。这些音频和每页一份的小清单会作为静态文件写入你的构建产物，放在 `/blume-narration/` 下，因此重播不花任何代价，也没有任何东西在服务器上运行。

在生成任何东西之前，构建会先打印它需要多少段新音频、总共覆盖多少字符，让成本一目了然：

```txt
Generating narration: 214 new clip(s), 15,880 characters, with openai/tts-1-hd
```

| 选项 | 默认值 | 说明 |
| --- | --- | --- |
| `model` | `openai/tts-1-hd` | gateway 的语音模型：`openai/tts-1`、`openai/tts-1-hd`、`fish-audio/s2.1-pro`、`spacexai/grok-tts`，以及 gateway 列出的其它模型。 |
| `voice` | `alloy` | 模型使用的声音。 |
| `instructions` |  | 指定声音该如何朗读，适用于接受指令的模型（「像一位老师那样，平静地朗读」）。 |
| `apiKeyEnv` | `AI_GATEWAY_API_KEY` | 保存 gateway 密钥的环境变量。在 Vercel 上，构建的 OIDC token 同样可用。 |
| `headers` |  | 随每次请求发送的静态响应头。 |
| `providerOptions` |  | 原样传给 AI SDK 的 `generateSpeech`，用于 Blume 没有具名列出的模型设置。 |

### 缓存 [#caching]

音频会按决定其声音的那些条件缓存在 `node_modules/.cache/blume/narration` 下：句子本身、它的语言、模型、声音和指令。重新构建时只需为发生变化的句子付费，被多个页面共用的句子只生成一次。Vercel 和 Netlify 会从各自的构建缓存中恢复 `node_modules`，所以在那里部署只会重新生成变化的部分。在其它 CI 上，请在两次运行之间缓存这个目录。

### 回退 [#fallbacks]

在没有音频的地方，生成的语音会回退到浏览器语音：

- 在 `blume dev` 中，它从不生成音频。运行 `blume build` 和 `blume preview` 就能在本地听到生成的语音。
- 构建时未设置密钥。构建会发出警告并跳过生成。
- 生成失败时。由于 AI SDK 已经重试过，构建会在第一段失败的音频处停下，并保留所有已缓存的音频。

## 语言 [#languages]

朗读会用每个页面自身的内容语言进行，因此在一个[国际化](/docs/content/i18n/)的站点上，每个 locale 都用自己对应的声音朗读。这些语音提示和播放器标签在每个内置 UI 语言中都已翻译。要按 locale 覆盖它们，在 [`i18n.ui`](/docs/content/i18n/) 中改写 `narration` 下的内容。

## 为单个页面关闭它 [#turning-it-off-for-a-page]

在页面的 [frontmatter](/docs/content/frontmatter/) 中设置 `narration: false`，就能让该页面不出现播放器：

```yaml
---
title: Changelog
narration: false
---
```

## 让内容不被朗读 [#keeping-content-out]

给某个元素加上 `data-blume-narration="skip"`，就能让它及其内部的一切都不参与朗读。Blume 就是靠它跳过自己的类型展示和预览的，对你自己的组件和交互岛同样有效：

```html
<div data-blume-narration="skip">
  <PricingCalculator />
</div>
```

## 分析 [#analytics]

读者开始收听时，播放器会发送一个 `narration_play` 事件，附带 `engine`（`audio` 或 `browser`）和页面的 `path`；页面播放到结尾时再发送 `narration_complete`。这两个事件和其它自定义事件一样，都会经过你的[分析](/docs/configuration/analytics/)适配器。