ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

AI语音识别如何为听障学生打造实时字幕系统?从技术链路到部署实践

AI语音识别如何为听障学生打造实时字幕系统?从技术链路到部署实践

老师讲到一个知识点,听障学生的眼睛要在老师、课本、手语翻译之间来回跳,很容易漏掉半句话。AI语音识别这几年真正的价值,不是替我们把视频自动生成字幕,而是把课堂里的口语实时变成字幕,让听障学生从“看口型、靠猜”变成“看屏幕、靠读”。我们参与开发的这套听障辅助视频字幕系统,底层接的是某AI开放平台的流式语音识别能力,跑过普通教室、录播课堂和远程视频课三种场景。这篇文章会把系统从需求到落地的完整链路讲清楚,尤其是那些文档里不会写、但现场调试一定会碰到的坑。

1. 场景与目标:先搞清楚“看得见的声音”解决什么问题

1.1 “没有实时字幕”才是痛点

我一开始以为听障学生的痛点是没有字幕,调研之后才发现,准确说法是“没有实时字幕”。现在主流视频平台基本都有离线字幕,可一次线下课堂、一次线上直播课,声音是即时发生的,不可能提前做好字幕。听障学生坐在教室里,获取信息的通道天然不完整:老师口语、板书、课件三线并行,普通学生的注意力都很难全程在线,更别说还要靠读唇或者等待手语翻译转达。

实时字幕等于把最不稳定的声音通道变成视觉信息,而且不需要额外人力。这意味着只要教室里有一台显示设备和一套音频采集设备,就有机会补上这条信息链。对随班就读的听障学生来说,这种自主性比“有人专门照顾”更重要——他们可以自己看字幕、自己查漏、自己跟上课堂节奏,而不是被动等待别人把信息转述过来。

1.2 核心指标:不只是准确率,还有延迟和可读性

做系统之前要先定指标,不然后续所有技术选型都会跑偏。我们最后定下三组关键指标,三条线同时约束:

指标目标为什么定这个值
延迟从发声到字幕上屏,1秒以内,容忍上限2秒延迟超过2秒,学生会感觉字幕和课堂是两套节奏,注意力会被撕开
准确率通用词不低于95%,课程专业词通过热词表达到90%以上领域词一旦错,整句语义可能反向,尤其对听障学生没有声音纠错机会
可读性每句20到24字,句末标点清楚,无口头禅堆叠一行塞40个字,视觉上根本没法读,学生抓不住重点

准确率这件事要特别说明:不能只看单字准确率,要听整句语义。比如生物课上把“反射弧”识别成“反射湖”,单字错一个,整句话对听障学生来说就是灾难。所以准确率指标必须配合领域词表一起看,后面我会专门讲热词表的用法。

1.3 适用场景和系统形态

这套系统不是只给特教学校用的。普通学校随班就读的听障学生、远程双师课堂、录播课回看,都有同样的需求。形态上我们做了两种:

  • 实时字幕在线版:适合直播课和线下课堂,声音进来,字幕几百毫秒后跟上。
  • 离线字幕生成版:适合把录好的课程视频批量转成srt或vtt字幕文件。

部署端也灵活,教室大屏、学生平板、个人手机都可以跑。对学校来说,这套系统能显著降低课堂辅助的人力成本,不需要每节课都配手语翻译。但我始终强调一个定位:它不是替代人工支持,而是降低信息获取门槛。这个定位决定了我们在设计上更强调“可调”和“容错”,而不是追求一次识别就百分百完美。

2. 技术链路:从麦克风到字幕显示的完整通路

2.1 音频采集与前端降噪:别让空调声抢了老师的话

识别引擎再好,前端音频不干净,后面全白搭。教室环境最典型的干扰源是空调、电扇、翻书声、走廊脚步声,还有学生小声说话。采集端可以选两种设备:教师领夹麦或者教室吊麦。领夹麦信噪比高,但老师移动时会有摩擦声;吊麦能覆盖整个讲台,拾音距离远,对降噪要求更高。我们最后采用的方案是老师佩戴领夹麦做为主信号,同时保留一个环境麦做回声消除的参考信号。

采样率直接用16kHz、16bit、单声道。这是主流ASR服务默认支持的规格,也是性价比最高的配置。语音的主要能量集中在300Hz到3.4kHz,16kHz已经能覆盖识别的有效频段,44.1kHz对识别准确率没有实质提升,只会白白增加带宽和成本。一路音频裸流的码率是256kbps,如果同时跑十个教室,上行带宽就是10倍,这个成本要在网络方案里提前算清楚。

