试听音频抓不到文字内容:给课程站补 AudioObject 与文字稿的结构化改造
一、先说一个真实的尴尬场景
我们运营的职业技能课程站,每门课的详情页都挂了 10 分钟左右的试听音频,用的是一个纯播放器组件:一段 <audio> 标签加进度条,页面上没有一个字的文字稿。用户听完只能凭记忆回忆讲师讲了什么,AI 搜索这边更惨——用户在 DeepSeek、豆包里问「XX 认证考试备考要注意哪些坑」,答案里会引用别家博客的转述帖,就是不会引用我们试听音频里的第一手内容。

问题的根源不难猜:主流 AI 引擎的抓取器拿到页面后,读到的是一个指向 mp3 的 URL。它们既不会在抓取阶段下载音频文件,也不会实时听音频再转文字。对我们来说,10 分钟试听音频里最值钱的干货,在 AI 引擎眼里约等于不存在。
今年 3 月我们做了一次针对性改造,给试听音频补上 Schema.org 的 AudioObject 结构化标注,并用 Whisper(OpenAI 开源的语音识别模型)在本地批量生成了文字稿。改造上线后第 3 周,AI 搜索引用开始零星出现,第 6 周时试听页被引用次数从每周 0 次涨到 14 次,课程咨询量也有明显变化。下面把整个改造过程拆开讲。
二、原理剖析:为什么音频内容必须转成文字才能进 AI 引用管线
这是整个改造的核心机制问题,值得单独讲清楚。
当前主流 AI 引擎不做实时音频理解。 无论是 DeepSeek、豆包、Kimi 还是 Perplexity,它们的引用来源都是文本语料。抓取器在抓取页面时做的是 HTML 解析和正文抽取,遇到 <audio src="lesson01.mp3"> 这种标签,最多把 URL 记下来,不会去下载几 MB 的音频文件、跑一遍语音识别再决定要不要引用——抓取成本和数据清洗成本都不允许这么做。也就是说,音频内容对 AI 引擎而言是「不可索引的黑盒」,除非你把内容以文本形式主动递给它。
那怎么递?两条通路:
- 页面可见文字稿:把 transcript 直接渲染在试听页上,AI 爬虫正常抓取就能读到;
- 结构化标注(Schema.org):在页面的 JSON-LD 里通过 AudioObject 的
transcript属性显式声明文字稿,并和课程实体建立关联。
两条都做,效果叠加。页面可见的文字稿保证了内容可抓取,结构化标注则告诉 AI 引擎「这段文字就是那段音频的内容,而这段音频是这门课程的试听」——这就是 transcript 与 audio 的语义关联机制:transcript 属性挂在 AudioObject 实体上,AudioObject 又通过嵌套关系挂在 Course 实体下,AI 引擎做知识图谱对齐时,能把「课程 → 试听音频 → 文字稿」这条语义链完整还原出来。文字稿里提到的考点、讲法、案例,就有了明确归属,可以被引用到具体课程上,而不是变成一篇无主体的悬空文本。
这也是生成式引擎优化(Generative Engine Optimization, GEO)里容易被忽略的一块:大多数团队只做文本页面的优化,音视频内容的存量价值被白白丢掉了。
三、AudioObject 挂在 Course 下的正确嵌套写法
Schema.org 官方对 AudioObject 的定义在 https://schema.org/AudioObject,对 Course 的定义在 https://schema.org/Course。两者怎么组合,官方没有统一模板,但实践中有两种合理写法。
写法 A:Course → hasCourseInstance → courseWork / 直接挂 AudioObject 属性。 Course 类型本身没有 hasAudio 这种属性,实践里通行的做法是把试听音频作为 associatedMedia 挂在 Course 或 CourseInstance 上,属性值是 AudioObject。
写法 B:AudioObject 单独作为顶层实体,用 about 指回课程。 适合试听音频有独立落地页的场景。
我们的试听音频就在课程详情页里,所以选了写法 A,核心字段如下:
| 字段 | 作用 | 我们的取值示例 |
|---|---|---|
@type |
实体类型 | AudioObject |
contentUrl |
音频文件直链,抓取器可访问 | https://cdn.example.com/lesson01.mp3 |
duration |
时长,ISO 8601 格式 | PT10M30S |
encodingFormat |
文件 MIME 类型 | audio/mpeg |
transcript |
文字稿全文(或开头段落) | 讲师口述内容的文字版 |
name / description |
音频标题与描述 | 「第 1 课试听:真题里的三个高频陷阱」 |
特别注意 duration 必须用 ISO 8601 的 PT 表示法:10 分 30 秒写成 PT10M30S,纯数字秒数或「10:30」这种写法都不符合规范,解析器会直接丢弃。
下面是我们课程页实际的 JSON-LD 片段(简化后),依赖环境:任意后端模板引擎即可,前端也可以直接服务端注入。
// 课程详情页 JSON-LD 结构化数据(服务端模板渲染输出)
const courseJsonLd = {
"@context": "https://schema.org",
"@type": "Course",
"name": "PMP 六周冲刺班",
// 课程一句话描述,AI 引擎引用时常用作摘要
"description": "面向零基础考生的 PMP 认证六周系统课",
"provider": {
"@type": "Organization",
"name": "课程站技术示例",
// 机构官网地址
"url": "https://www.example.com"
},
// 试听音频通过 associatedMedia 挂在课程实体下
"associatedMedia": {
"@type": "AudioObject",
// 音频文件直链,必须是抓取器可访问的公开 URL
"contentUrl": "https://cdn.example.com/audio/lesson01-trial.mp3",
// 时长用 ISO 8601 格式:10 分 30 秒
"duration": "PT10M30S",
// 文件 MIME 类型
"encodingFormat": "audio/mpeg",
// 音频标题
"name": "第 1 课试听:真题里的三个高频陷阱",
// 文字稿全文,语音内容进 AI 引用管线的关键字段
"transcript": "大家好,今天这节试听课我们讲 PMP 真题里出现频率最高的三类陷阱题……",
// 内容语言标识
"inLanguage": "zh-CN"
}
};
// 挂载到页面 head 中的 script 标签
// <script type="application/ld+json"> 里放 JSON.stringify(courseJsonLd)
# 视频同源场景可选:多档音频码率时用 encoding 列表声明
# 这里演示同一音频的多个封装格式
audio_encoding = {
"@type": "AudioObject",
"contentUrl": "https://cdn.example.com/audio/lesson01-trial.mp3",
"duration": "PT10M30S",
"encodingFormat": "audio/mpeg",
"transcript": transcript_text, # 文字稿变量,由转写脚本生成
# 播放页面地址,方便回跳
"url": "https://www.example.com/course/pmp/trial/lesson01"
}
# 实际部署时 JSON-LD 由模板引擎在服务端拼装
# 避免前端动态注入导致爬虫抓不到
两个代码块,一个讲嵌套结构,一个讲多封装格式的备选写法,生产环境按需取用。
四、用 Whisper 本地批量生成 transcript
文字稿从哪来?我们有 217 门课、平均每门 2.2 段试听音频,人工转写不现实。选了 openai-whisper 在本地转写,模型是 base 与 small 两档混合使用——中文课程内容实测 small 档准确率明显更好,GPU 单卡转一段 10 分钟音频约 40 秒。
依赖安装(Python 3.9+,建议先装好 ffmpeg 并加入 PATH):
# 安装 openai-whisper 及其依赖
pip install -U openai-whisper
# 音频解码依赖 ffmpeg,Windows 可用 conda 安装
conda install -c conda-forge ffmpeg
# 转写脚本运行命令示例,lang 指定中文
whisper lesson01-trial.mp3 --model small --language zh
批量转写的 Python 脚本骨架如下:
# -*- coding: utf-8 -*-
# 批量转写课程试听音频并输出 transcript 文本
import os
import json
import whisper
# 加载 small 档模型,中文场景准确率比 base 档好一截
model = whisper.load_model("small")
# 音频文件所在目录
audio_dir = "D:/trial_audio/batch_202603"
# 转写结果输出目录
out_dir = "D:/trial_audio/transcripts"
for fname in sorted(os.listdir(audio_dir)):
# 只处理 mp3 与 m4a 格式
if not fname.lower().endswith((".mp3", ".m4a")):
continue
audio_path = os.path.join(audio_dir, fname)
# 执行转写,language 参数显式指定中文
result = model.transcribe(audio_path, language="zh")
# 拼接全部分段文本为一份完整文字稿
transcript = "".join(seg["text"].strip() for seg in result["segments"])
# 按同名规则写出 json 结果,便于后续入库
out_path = os.path.join(out_dir, os.path.splitext(fname)[0] + ".json")
with open(out_path, "w", encoding="utf-8") as f:
json.dump({"audio": fname, "transcript": transcript}, f, ensure_ascii=False)
# 打印进度,方便批量任务观察
print(f"完成转写: {fname} -> {len(transcript)} 字")
转写完的 transcript 走了两步处理再上线:一是人工抽查了 30 段修正专有名词(讲师口播里的英文缩写、产品名,Whisper 偶尔会转错);二是文字稿同时渲染到试听页面上(折叠面板形式,默认展开前两段),保证纯文本爬虫也能读到。
五、课程页与转写管线的整体结构
两图回顾全貌。先是课程页的结构化数据嵌套关系:
graph TD
A[Course 课程实体] --> B[CourseInstance 期次]
A --> C[associatedMedia]
C --> D[AudioObject 试听音频]
D --> E[contentUrl 音频直链]
D --> F[duration PT10M30S]
D --> G[encodingFormat audio/mpeg]
D --> H[transcript 文字稿全文]
A --> I[provider 机构实体]
再是音频内容从原始文件到被 AI 引擎引用的完整管线:
graph LR
A[试听音频 mp3] --> B[Whisper 本地转写]
B --> C[transcript 文本]
C --> D[人工抽查修正]
D --> E[试听页渲染文字稿]
D --> F[JSON-LD transcript 字段]
E --> G[AI 引擎抓取页面文本]
F --> H[结构化解析建立语义关联]
G --> I[进入引用候选池]
H --> I
I --> J[AI 搜索回答中引用试听内容]
六、改造前后 6 周对照
单站观测数据(仅本站后台与日志统计,样本量有限,仅供参考)。统计起点为 3 月 9 日 JSON-LD 上线,文字稿页面渲染 3 月 12 日完成。
| 观测项 | 改造前 4 周均值 | 第 1-2 周 | 第 3-4 周 | 第 5-6 周 |
|---|---|---|---|---|
| 试听页被 AI 引用次数(周) | 0 | 2 | 7 | 14 |
| 引用来源覆盖的 AI 引擎数 | 0 | 1 | 3 | 4 |
| 试听页自然搜索点击(周) | 86 | 92 | 128 | 155 |
| 课程咨询量(周,含表单+客服) | 61 | 58 | 73 | 89 |
几点补充观察:
- 引用出现滞后约 3 周,符合结构化数据被重新抓取、解析、进入索引的常规周期,第 1 周那 2 次引用出现在 Kimi,恰好是它近期重抓了我们全站;
- 咨询量里「直接问试听课内容」的会话占比从接近 0 涨到约 22%,说明引用流量在往转化漏斗里走;
- 有 3 次引用来自 transcript 里的「陷阱题」表述,这种讲师口语里的高频关键词,文本页面反而没写。
七、两个容易踩的坑
坑一:PodcastEpisode 的播客经验不能照搬。 社区里关于音频结构化的文章大多在讲 PodcastEpisode 配 transcript,播客和课程试听是两类实体:播客是独立内容单元,课程试听是课程的附属物料。试听音频挂 associatedMedia 而不是另造 PodcastEpisode,否则 AI 引擎做实体对齐时会把试听音频理解成一个独立节目,语义归属反而乱了。
坑二:transcript 别只塞 JSON-LD。 有段时间我们的文字稿只存在于结构化数据里、页面上不渲染,某类只做正文抽取的抓取器完全读不到。文字稿必须双通道:页面可见 + 结构化标注,缺一不可。
趋势上判断一点:多模态输入的 AI 引擎迟早会直接理解音频,但文本 transcript 作为最稳定的语义载体,在可预见的周期内仍是语音内容被引用的基础设施。这个改造做完,站点结构化的地基又厚了一层,和传统的 SEO 收录优化互不冲突,收尾一句点到:网页侧的收录排序问题是另一条线,后续单独展开。
参考与延伸
- Schema.org AudioObject 官方定义:https://schema.org/AudioObject
- Schema.org Course 官方定义:https://schema.org/Course
- openai-whisper 开源仓库(安装说明与模型列表):https://github.com/openai/whisper
关键词:GEO、AI优化AIO、课程被 AI 推荐、AudioObject、transcript 文字稿、语音转写、JSON-LD、课程结构化