跳到主要内容

文档音频工作流 (Docs TTS)

文档分类下的文章(如 docs/LookAround/)支持 TTS 朗读音频播放。与博客音频不同,文档音频播放器自动注入到页面布局中,无需在每篇文章的 MDX 文件里手动引入组件。

当前已接入音频的文档分类:

分类目录文章数中文音频英文音频
东张西望 (LookAround)docs/LookAround/8已生成已生成

架构概览

文档音频系统复用了博客音频播放器,但使用独立的文档音频清单和独立 OSS 路径。这样博客和文档可以共享播放体验,又不会互相污染 manifest。

组件关系

BlogAudioPlayer (核心播放逻辑)
├── BlogPostPage 自动注入:<BlogAudioPlayer slug={audioSlug} />
│ ├── slug 由 getBlogAudioSlug(metadata) 从 permalink 推导
│ └── 读取 blogAudioManifest.json(默认清单)

└── DocsAudioPlayer (薄包装)
└── <DocsAudioPlayer slug="xxx" />
└── 传入 docsAudioManifest.json + keyPrefix="docs/"

关键文件

文件作用
src/components/BlogAudioPlayer/index.js核心播放器组件,支持 manifestkeyPrefix props
src/components/DocsAudioPlayer/index.js文档音频薄包装,传入文档清单和前缀
src/theme/BlogPostPage/index.js博客页面布局 swizzle,自动注入博客播放器
src/utils/blogAudio.js从 permalink 推导博客音频 slug
src/utils/lookAroundDocs.jsLookAround 文档检测工具函数
src/theme/DocItem/Layout/index.js文档布局,自动注入文档播放器
src/data/blogAudioManifest.json博客音频清单(静态 import)
src/data/docsAudioManifest.json文档音频清单(静态 import)
static/audio/blog/manifest.json博客音频清单(备用访问路径)
static/audio/docs/manifest.json文档音频清单(备用访问路径)

清单文件结构

docsAudioManifest.json 的 key 格式:

  • 中文:docs/{slug},如 docs/omega-horizontal-vertical-analysis
  • 英文:en/docs/{slug},如 en/docs/omega-horizontal-vertical-analysis

每个条目包含:

{
"docs/omega-horizontal-vertical-analysis": {
"urls": [
"https://oss.nevergpdzy.com/Audio/docs/omega-horizontal-vertical-analysis_001.mp3",
"https://oss.nevergpdzy.com/Audio/docs/omega-horizontal-vertical-analysis_002.mp3"
],
"voice": "茉莉",
"generatedAt": "2026-05-02T06:10:34.705519+00:00"
}
}

OSS 路径

内容OSS 路径
中文文档音频Audio/docs/{slug}.mp3Audio/docs/{slug}_001.mp3
英文文档音频Audio/docs/en/en_{slug}.mp3Audio/docs/en/en_{slug}_001.mp3
文档音频清单Audio/docs/manifest.json

文档音频 manifest 中的公开 URL 必须使用 https://oss.nevergpdzy.com/,不要把已退役 picture 域名重新写回 src/data/docsAudioManifest.jsonstatic/audio/docs/manifest.json

与博客音频(Audio/blog/)完全独立,互不干扰。

生成策略

文档文章通常比博客长,而且包含表格、参考资料和图片资源段落。生成器的 docs 模式做了这些处理:

  1. 提取纯文本,去除 frontmatter、代码块、JSX、图片、原始 URL、来源/参考资料/图片资源等不适合朗读的段落。
  2. 在处理普通 Markdown 之前先转换表格,把每行转换成适合朗读的短句,而不是直接朗读竖线和分隔符。
  3. 按段落边界分块,默认 --chunk-char-limit 2400,必要时可以对单篇文章降低到 1200 之类的更稳值。
  4. 并发只发生在文章级别,--article-jobs 会同时处理多篇文章;同一篇文章内部的 chunk 始终串行生成,保证音频顺序和文字顺序严格一致。
  5. 每个 chunk 生成后用 ffprobe 检查时长,明显过短或截断的音频会删除并重试。
  6. 只要有文章失败,本次 publish manifest 不会上传,避免发布部分缺段的清单。