降噪模块不能做得太激进。有些实时语音算法噪声抑制很重,会把老师轻声讲解的细节一起削掉,识别结果就会变得断断续续。我们用维纳滤波做稳态噪声抑制,配合回声消除处理教室扩音反射。这里有一个很关键的经验:开启回声消除之后,一定要拿真实课堂音频测试,不能只放一段测试音。否则很容易出现“识别引擎完全没收到干净声音”的极端情况,老师声音直接被当回声消掉了。

语音活动检测(VAD)一定要做在采集端,而不是丢给识别服务端。好处有两个:一是节省上行流量,二是避免没声音的时候还在向引擎传静音帧,白白占用并发额度。VAD参数里面,能量门限和静音长度是两个核心项。我们的经验值是门限设在-35dBFS左右,静音持续400毫秒以上认为一句话结束。门限太高,老师声音稍微变小就被切掉;门限太低,“嗯”“那个”这些语气词全被当成有效语音。不同老师的音量习惯不一样,每个学期开始的时候最好做一轮校准。

2.2 流式语音识别:为什么必须“边说边出”

如果这套系统做成“录音结束再识别”,延迟就是整整一节课45分钟。实时字幕系统必须走流式识别。所谓流式,就是麦克风音频在采集的同时,被切成一个个小帧通过长连接推给识别服务,服务端一边接收一边输出结果。输出的结果分两类:partial,表示当前识别结果还没稳定,后续可能会被修正;final,表示这一句已经定稿。可以类比成两个人聊天,对方一边听一边重复“你刚才说的是……”,这就是partial;等你说完整句,才给出完整准确的回应。

对接流式识别时,最容易被忽略的是连接保活。课堂连续45分钟,网络闪断、防火墙空闲超时都可能断开。我们在客户端单独做了一个重连模块:断线之后保留上下文窗口,重连成功后再把断线期间缓存的音频快速补发,同时丢弃已经返回的旧partial,只等最新结果。这个模块看起来不起眼,却是整体稳定运行的关键。

另一个容易被误解的参数是音频分帧大小。我们一般按100毫秒切一段,也就是1600个采样点,每个采样点16bit,一帧就是3200字节。帧太大会抬高首字延迟,字幕出得慢;帧太小网络包太碎,对弱网环境不友好。不同网络条件下最优帧大小不一样,不要照搬SDK示例里的固定值,要用自己的教室网络环境实测一遍。实测方法很简单:分别用50毫秒、100毫秒、200毫秒的帧跑同一段音频,比较首字延迟和丢包情况。

2.3 文本后处理与领域热词:让字幕“像人读过的”

ASR给出来的是“原生态文本”,直接显示会有几个问题:口语词多、同音字错、数字格式不统一。后处理要用轻量规则,不是上大模型改写。我们主要干三件事:删除无语义口头禅,比如“那个”“就是说”“然后”;把数字、日期、金额恢复成用户习惯的写法;结合领域词表修正明显字错。比如课堂出现“330”,我们希望显示成“330”而不是“三百三十”,这个需要识别引擎的数字标准化能力配合。

领域词表是这套系统的灵魂。每门课有自己的词表:生物课放“光合作用”“反射弧”,数学课放“正弦定理”“二次函数”,语文课放“通感”“借代”。我们的做法是课前把老师的讲义、课件文本抽一遍词频,挑出高频专业词和低频词,加入识别请求的热词参数,并调高权重。权重不是越大越好,在多数引擎里100到200这个区间比较稳妥,权重过高会让引擎对相近读音的普通词产生错误偏好。

热词表数量也要克制。一次识别会话里塞几千个词,效果不升反降,因为搜索空间变大,响应变慢,partial结果会更迟。我们一般控制在200到500条,并支持按课程切换词表。这个数字来自实际压测,不同识别引擎可能有差异,但总体上“少而准”比“多而杂”可靠。

2.4 字幕渲染:显示、断句与同步的工程细节

字幕不是把文字丢到屏幕上就完了。听障学生看字幕时还要同时注意老师动作和板书,所以字幕位置、字号、背景、换行都要设计。我们最后用的是屏幕下方偏上一点的位置,避免和老师嘴部区域重叠,又不能太低被桌面设备挡住。大屏端字体不小于42px,平板端可以小一些,但背景必须是半透明深色,文字用白色。用黑色半透明底而不是白底,是因为教室光线复杂,白底反光严重,长时间看很容易疲劳。

