播客音频提取实体方案
一、背景
需求:指定播客频道,从以往所有播客中提取出实体列表,比如公司列表。
- 播客音频规模:数量大概 500 条,每个时长在 1 小时~2 小时之间
- 速度要求:离线处理,不需要实时快速处理,但是离线处理要尽量快
- 预算:0
二、方案
总体方案:
- 根据频道获取所有播客列表
- 获取所有音频
- 所有音频转文字
- 使用 llm 根据文字提取出实体列表
三、获取播客列表和音频
找到播客频道的 rss,rss 里面有该频道的所有播客列表
比如下面的:
- 知行小酒馆:https://feed.xyzfm.space/j8yp8gxkmgqr
- 中场沉思录:https://feed.xyzfm.space/jtvfkcxqmnkg
- 无人知晓:https://feed.xyzfm.space/ypn9dydpbxpc
- 面基:https://feed.xyzfm.space/6hpdgggtxpxb
里面包括了该频道所有的播客列表,每个播客都有具体的信息还有音频的下载链接,并且 rss 和音频下载链接直接访问就行,不需要鉴权
刚开始的时候我是直接调用小宇宙的接口来获取播客列表,然后得到每一集的详细页面,接着可以直接访问这个页面,拿到网页的 HTML,从网页的 HTML 里面解析得到音频的下载链接。
有个问题是,小宇宙的接口需要鉴权,网页 HTML 倒是不需要。但是调用小宇宙接口次数多了,号直接被封了,我用的是这个封装好的库:https://github.com/ultrazg/xyz/,里面没有提示这个封号的风险,我还以为不严格。
被封号后,我找到了这个项目,里面倒是提示了风险:https://github.com/shiquda/xyz-dl。 所以我怀疑是某个字段没设置对,调用多了导致被封号。后来我尝试找一些接码平台低成本多注册几个帐号(虽然这里超出了预算,但是一个码几毛线也还好),打算缓存一下接口内容并且很低频率调用接口更新内容,但是没找到能用的接码平台,或者没有能接小宇宙注册的平台。
于是我叫 Codex 帮我查找一下有没有别的方案,Codex 直接帮我找到了上述 rss 方案,确实是下次先问一下 Codex,判断自己的方案可行性以及还有没有更好的方案。🤥
四、所有音频转文字
流程如下:
- 音频切分分段:使用 Ten VAD 判断人声,使用 dp 算法以整个音频的视角确定最佳切分点,保证每个音频的断点尽量好,长度尽量在一个可控的区间,比如 10~20s 每段,每一个音频大概有 200~400 小段。
- 使用 ASR 模型转写:使用豆包 ASR 模型对每一段进行转写。
- 按照上述流程处理完所有的音频。
这里有两个方案:一个是直接长音频识别,第二个是切分后再识别。
- 长音频识别
- 有体验额度的商业开箱方案:可以调用字节的长音频识别 api 或者是阿里的通义听悟,这两个都是有一定的体验额度,后者最多了,每次登陆都送好几个小时,但是我这个有几百个小时可能,所以这两个都不适用,除非付款,但是我不想花钱。
- 本地模型:我试过直接用 FireRedASR2-AED-ONNX 离线转写 1 个小时视频,是能转出来,但是结果我没有细看,大概扫了一眼,挺完整的。但是需要的时间太长了,因为我这台电脑是 mba m1,基本就是音频时长多久就得等多久,几百个小时没法搞了。
- 第二个是切分后识别
- 第一个好处是对模型要求不大,因为有的模型不能一下子识别很长的音频,比如 1 小时,自己切分一下能够适用更多的模型,包括在线 api 和离线的自己部署的。
- 第二个是断点续传灵活,因为每一段比较短,如果失败了,直接从失败的那一段开始就行,恢复点对比一下子转写很长音频的灵活。
- 缺点就是自己处理音频的切分和结果的合并来保证质量。
- 最终我选择了自己切分识别,因为自由度大点,我也可以多种识别方式组合在一起,来尽量免费转写🫢。我试了通义听悟,虽然识别度已经很好了,但是还是有些错误的地方,我自己处理的话也能对这些地方做更多的操作。
五、音频切分算法
音频切分算法主要有三个版本的演进:
a. 粗糙贪心算法:
参考了下:https://pyvideotrans.com/youhua,但是和 AI 设计的时候设计歪了。
- 用 VAD 判断每个约 16 毫秒的音频帧是否有人说话。
- 连续静音达到 600 毫秒时,认为一段自然语音结束。此时会得到很多长度不一的自然语音段。
- 将过短的语音段按顺序贪心合并到相邻片段。
- 对超过最长时长的片段,从最长时长的 50% 位置开始向后寻找切点:
- 如果存在静音帧,选择最后一个静音帧。
- 如果没有静音帧,选择音量最低的一帧硬切。
这种方案的优点是实现快、处理步骤直观,也能基本避免固定时长切分。
主要问题是切点比较粗糙:
- 直接选择最后一个静音帧,没有判断这个静音是完整停顿,还是只是一个很短的静音点。
- 只在最长时长的后半段寻找,提前假定理想切点一定靠后,可能错过更自然的停顿。
- 每次只选择当前片段的切点,不考虑这个选择会让后续片段变得多长。
- 先合并短片段,再切分长片段,有可能合并后重新变长,但是切完又留下很短的一个片段。
b. 带停顿评分的贪心算法
第二版仍然逐段贪心处理,但不再简单选择最后一个静音帧,而是会收集当前搜索范围内的连续静音区间,并对每个候选停顿进行评分。
搜索范围也从原来的【最长时长后半段】扩大为:【当前片段起点 + 最长时长的 25%】 到 【当前片段起点 + 最长时长】,这样既避免过早切出只有几秒的片段,也给算法留下更大的候选范围。
- 每个静音区间产生一个代表切点,先收集所有的静音切分侯选点:
- 少于 48 毫秒的静音片段通常只是瞬时波动,不参与侯选。
- 对每个合格的静音区间,选择其中音量最低、同时最靠近停顿中心的位置作为实际候选切点。
- 记录停顿持续时间和平均声音能量,供后续评分使用。
- 计算停顿长度得分,停顿越长,越可能是新的一句或说话间隔:
停顿长度得分 = min(1, 停顿时长 / 300ms)- 例如:48ms 停顿 → 得分约 0.16
- 150ms 停顿 → 得分 0.50
- 300ms 停顿 → 得分 1.00
- 600ms 停顿 → 得分仍为 1.00
- 达到 300 毫秒后不再继续加分,因为此时已经可以认为停顿足够完整。
- 计算安静程度得分,减少切到呼吸声、背景声或残留发音上的概率:
安静程度得分 = 1 - (当前能量 - 最低能量) / (最高能量 - 最低能量)- 因此:最安静的候选得 1 分。
- 声音最大的候选得 0 分。
- 其他候选按能量大小落在 0~1 之间。
- 如果所有候选的能量相同,则统一得到 1 分,避免除以零。
- 切分位置合理得分,算法希望切点位于最长时长的约 80% 处:
- 不要求必须在 80% 处切分,只是防止算法为了选择更安静的停顿,切出过短的片段。
片段长度得分 = max(0, 1 - |切点比例 - 0.8| / 0.8)切点比例 = 当前片段长度 / 最长允许时长- 假设最长时长为 30 秒,在 24 秒处切分 → 比例 0.8,得分 1.00
- 在 12 秒处切分 → 比例 0.4,得分 0.50
- 在 30 秒处切分 → 比例 1.0,得分 0.75
- 综合评分:
总分 = 停顿长度得分 × 50% + 安静程度得分 × 25% + 片段长度得分 × 25%- 如果两个候选的总分相同,则选择位置更靠后的切点,让当前片段尽量保留更多内容。
- 没有合格停顿时,算法仍会在整个搜索范围中选择音量最低的一帧作为备用硬切点。
这个版本解决了以下问题:
- 不再把很短的静音波动当成自然停顿。
- 不再因为某个静音位置更靠后,就无条件选择它。
- 能在停顿完整程度、安静程度和当前片段长度之间作出平衡。
- 搜索范围从最长时长的 50% 扩大到 25%,可以发现更多自然停顿。
但它仍然是局部贪心算法:每次只选择当前片段得分最高的切点,无法判断这个切点会给后面的片段造成什么影响,仍然容易产生局部最优、整体较差的结果。
c. 动态规划算法(现版本)
动态规划的含义是:不立刻决定某一个切点,而是比较从音频开头到结尾的多种切分组合,选择总损失最低的一组,从全局角度出发审视静音切分点。
- 段间自然停顿点:连续静音达到 600 毫秒时,认为一段自然语音结束。此时会得到很多长度不一的自然语音段。
- 段内短停顿点:在自然语音段内部寻找所有至少持续 48 毫秒的短停顿点。
- 备用硬切点:为没有停顿的连续语音长片段准备均匀分布的备用切点。
- 收集好上述的切分点后,使用 dp 推导出最佳切分点。
优先考虑:
- 每段不能超过最长时长。
- 默认尽量让片段达到 20~30 秒。
- 对不足 20 秒的片段额外处罚。
- 优先使用停顿更长、语音发音能量更低的切点。
- 成本相同时优先生成更少的片段。
转移方程:1
2
3
4
5dp[i] = min {
dp[j]
+ segment_loss(L(j, i))
+ boundary_loss(i)
}
min: 枚举所有合法的前一个切点j,计算每种方案的总损失,然后选择其中最小的一个。dp[i]表示从音频开头一直切到第 i 个候选边界时,能够得到的最小总损失- 计算
dp[i]时,算法会枚举前面所有可以连接到i的j,并计算三部分:dp[j]:从开头切到j的已有最优方案。segment_loss(L(j, i)):从j到i形成的新片段,长度是否合理。boundary_loss(i):在i处切开是否自然,例如停顿是否足够长、足够安静。
这个比较麻烦,后续专门开一篇文章介绍这个。
六、ASR识别
主要是在线 api 和离线模型,在线 api 我用的是火山的 Doubao-lite,但是送的额度不够用,如果有公益站有 Gemini-flash 也行,但是也没找到。
离线模型我试了三个模型,Qwen3-ASR,FunASR,FireRedASR,速度都差不多了,但是准确度有点区别,差不多要转写的时间是音频时长 * 0.8,太慢了,放弃。
后来我想起豆包输入法的逆向项目,但是市面上的都是旧协议,我让 Codex 自己逆向了一下新协议,接着完善了使用方式,然后现在主要是用这个,已经转写了 200 多个小时没有问题,准确度差一点,甚至没有本地部署的 FireRedASR 的准确度高,有可能是我逆向的问题,不过也够用了,主要是速度够快,精确度还行。
如果要求极高的精确度,比如我要学习的视频,想要提取出文本进一步学习,我会自己分段,然后使用 Doubao-lite 或者 Gemini-flash 来识别,速度快,并且自己要学习的视频也不多,所以免费额度也够用。然后再用 llm 润色一遍,主要是修改一下标点符号,断句。
碍于收到法律信的风险(旧协议的分享者已经收到了),我这里不能分享新的协议代码,可以提供个关键字自己找, doubao-ime 或者 doubaoime-asr。
后续可以写一篇识别准确度和速度的对比文章,这个还是有点意思,正好我也有一点样本。对比一下逆向的 Doubao-ASR,离线部署的几个模型,Doubao-lite ,Gemini-flash 还有通义听悟。