中文语音使用「茉莉」,提示词强调缓慢、温柔舒缓、自然停顿。英文语音使用 Chloe,保持自然流畅朗读提示词,不额外强制舒缓节奏。不要再用 atempo 或其他 MP3 后处理去改变语速;语速应由 TTS 提示词和分块稳定性控制。

生成音频

全量生成中文 LookAround 音频

cd ../tts-blog-generator
python generate.py --type docs --lang zh --force --article-jobs 2

默认扫描 ../Dev-Knowledge-Base/docs/LookAround

全量生成英文 LookAround 音频

cd ../tts-blog-generator
python generate.py --type docs --lang en \
--blog-dir "../Dev-Knowledge-Base/i18n/en/docusaurus-plugin-content-docs/current/LookAround" \
--force --article-jobs 2

英文音频使用 Chloe 语音,文件名带 en_ 前缀,存储在 OSS 的 Audio/docs/en/ 目录下。

只重生成单篇文章

cd ../tts-blog-generator

# 中文
python generate.py --type docs --lang zh \
--include mercedes-benz-g-class-history-category-industry-position \
--force --article-jobs 1

# 英文
python generate.py --type docs --lang en \
--blog-dir "../Dev-Knowledge-Base/i18n/en/docusaurus-plugin-content-docs/current/LookAround" \
--include mercedes-benz-g-class-history-category-industry-position \
--force --article-jobs 1

如果某篇中文长文出现短音频、乱码感朗读或 2 分钟左右突然读错内容,先检查抽取文本,再降低分块上限重新生成:

python generate.py --type docs --lang zh \
--include mercedes-benz-g-class-history-category-industry-position \
--force --article-jobs 1 --chunk-char-limit 1200

这次 G-Class 中文音频就是按 1200 字左右重新分成 15 段生成,替代了原来的 7 段长 chunk。

常用参数