同步机制上,partial文本放在一个预览区里原地更新,不要整句跳动;final文本进入一个句子队列,显示组件每隔700到900毫秒弹出下一句。为什么加这个队列?因为ASR返回final的时间间隔并不稳定,如果每次都直接更新DOM,字幕会频繁闪跳,给学生造成“一直在晃”的压迫感。队列化之后,每句话都能停留一个稳定的时间,体感会好很多。

断句是显示端最容易出问题的环节。我们按三个信号综合处理:标点优先,ASR返回的句号、问号、感叹号直接作为切句点;标点不够就看静音长度,VAD检测到400毫秒以上的静音就认为是句间停顿;最后兜底看字符长度,超过24个字符强制切下一行。中文没有空格,硬切的时候要避开语义边界,不能从词语中间断掉,这个规则表需要维护和迭代。

3. 实操过程:搭一套可复用的实时字幕系统

3.1 总体架构与模块职责

整个系统的链路是固定的:音频采集端 -> 本地音频预处理(降噪+VAD) -> 流式ASR客户端 -> 文本后处理服务 -> 字幕分发服务 -> 前端渲染。实时链路之外还有一条离线链路,同一段音频同时落盘,课后提交批量识别,生成带时间轴的srt文件,方便学生回看。

模块职责要点
音频采集端拾音、降噪、VAD16kHz单声道,领夹麦优先
ASR客户端长连接管理、音频推流、结果接收区分partial和final,断线重连
文本后处理口语过滤、标点补全、热词修正必须轻量,不能拖延迟
字幕服务句子队列、断句、分发低延迟,秒级容错
前端渲染字号、位置、背景、换行42px以上,三行内

模块拆分的原则是每段只做一件事,方便单独替换。比如后处理服务有独立的实例,即使它挂了,临时绕过也不会影响ASR链路,只是字幕会粗糙一些。这个设计在后期排查问题时帮了大忙。

3.2 核心代码:音频推流与识别结果接收

下面这段代码是采集端和识别客户端的最小骨架。为了不绑定具体厂商,接口路径用示例域名代替,重点是理解数据是怎么流动的。

import asyncio import json import pyaudio import websockets RATE = 16000 CHUNK_SAMPLES = 1600 SAMPLE_WIDTH = 2 async def run(): p = pyaudio.PyAudio() stream = p.open( format=pyaudio.paInt16, channels=1, rate=RATE, input=True, frames_per_buffer=CHUNK_SAMPLES, ) url = "wss://asr.example.com/v1/stream" async with websockets.connect(url) as ws: # 先发送识别配置 await ws.send(json.dumps({ "config": { "sample_rate": RATE, "enable_punctuation": True, "enable_itn": True, "hotwords": {"反射弧": 100, "光合作用": 100}, } })) async def sender(): while True: audio_data = stream.read(CHUNK_SAMPLES, exception_on_overflow=False) # 一帧是1600个采样点,每个采样点2字节,共3200字节,约100ms await ws.send(audio_data) async def receiver(): async for raw_message in ws: message = json.loads(raw_message) msg_type = message.get("type") text = message.get("text", "") if msg_type == "partial": update_preview(text) elif msg_type == "final": push_final_sentence(text) else: log_unknown_message(message) try: await asyncio.gather(sender(), receiver()) finally: stream.stop_stream() stream.close() p.terminate()

这段代码里最核心的是sender和receiver两个协程在同一个长连接里并行跑。sender负责把麦克风采到的音频帧不断推给识别服务,receiver负责接收识别结果。注意sender里没有额外加延迟,因为pyaudio的stream.read本身就会按照真实录音节奏阻塞,读多少就等多久,不需要定时器。

有一些商家提供的SDK默认用grpc而不是websocket,协议不通但原理完全一样,都是双向流,只是一边发音频一边收文本。替换连接层很容易,参数名可能需要对应调整,阅读官方文档时重点看“流式”“双向”这几个字。

3.3 字幕显示端实现:两个层级的渲染方案

显示端我们分了两个层级:预览区和正稿区。预览区显示partial结果,允许不断变化;正稿区显示final句子,稳定展示。这样听障学生可以先看到大概内容,等定稿句子出现时再看一遍确认,避免一句话只闪过零点几秒就消失。

