做数字人项目做到第5篇,模型、材质、灯光这些视觉层面的东西基本都解决了,但你会发现一个特别现实的问题:用户对着屏幕说了半天话,数字人完全没反应,这哪里叫交互。语音输入就是数字人的耳朵,语音识别则是耳朵后面的听觉皮层,没有这一环,前面做的写实渲染再漂亮也只是个高级手办。这篇我就把“语音输入和语音识别”这块掰开揉碎,讲清楚它在HDRP数字人里的完整接入方式、选型逻辑和一系列我在实际项目中踩过的坑。
这篇内容主要服务两类人:一类是正在用Unity HDRP做写实数字人,卡在“不知道怎么把用户声音变成文字”这个阶段的开发者;另一类是想做数字人直播、虚拟助手、数字人导览,但不确定用本地识别还是云端识别,也不清楚麦克风数据怎么在Unity里正确采集和转发的人。我会沿着从整体链路到具体代码,再到平台差异和线上排查的顺序,把整个音频识别通道完整过一遍。
1. 先理清链路:语音识别在数字人系统里的位置与作用
1.1 数字人的“听-想-说-演”完整链路
先给整套系统画个像。一个能自然对话的写实数字人,内部其实是一条流水线:
用户说话 -> 麦克风采集 -> 语音识别(ASR) -> 大模型/语义理解(LLM/NLP) -> 语音合成(TTS) -> 数字人发声 -> 口型同步/表情/动作
这条链路里,语音输入负责的是最前面那段:把麦克风捕捉到的空气振动,转换成压缩后的音频数据;语音识别负责的是紧接着那段:把音频数据里的语音内容转成文本。这两个环节合起来,就是数字人的“耳朵”和“听觉处理中枢”。
第5篇聚焦的正是从“用户开口”到“文字上屏”这一整段。它的输出,是后面大模型模块的输入;它的延迟和准确率,直接影响用户对数字人“灵不灵”的第一印象。
1.2 为什么语音识别值得单独开一篇:难点根本不在SDK
很多新手容易有个错觉:语音识别不就是调个API吗?把音频文件扔过去,等一个返回字符串,完事。真要这么做,你会在HDRP数字人项目里死得很难看。
难点集中在四个方面:
- 实时性要求高:数字人交互不是“录完一整段再识别”,而是“边说边识别”,首字延迟控制在1秒内才算及格。这要求采集、编码、传输、识别、回调全链路流式处理。
- 音频格式转换:Unity的AudioClip拿到的是float数组,识别服务要的是16bit PCM或特定编码的WAV,采样率还要对齐。很多“识别结果乱码”的报错,根因都是这个环节。
- 线程与生命周期:WebSocket回调、网络线程、Unity主线程、音频采集线程,四者之间数据怎么同步,队列怎么设计,稍不注意就会出现崩溃或者界面卡死。
- 平台差异:Windows、Android、iOS对麦克风权限、音频会话、采样率支持都不一样,PC上跑通的代码,打包到手机直接罢工是常态。
这就像请了一位专业翻译(识别服务),但你必须自己把会场的声音接出来,经过降噪、编码、排队,实时送到翻译耳边,还得及时把翻译结果传回主持人手里。翻译本身厉害没用,你的“音频管道”得先能跑起来。
所以这篇不打算只给一堆API调用截图,我会直接给你一套可在HDRP工程里落地的语音输入管理器方案,加上本地/云端两种识别接入方式,以及数字人动画系统如何消费识别结果。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 选型:本地识别还是云端识别,我为什么这么选
2.1 三种主流方案对比
做数字人语音识别,绕不开几个现实选项。我用过讯飞、Azure、阿里云这类云端ASR,也深度用过sherpa-onnx这类本地开源方案,还看过一些团队自训模型的方案。先给你一张对比表:
| 方案 | 识别方式 | 首字延迟 | 成本 | 离线可用 | 中文效果 | 部署复杂度 |
|---|---|---|---|---|---|---|
| 讯飞/Azure等云端ASR | 云端API/WebSocket | 0.4-1.5秒 | 按量付费,有免费额度 | 不支持 | 高,支持热词 | 低,SDK接入 |
| sherpa-onnx本地ASR | 本地onnx模型推理 | 0.3-0.8秒 | 无流量成本 | 支持 | 不错,zipformer中文模型可用 | 中,需自己启动服务或嵌入 |
| 自训练/私有化部署 | 自建推理服务 | 取决于资源 | 高昂 | 支持 | 取决于训练数据 | 高,需算法团队 |
这个表格里的延迟数据是我在数字人对话场景下的实测范围,不是官方宣传值。实际数字会受设备性能、网络状况、VAD参数影响,但这个数量级是有参考意义的。
2.2 我的选型结论与判断逻辑
先说结论:数字人直播、虚拟助手这类正式项目,我现在默认用本地ASR做实时监听和唤醒,再按需切到云端做高精度正式识别;如果是赶Demo、快速验证玩法,直接调云端HTTP接口,一天内跑通。
这么选的原因很实际:
- 实时性优先:数字人交互场景里,“用户说完话1秒后数字人才有反应”和“用户说完话2秒后才有反应”,体验完全不一样。本地ASR省去网络RTT,首字延迟明显更可控。
- 成本与隐私:直播场景可能连续开几个小时,如果全部走云端按量付费,成本随并发涨得很快。本地推理只吃CPU/GPU,且用户语音不出设备,省了很多隐私合规方面的麻烦。
- 精度与热词权衡:本地方案的泛化能力确实比头部云端差一点,尤其是口音重的用户。但它支持热词表,把项目里的专有名词、品牌词、人名词条灌进去之后,大多数场景够用。真遇到本地识别置信度低的情况,再升级到云端识别也不迟。
- 断网兜底:线下展会、门店数字人经常遇到网络不稳的情况,本地识别保证基础交互不中断。
我用生活里的逻辑类比一下:本地识别就像是自己请了个驻场翻译,随叫随到、按天付费;云端识别像是给翻译公司打电话,翻译质量高,但每次都要拨号、等待接通、计费。驻场翻译能覆盖90%的日常沟通,那剩下10%的电话翻译需求,按场景按需调用就好。
选型的核心不是“哪个效果好”,而是“哪个适合你的业务场景和预算结构”。先想清楚你的数字人部署在什么环境、用户是谁、在乎延迟还是在乎准确率,再选方案。
3. 实战前准备:HDRP工程里的麦克风采集与音频处理
3.1 工程侧准备
在Unity里做语音输入,不用装太多额外插件。HDRP项目创建好之后,Audio系统本身是默认可用的。你需要根据识别方案补几个东西:
- 如果用新输入系统(Input System):建议装上,麦克风权限请求和旧输入系统行为有差异,统一用新的省心。
- 如果用WebSocket连云端识别:Unity自带的UnityWebRequest不支持ws/wss协议,需要引入NativeWebSocket或者WebSocketSharp这类库,注意选与你的Unity版本兼容的版本。
- 如果用本地sherpa-onnx:需要把对应的运行时和模型文件放到StreamingAssets或持久化目录,按平台分目录管理。
还有一个容易忽略的事:权限声明。Android需要在AndroidManifest.xml里加RECORD_AUDIO权限,而且Android 6.0以上还要运行时请求;iOS需要在Info.plist里配置NSMicrophoneUsageDescription,否则一调用麦克风就闪退;Windows桌面端一般不用特殊声明,但你要处理“默认录音设备被拔掉”这类情况。
3.2 Microphone类的正确打开方式
Unity自带Microphone类是采集麦克风数据最直接的入口,核心API就几个:Microphone.devices、Microphone.Start、Microphone.GetPosition、Microphone.End。但很多人栽在参数设置上。
我常用的采集参数是这样:
csharp复制// 定义采集配置
private const int SampleRate = 16000; // 语音识别黄金采样率
private const int ChannelCount = 1; // 单声道,双声道后面还得混音
private AudioClip _audioClip;
private int _lastSamplePosition;
// 启动麦克风采集
public void StartRecording()
{
string deviceName = Microphone.devices.Length > 0 ? Microphone.devices[0] : null;
_audioClip = Microphone.Start(deviceName, false, 30, SampleRate);
if (_audioClip == null)
{
Debug.LogError("麦克风启动失败,请检查设备权限");
return;
}
_lastSamplePosition = 0;
}
这里有个关键点:采样率为什么用16000。绝大多数ASR服务(包括讯飞、Azure、sherpa)都接受或者更偏好16kHz单声道16bit PCM,这是语音识别领域的“普通话标准”,因为人类语音的主要能量集中在300Hz到3400Hz,16kHz采样率足够覆盖且省带宽。Unity的Microphone.Start支持你指定采样率,但要注意:部分设备可能不支持你指定的采样率,要么硬件会重采样,要么返回的AudioClip频率和你要求的不一致,所以启动后最好校验一下clip.frequency。
再强调一个细节:Microphone采集是持续往AudioClip里写数据的,为了防止数据覆盖,我会用一个“游标”——记录上次读到哪个位置,每次只取游标到当前Microphone.GetPosition之间的增量数据。这就是流式音频读取的基础。
csharp复制private void Update()
{
if (!Microphone.IsRecording(null)) return;
int currentPos = Microphone.GetPosition(null);
if (currentPos < _lastSamplePosition)
{
// 循环绕回,先处理末尾到结束的数据,再从0开始
}
int samplesToRead = currentPos - _lastSamplePosition;
if (samplesToRead > 0 && _audioClip != null)
{
float[] buffer = new float[samplesToRead * _audioClip.channels];
_audioClip.GetData(buffer, _lastSamplePosition);
ProcessAudioFrame(buffer);
_lastSamplePosition = currentPos;
}
}
这段代码每隔一帧取一次增量数据,“增量”是流式识别的基础。不要每次把整个AudioClip都读一遍,既浪费资源,又会让识别服务收到大量重复帧。
3.3 音频格式转换:float PCM到16bit PCM/WAV
Unity的AudioClip.GetData拿到的是float数组,取值范围是[-1.0, 1.0],而识别服务要的是16bit PCM字节流或者WAV文件。中间这个转换,是大量“识别结果乱码”问题的源头。
标准转换代码是这样的:
csharp复制public static byte[] ConvertFloatToPcm16(float[] floatData)
{
short[] pcmData = new short[floatData.Length];
byte[] bytes = new byte[floatData.Length * 2];
for (int i = 0; i < floatData.Length; i++)
{
float sample = Mathf.Clamp(floatData[i], -1f, 1f);
pcmData[i] = (short)(sample * 32767);
}
// 小端字节序
Buffer.BlockCopy(pcmData, 0, bytes, 0, bytes.Length);
return bytes;
}
这里有两个容易错的地方:
- Clamp必须做。float数据偶发超过[-1, 1]范围,不截断会让short转换溢出,产生刺耳的爆音甚至数据损坏。
- 字节序要看服务端要求。国内云端服务绝大多数按小端处理(Intel x86/ARM都是小端),但某些私有协议可能要求大端,转换前先查文档。Buffer.BlockCopy默认按内存顺序拷贝,在小端机器上输出就是小端字节流,够用。
如果你对接的是要求WAV文件的服务,还要在PCM数据前拼一个44字节的WAV头,里面记录采样率、位深、声道数、数据长度。这个头拼错,服务端会直接返回“音频解码失败”。
3.4 设计语音输入管理器
我不建议把麦克风采集代码直接写在UI脚本里。因为一个数字人项目里,可能有多个系统都要消费音频数据:识别模块要音频帧,音量和VAD检测模块要音频能量,录音存档要完整音频。把它们耦合在一起,后期改一处崩三处。
我的做法是做一个独立的语音输入管理器,它只负责一件事:采集麦克风数据,对外提供原始PCM帧事件,内部不关心谁在订阅。
csharp复制public class VoiceInputManager : MonoBehaviour
{
public event Action<byte[]> OnPcmDataReady; // 16bit PCM小端字节流
public event Action<float> OnVolumeChanged; // 实时音量,用于可视化/检测
public void StartCapture() { }
public void StopCapture() { }
public byte[] GetLatestPcmFrame() { }
}
这样分层之后,识别服务类是VoiceInputManager的订阅者,VAD模块是另一个订阅者,谁也不会干扰谁。你后面要换识别供应商,只需要新增一个服务类,复用同一个PCM数据源,完全不用动采集层。
4. 语音识别接入:从“识别一句话”到“流式识别”
4.1 先走通单次识别:把全链路debug通
不管最终方案是本地还是云端,我建议第一步都先做“单次识别”:按下录音键,用户说话,松开录音键,把整段音频送去识别,返回文本,显示在屏幕上。单次识别流程最简单,适合验证“麦克风采集-格式转换-网络请求-回调主线程”这条链路有没有问题。
以云端HTTP接口为例,核心流程这样走:
- 用户点击按钮,VoiceInputManager.StartCapture()。
- 松开按钮或超时,StopCapture(),把累积的AudioClip数据转换成16bit PCM或WAV。
- 把音频数据通过HTTP POST上传,请求识别。
- 服务端返回JSON,里面有文本、置信度等字段。
- 解析结果,通过事件抛给UI。
这个过程能在1小时内跑通,但单次识别有个致命问题:用户必须说完一句话才能得到结果,中间没有任何反馈。数字人对话场景里,尴尬的沉默是体验杀手。你说完话之后得干等着两秒才知道数字人听懂了没有,这不行。
所以单次识别只适合当“Hello World”用,真正的生产方案是流式识别。
4.2 流式识别:边录边识别
流式识别的核心逻辑是:建立一条长连接(通常是WebSocket),持续把麦克风采集到的音频帧推送过去,服务端一边收一边把识别结果增量返回。你还未说完,服务端的中间识别结果就已经在路上了。
用sherpa-onnx举例子。它以HTTP和WebSocket两种方式提供流式识别服务,本地跑起来后,Unity客户端往它的WebSocket服务推音频帧。我自己常用的分片策略是每200毫秒推一次。太小的分片会让网络包过多,浪费在包头开销上;太大的分片会增加服务端等待完整包的延迟,首字变慢。200毫秒是个兼顾延迟和网络负载的经验值,实测在一般局域网环境下很稳。
状态机的流转我是这么设计的:
text复制Idle(空闲)-> Listening(监听中)-> Speaking(用户说话中)-> Processing(服务端处理中)-> Responding(数字人回复中)-> Idle
在这个状态机里,监听和识别是两个独立循环:监听阶段只做VAD检测,判断用户有没有开口;一旦确认开口,进入Speaking状态,开始向识别服务推流;用户停顿超过阈值,判定一句话说完,停止推流,等最终结果返回。
语音活动检测(VAD)是整个流式识别的节拍器。它的作用有两个:第一,判断用户什么时候开始说话,避免把环境噪音送去识别;第二,判断用户什么时候说完,决定何时请求最终结果。
4.3 结果回调与Unity主线程调度
这是一道经典的多线程考题。Unity的Audio采集、WebSocket网络回调通常不在主线程上执行,但GameObject、UI更新、动画状态切换必须在主线程。如果你直接在WebSocket回调里修改UI,大概率会看到Unity抛“UnityException: get_isActiveAndEnabled can only be called from the main thread”这类错误。
我的套路是用一个线程安全的队列加主线程轮询:
csharp复制public class MainThreadDispatcher : MonoBehaviour
{
private static readonly Queue<Action> _actions = new Queue<Action>();
private static readonly object _lock = new object();
public static void ExecuteOnMainThread(Action action)
{
lock (_lock)
{
_actions.Enqueue(action);
}
}
private void Update()
{
Action action = null;
lock (_lock)
{
if (_actions.Count > 0)
action = _actions.Dequeue();
}
action?.Invoke();
}
}
网络回调拿到识别文本后,用MainThreadDispatcher.ExecuteOnMainThread把更新UI和驱动数字人动画的逻辑扔回主线程执行。这个模式简单可靠,比在回调里直接搞协程靠谱得多,也方便调试。
4.4 语音识别的关键参数
不管是云端还是本地,有几个参数是通用的,我在不同项目里反复调过,整理成一张表:
| 参数 | 推荐值 | 说明 |
|---|---|---|
| 采样率 | 16000 Hz | 语音识别黄金标准,兼容性最好 |
| 位深 | 16bit | PCM标准位深,几乎所有ASR都支持 |
| 声道数 | 1(单声道) | 双声道需先混音,否则数据量翻倍且部分服务报错 |
| 编码格式 | PCM/WAV/OPUS | 云端流式常用OPUS省带宽,本地多用PCM |
| 分片时间 | 200-500ms | 兼顾延迟和网络开销 |
| 语言 | 中文/中英混合 | 根据目标用户切换,部分服务支持自动检测 |
| 热词表 | 项目内专有名词 | 显著提升特定词的识别准确率 |
| 输出标点 | 开启 | 后续给LLM和TTS用,标点能帮助情感判断 |
如果你对接的服务要求44100Hz,而你的采集设备只有16000Hz,别硬上,找找服务端有没有重采样能力,或者在客户端把音频重采样到16kHz。语音识别对高频细节不敏感,16k足够,传输和计算成本还低。
5. 识别文本如何接到数字人交互,而不只是“显示文字”
5.1 触发逻辑设计:怎么判断用户要说话
识别结果出来了,但你不能让数字人24小时竖起耳朵听所有人说话,那样会疯狂误触发。你需要定义一套触发机制。
常见三种方式:
- 按住说话:最简单可靠,但交互成本高,适合对讲机式问答终端。
- 关键词唤醒:用户先说“你好小X”之类的唤醒词,唤醒后开始识别。适合数字人接待、导览场景,省资源而且误报率最低。
- 全时段监听+VAD:一直开着麦克风,检测到人声并且音量超过阈值就触发识别。适合数字人直播,体验最自然但资源占用和误触发风险更高。
我的建议是两段式:先用关键词唤醒,再进入短时持续识别。本地ASR可以跑一个轻量唤醒模型,或者直接用识别结果里的前几字做匹配。“你好小X”这类固定唤醒词识别非常准,而且本地跑毫无压力,不用每次都把整段对话丢给云端。
5.2 把识别结果包成统一事件,解耦后路
识别结果不要直接用字符串满天飞。我建议定义一个统一的SpeechResult结构,所有下游模块都订阅同一个事件:
csharp复制public class SpeechResult
{
public string RawText; // 原始识别文本
public string NormalizedText; // 清洗后的文本(去口气词等)
public bool IsFinal; // 是否最终结果
public float Confidence; // 置信度
public string Language; // 检测到的语言
public long TimestampMs; // 识别完成时间戳
}
大模型/NLP模块只监听OnSpeechResultFinal事件,拿到NormalizedText去做语义理解;UI模块只监听OnSpeechResultPartial事件,做实时字幕。这样子模块间完全解耦,音频那些破事和下游根本无关。后面你把本地识别换成云端识别,或者反过来,下游一行代码都不用改。
5.3 识别结果如何映射到数字人的表情、口型和眼神
很多人在数字人项目里忽略了一件事:光把识别文本喂给大模型,数字人做得再精细,回复时表情都是僵的。写实数字人的表现力,一半在生产情绪,另一半在消费情绪。
从识别结果里可以挖出不少可用的表现线索:
- 标点符号:句号是平铺直叙,问号可以触发微微歪头疑惑的表情,感叹号可以触发惊讶或兴奋的情绪。
- 关键词情绪:识别到“开心”“难过”“厉害”这些带情绪色彩的词,可以映射到数字人的基础情绪状态。
- 语音韵律:本地ASR有些会返回韵律标记或语气词位置,虽然精度还不算高,但足够驱动嘴巴张合幅度的变化。
口型同步这块,我目前的做法是:如果是简单Demo,用SALSA或OVRLipsync这类插件,它们直接绑定AudioSource,靠实时音频音量驱动嘴巴张合,不依赖识别文本;如果要文本级口型,把识别文本转成音素序列,再驱动Viseme,这个精度更高,但需要接入TTS的文本分析结果。实操建议是先用音量驱动跑通,再去升级音素级。
还有一个很关键但容易漏的细节:数字人的眼神。识别结果里出现“你”“我”“这个人”这种指代时,可以触发表情管理器的注视偏移,让数字人看向对应方向。这种微表现对写实数字人的“真人感”提升非常明显。
5.4 打断与多轮对话的基本状态机
真实对话不是一问一答那么规整。用户可能在数字人说话到一半时突然开口:“等等,我问的不是这个。”如果系统没有打断机制,体验会非常糟糕。
我常用的状态机在5.2基础上补一条:在Responding(数字人回复中)状态,VoiceInputManager检测到有效人声后,立刻广播打断事件,TTS播放器停止播放,数字人切回Listening状态。
csharp复制void OnUserSpeechDetected()
{
if (_currentState == InteractionState.Responding)
{
OnInterrupted?.Invoke(); // 打断TTS和口型
_ttsPlayer.Stop();
_lipsyncPlayer.Stop();
_currentState = InteractionState.Listening;
}
}
这个打断逻辑必须在识别早期就触发,不需要等整句话识别完。只要VAD判断用户确实开口了(连续多帧有声纹),就立刻打断当前回复。做法是在VAD模块增加一个高优先级回调,和“开始推流识别”分成两个事件,一个用于打断,一个用于正式识别。
6. 实测避坑:延迟、平台差异、断线重连这些坑我都踩过
6.1 延迟拆解:从开口到文字上屏,时间花在哪
做数字人交互,第一个要盯的指标就是“用户开口到首个文字上屏”的延迟。这个延迟不是单一环节造成的,我拆给你看:
| 延迟环节 | 本地ASR典型值 | 云端ASR典型值 |
|---|---|---|
| 采集缓冲 | 20-100ms | 20-100ms |
| 音频编码/分片 | 可忽略 | 10-50ms |
| 网络传输 | <1ms(本地) | 20-200ms |
| VAD判定开口 | 100-300ms | 100-300ms |
| 服务端识别到首字 | 200-500ms | 300-800ms |
| 结果回传/解析 | <5ms | 20-100ms |
本地方案从开口到首字上屏,实测能压到400-900ms;云端方案一般在700-1800ms。这不是我瞎编的,是同一个数字人Demo在同样环境下的对比结果。
延迟最容易被低估的是VAD判定。VAD模块为了不误触发,通常会连续多帧检测到人声才确认“用户开口了”,这几百毫秒很容易被忽略,但其实占了整个延迟的大头。如果你确认环境安静,可以把VAD触发帧数调低,延迟能明显降下来。如果环境嘈杂,噪声大,那就两难一些,可以考虑先让VAD跑一个降噪前端,让识别更稳。
降低延迟的几个实战手段:
- 优先用本地识别,砍掉网络RTT。
- 流式分片设小一点,200ms左右。
- VAD触发阈值调激进,宁可多一点误触发也不能让首字延迟太高。
- 预热模型:机器启动时就把ASR模型加载好,别等用户说话时才开始初始化。
- 不要等用户说完再请求最终结果,中间结果先上屏,用户体感明显更快。
6.2 平台差异:PC跑通,手机上翻车
写实数字人项目经常要跨平台部署,Windows、Android、iOS三个平台的麦克风采集差异,我个个都栽过。
Android平台:
- 运行时权限必须处理:Android 6.0以上,光在Manifest里配了RECORD_AUDIO不够,还要在代码里运行时请求,用户拒绝之后要有兜底提示。
- 采样率兼容差异很大:低端安卓机对16kHz采样率支持不好,Microphone.Start返回的AudioClip频率可能被系统悄悄改成44100Hz,采集前必须校验clip.frequency,不匹配就要重采样。
- 部分机型录不到“内部音频”:如果要做“数字人在听自己说话”这种场景,得走AudioRecord原生层,Unity的Microphone类搞不定。
iOS平台:
- 除了Info.plist配NSMicrophoneUsageDescription,还要注意iOS音频会话。Unity项目里如果音频会话默认设置不对,会导致麦克风无法采集,需要在原生层配置AVAudioSessionCategoryPlayAndRecord。
- iOS对音频格式的管理比Android严格,封装成PCM推流时,注意单位是sample还是byte,单位弄错数据会全乱。
Windows平台:
- Microphone.devices有时会返回空列表,尤其是没有插麦克风的台式机,要做空设备兜底。
- 默认录音设备的切换(比如插入USB耳机)会导致采集中断,监听一下设备变化事件,需要时自动重启采集。
编辑器与真机的差异也要提一嘴:本地模型文件在编辑器下放StreamingAssets能直接读,真机上要用Application.persistentDataPath加文件拷贝逻辑,否则模型加载会静默失败。
6.3 断线重连与静音检测的可靠性设计
如果你用的是云端WebSocket识别,断线重连是必须做的。我踩过的坑是:网络一抖动,WebSocket断开,但Unity端的识别状态机还以为自己在“识别中”,一直往一个死了的连接里推数据,然后等一个永远不会回来的结果。
我的处理方案是:
- 心跳保活:每隔15秒发一个轻量ping帧,连续2次没收到pong就判定连接死了。
- 指数退避重连:断开后1秒、2秒、4秒,直到30秒上限,重连成功后重置退避。
- 重连后丢弃旧音频:重连成功后,把重连期间积压的旧PCM帧全部清空,防止服务端收到乱序数据。
静音检测也有个经典的坑:单靠音量阈值判断是否说话,在安静环境好用,但只要有空调声、键盘声、旁边人说话,就会疯狂误触发。我现在的方案是RMS能量加零过率双重判断:
csharp复制float GetRms(float[] buffer)
{
float sum = 0f;
foreach (float sample in buffer)
sum += sample * sample;
return Mathf.Sqrt(sum / buffer.Length);
}
RMS能量高于阈值,且过零率ZCR在正常语音范围内,才判断为“有人说话”。单纯的能量阈值会把持续噪音误判为人声,加上零过率可以过滤掉相当一部分稳态噪音。
6.4 文本后处理:同音字、口语词和敏感信息
识别结果是“裸文本”,直接丢给大模型会有很多问题。我现在的管线里,识别结果出来之后要过三道后处理:
第一,口语词清洗。中文对话里的“嗯”“啊”“那个”“就是”这类填充词,会严重影响大模型的语义理解,需要用正则或白名单把常见词清掉。但注意别把“那个”这种可能是实义的词全删了,要做上下文判断,比如“那个选项”里的“那个”就要保留。
第二,热词表增强。数字人项目里经常涉及品牌词、人名、专有名词,比如“HDRP”“写实数字人”“某某产品名”,默认识别模型经常识别错。把专有名词加入热词表后,识别准确率能明显提升。本地ASR和云端ASR都支持热词,只是配置方式不同,云端通常是调用参数传一个词表数组,本地是写一个热词文件。
第三,敏感信息过滤。识别到的文本必须过一层敏感词过滤和隐私信息脱敏,尤其是数字人直播场景。这层过滤可以在识别服务端做,也可以在Unity侧配置一份词库做初步兜底。注意用词要具体,别误杀正常词汇。
多语言混说也是数字人的常见场景,中英文夹杂的“今天我们要聊Unity HDRP渲染管线”这类句子,不少识别服务容易在语种切换处出错。解决方案是开启服务的“中英混合识别”模式,或者备两套识别服务按检测结果切换,具体看你用的服务支持哪种。
我现在把整套语音输入和识别链路串起来之后,最大的感触是:写实数字人项目里,“视觉像人”只是入场券,“交互像人”才是分水岭。而交互像人的第一步就是听得清、认得准、反应快。语音输入和识别这块做得好了,后面的大模型对话和TTS对口型才有用武之地;做得糙了,模型再精致,用户第一句话说完就会发现它是个“聋子”。按我给的方案先把“识别一句话”跑通,再上流式,然后逐步把本地识别、热词、VAD、打断这些细节打磨到位,你的数字人就能真正“长”出耳朵来。