参数说明
--type docs生成文档音频(默认扫描 docs/LookAround/
--lang zh / --lang en指定语言
--blog-dir <path>自定义扫描目录,英文文档必须显式传入英文镜像目录
--include <slug>只处理指定 slug 或文件名,适合单篇修复
--force强制重新生成,并移除目标文章的旧 manifest 条目
--article-jobs <n>文章级并发数;同一篇文章内 chunk 仍然串行
--chunk-char-limit <n>单个 TTS chunk 的最大字符数
--dry-run预览分块和文件名映射,不调用 API
--skip-upload跳过 OSS 上传

文本检查

重新生成有问题的文章前,必须检查要送给 TTS 的文本。推荐用生成器的抽取函数导出检查文件:

cd ../tts-blog-generator

python - <<'PY'
from pathlib import Path
import generate

slug = "mercedes-benz-g-class-history-category-industry-position"
path = Path("../Dev-Knowledge-Base/docs/LookAround") / f"{slug}.md"
text = generate.extract_text(path)
chunks = generate.chunk_text(text, limit=1200)

Path("output").mkdir(exist_ok=True)
Path("output/check_zh_gclass.txt").write_text(
"\n\n".join(
f"=== chunk {i:03d} / {len(chunks)} ({len(chunk)} chars) ===\n{chunk}"
for i, chunk in enumerate(chunks, 1)
),
encoding="utf-8",
)
print(len(text), [len(chunk) for chunk in chunks])
PY

检查重点:

  • 没有 等乱码特征。
  • 没有原始 URL、Markdown 图片、表格分隔符、参考资料附录。
  • chunk 编号、文件名和正文顺序一致。
  • 表格被转换成可读句子,而不是竖线和分隔线。

注意:Windows PowerShell 直接 Get-Content 中文文件时可能显示乱码,这是控制台编码问题。以 Python encoding="utf-8" 读出的文本为准。

同步清单

生成器现在会同时写:

  • output/manifest.json:包含博客和文档的总清单。
  • output/docs-manifest.json:只包含文档音频条目,适合复制到站点仓库。
  • output/blog-manifest.json:只包含博客音频条目。

文档音频生成完成后,从 tts-blog-generator 目录复制文档专用清单:

cp output/docs-manifest.json ../Dev-Knowledge-Base/src/data/docsAudioManifest.json
cp output/docs-manifest.json ../Dev-Knowledge-Base/static/audio/docs/manifest.json

不要再把 output/manifest.json 直接复制到 docsAudioManifest.json,否则可能把博客条目混入文档清单。

播放器集成方式

自动注入(当前方式)

文档音频播放器在 src/theme/DocItem/Layout/index.js 中自动注入,无需修改任何 .md 文件。

import {isLookAroundDocMetadata, getLookAroundDocSlug} from '@site/src/utils/lookAroundDocs';
import DocsAudioPlayer from '@site/src/components/DocsAudioPlayer';

const isLookAround = isLookAroundDocMetadata(metadata);
const lookAroundSlug = isLookAround ? getLookAroundDocSlug(metadata) : null;

<DocBreadcrumbs />
<DocVersionBadge />
{isLookAround && lookAroundSlug && <DocsAudioPlayer slug={lookAroundSlug} />}
{docTOC.mobile}

检测函数 isLookAroundDocMetadata() 通过 metadata.sourceDirName === 'LookAround' 判断当前文档是否属于 LookAround 分类。

手动引入(备选方式)

如果需要在特定文档中手动控制播放器位置,也可以在 MDX 文件中直接引入:

import DocsAudioPlayer from '@site/src/components/DocsAudioPlayer';

<DocsAudioPlayer slug="your-doc-slug" />

slug 规则:文件名去掉扩展名。例如 omega-horizontal-vertical-analysis.md 对应 omega-horizontal-vertical-analysis

添加新文档分类的音频支持

如果需要为其他文档分类添加音频支持:

  1. src/utils/ 下新建检测工具函数,参照 lookAroundDocs.js
  2. src/theme/DocItem/Layout/index.js 中导入并添加条件渲染。
  3. 运行 python generate.py --type docs --blog-dir "../Dev-Knowledge-Base/docs/YourCategory" 生成中文音频。
  4. 运行英文镜像目录的生成命令。
  5. output/docs-manifest.json 合并或复制到 src/data/docsAudioManifest.jsonstatic/audio/docs/manifest.json

如果新分类需要不同 OSS 路径前缀,需要同步调整 generate.py 中的 docs OSS key 生成逻辑和播放器的 keyPrefix 约定。

排查与验证

本地音频时长检查

cd ../tts-blog-generator
ffprobe -v error -show_entries format=duration,size -of json output/your-file.mp3

长中文 chunk 如果只有几十秒或明显短于同等长度 chunk,通常是 TTS API 返回了截断音频,应删除并重试。现在生成器会自动拦截大多数这种情况;如果仍然可疑,降低 --chunk-char-limit

OSS URL 检查

新增或重生成后至少探测目标文章的 URL 是否可访问,确认返回 audio/mpeg。可以用脚本批量 HEAD 当前 manifest 中的目标文章 URL。

构建验证

cd ../Dev-Knowledge-Base
npm run build

构建必须同时通过 zh-Hansen

测试清单

新增或更新文档音频后,至少检查:

  • 已用 UTF-8 检查抽取文本,没有乱码和非朗读材料。
  • 中文音频已通过 python generate.py --type docs --lang zh ... 生成。
  • 英文音频已通过 python generate.py --type docs --lang en --blog-dir "..." 生成。
  • 对长中文文章必要时使用更小的 --chunk-char-limit
  • docsAudioManifest.json 已从 output/docs-manifest.json 同步到 src/data/static/audio/docs/
  • src/data/docsAudioManifest.jsonstatic/audio/docs/manifest.json 内容完全一致。
  • 目标文章的 OSS 音频 URL 全部可访问。
  • npm run build 通过。
  • 访问 LookAround 文章时播放器出现在面包屑下方。
  • 播放按钮可点击,音频正常播放。
  • 多段音频按 _001_002 顺序衔接。
  • 进度条显示正确总时长。
  • 倍速切换正常。
  • 键盘快捷键正常:Space 播放/暂停,←/→ 快退/快进 5 秒,Shift+←/→ 快退/快进 30 秒。
  • 在搜索框、评论区等输入框中输入时,键盘快捷键不会干扰。
  • 访问非 LookAround 文档时播放器不出现。
  • 切换 locale 后读取对应语言的音频。
  • OSS 防盗链白名单包含当前域名(生产 + localhost)。