const captionText = document.getElementById('caption-text'); const previewText = document.getElementById('preview-text'); const queue = []; let displayTimer = null; function updatePreview(text) { // partial结果只在预览区原地更新,不进入正式队列 previewText.textContent = text; } function pushFinalSentence(text) { queue.push(text); if (!displayTimer) { displayTimer = setTimeout(shiftAndShow, 800); } } function shiftAndShow() { const sentence = queue.shift(); if (sentence) { captionText.textContent = sentence; } if (queue.length) { displayTimer = setTimeout(shiftAndShow, 800); } else { displayTimer = null; } }

为什么不直接把partial结果塞进正式区?因为partial经常抖动,比如识别引擎先出一个“反射湖”,下一秒修正成“反射弧”,如果把这个抖动流程放在正式区域,视觉体验会非常糟糕。队列机制让final结果的显示频率变得可控,也避免了网络波动带来的瞬时堆积。

样式上,核心是保证对比度。我们用的样式可以当模板:

.caption-box { position: fixed; bottom: 12%; left: 50%; transform: translateX(-50%); max-width: 80%; background: rgba(0, 0, 0, 0.65); color: #ffffff; border-radius: 8px; padding: 12px 20px; text-align: center; letter-spacing: 1px; z-index: 999; } #caption-text { font-size: 42px; line-height: 1.4; } #preview-text { font-size: 28px; color: #e0e0e0; min-height: 1.4em; }

预览区字号比正稿区小,颜色偏暗,这样它的定位就是“辅助提示”,不会抢正式字幕的注意力。如果需要做字号切换,直接改caption-text的font-size就行,同时要注意caption-box的宽度可能需要相应调整。

3.4 教室里部署时的关键参数

实际部署时,我们按下面这张表核对环境,这个表来自多所学校的课堂实测,不同设备和网络可以在此基础上微调:

项目推荐配置备注
麦克风位置教师领夹麦为主,讲台吊麦为辅避免桌面式全向麦,串音严重
音频参数16kHz / 16bit / 单声道ASR服务通用规格
上行带宽每路至少256kbps,建议预留到512kbps多教室并发要乘以路数
字幕延迟目标1秒,上限2秒超过2秒要查缓冲队列
大屏端字号不小于42px学生距离屏幕3米以上时更要加大

有一个细节很值得提醒:如果字幕机和大屏之间用无线投屏,会额外引入几百毫秒延迟,而且信号不稳定的时候会有画面撕裂。我们后期全部改成HDMI直连或者专用稳定的网络连接,问题立刻少了一半。上课前最好像测试广播一样发一段音频跑一遍完整链路,确认能出声、能识别、能显示,而不是等老师一开讲才发现设备没接通。

4. 常见问题与排查技巧实录

4.1 老师说话但字幕没反应

这个问题出现的频率最高。先不要怀疑识别引擎坏了,90%的问题出在前端采集链路。常见原因有几种:VAD阈值太高,把老师轻声讲的内容过滤掉了;音频输入设备选错,采集到的还是系统内置麦克风;回声消除模块把老师声音当成回声抵消了;或者长连接已经断开但没有触发重连。

我们的排查流程是固定的,步骤顺序很重要:

  1. 查看采集端的原始波形,确认音频数据有没有真正进入系统。
  2. 临时关闭VAD做一次原始识别,判断问题是否出在前端。
  3. 查看ASR连接状态日志,确认长连接是否正常。
  4. 最后再用测试音频单独压测识别接口,排除服务端能力问题。

遇到过最典型的案例是老师声音偏小,教室空调噪声大,VAD门限按通用值设置,导致每讲两三句就被切一次。后来把门限从-30dBFS降到-38dBFS,同时让老师换用领夹麦,问题就彻底消失了。这里的关键是不要只看门限参数本身,要结合拾音距离和老师说话习惯综合判断。

4.2 专业术语总是识别成同音字

术语错误在听障课堂里是最容易被放大的问题,因为学生没有声音来辅助纠错,看到屏幕上写着“反射湖”会比听到错误发音更加困惑。处理这个问题不是靠事后纠正,而是靠热词词表前置。

我们有一套课前词表生成流程:把老师的教材和讲义文本跑一遍词频统计,挑出专有名词和低频词,配合识别接口的热词参数调高权重。“反射弧”变成“反射湖”、“海拔”变成“嗨吧”这类高频错误,加入热词表之后基本都能解决。注意权重不能一直往上加,调到100到200之间足够,再高可能引发新的误识别。

热词表的维护节奏也很重要:学期初建立基础词表,每节课后把课上漏识别的词补进去。热词数量保持在200到500条之间,并且支持不同课程切换词表。比如语文课的词表不应用到数学课,否则词表之间的互相干扰会降低整体准确率。

4.3 字幕延迟越跑越远

延迟漂移是最隐蔽的问题,也是最容易被忽略的。刚开始字幕和老师口型基本对齐,过十几分钟后越来越慢,最后学生看到的基本是几分钟前的内容。常见原因有几种:弱网导致音频帧在客户端堆积;后处理服务排队时间变长;显示队列过长但弹句节奏没有自适应。我们早期没有给显示队列设上限,页面会在内存里堆几百句话,字幕像PPT一样一页页翻,完全失去实时性。

处理思路分三层:第一层,发送端做拥塞控制,本地音频缓冲超过500毫秒就丢弃旧帧,优先保证实时性;第二层,接收端队列限制,超过2秒的显示量就跳过中间句,直接落到最新句;第三层,系统做延迟告警,当从采集到展示的总延迟超过2.5秒时,管理端出日志。

症状原因解决方案
几分钟后字幕越来越慢客户端音频帧堆积发送端缓冲超500ms丢弃旧帧
显示队列无限增长后处理或分发服务排队接收队列超过2秒跳过中间句
有时快有时慢,毫无规律网络抖动导致重连和补发做缓冲自适应,固定节奏弹句

我后来发现一个比看监控数据更管用的办法:在教室角落放一块能看到字幕的测试屏,每个整点看一眼。人眼对延迟的敏感度远超阈值监控,只要你感觉不对,那大概率已经超限了。

4.4 长句不断句、一行塞满几十个字

ASR返回的final有时候是一大段,标点偏后甚至缺失,如果直接显示,一行字幕就会塞满几十个字,听障学生根本来不及读完。显示端必须做断句兜底。我们采用三层策略:优先用标点;标点不够就看静音长度;再不行按字符数硬切,但硬切要避免从词语中间断。

中文字符没有空格,硬切主要看语气词、连接词和语义边界。后处理服务里维护一个轻量断句规则表,遇到“但是”“然后”“因为”这类连接词,会把位置前移半句作为候选切点。这样即使没有标点,也能保证断出来的句子基本符合中文阅读习惯。

显示端限高3行,超过就向上滚动或清除最早一行。字幕窗口的位置保持固定,内容尽量留在视线中心,不要频繁浮动。这个设计看起来简单,但对实际使用体验的提升非常明显。

5. 实测体会与可以继续做的方向

5.1 识别率之外更重要的事

跑了大半个学期,最大的体会是实时字幕系统的体验天花板,往往不在识别引擎,而在工程容错和显示策略。识别偶尔错几个字,学生能靠上下文猜;但如果字幕频繁跳动、延迟漂移、队列卡死,学生会彻底放弃这个工具。所以我们的准则是稳定优先,哪怕为了稳定性牺牲一点准确率也值得。

调优路径建议从真实课堂录音开始,而不是拿标准测试集看综合得分。我们把错误分成三类:同音字错误、漏识别、语气词干扰。对应处理完全不同:同音字靠热词表,漏识别靠调低VAD门限和改善音频质量,语气词干扰靠后处理规则。每类错误单独测试、单独解决,整个系统就能在一个学期里稳步变好。

5.2 可以继续做的几个方向

这套系统的扩展空间比想象中更大。最简单的是双语字幕,给英语课堂和外来讲座用,相当于把听障辅助工具转成通用数字学习工具。再进一步可以做关键词高亮,把老师反复讲的术语在字幕里标出来,帮助学生快速捕捉重点。声音可视化也是很有价值的尝试,比如音量条和说话人识别,让听障学生也能感知语气和声源位置。离线转写积攒一个学期的课程素材后,还可以自动生成课堂笔记和知识点检索索引。

我在实际操作中的一个真实感受:这个项目要跑得好,靠的不是某个精准模型,而是一套愿意在真实课堂里反复打磨的工程流程。每个月回看录像、分析错误类型、更新热词、调整显示策略,比任何一次上线都重要。如果你也准备做类似系统,我的建议是从离线转写链路先跑通,把数据收集起来,再上实时链路,后面会顺很多。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进