实验室里跑通的数字人,搬到客户现场经常变成四种"不灵":嘴动了但慢半拍、说着说着嘴不动了、画面正常但叫它不理、欢迎词死活不响。这四种现象在客户眼里都是"你这东西坏的",但在工程上是四条完全不同的链路。
我们在展厅、门店、政务大厅这类无人值守、长期开机、网络不由我们控制的现场反复踩过这四类问题。下面这张分诊表和后面的判据,是每一次都把"猜"换成"量"之后才定下来的。每条判据都能远程执行,不需要人在机器前面点鼠标。
这是现场排查最容易走错的一步。数字人页面里的 idle、talk 两段视频是浏览器缓存过的,网络断了它照样播。而每句回复的 TTS 音频是每次新生成的 URL,必然要现拉。
所以"人像在动、就是不说话"这件事,恰恰证明不了网络好。必须在设备本机实测下载速度:
curl -sk -o NUL --max-time 30 -r 0-2097151 -w "%{speed_download}" \
https://<你的入口>/assets/fenshen_v1_talk_1080x1920.mp4
判据很简单:低于 50 KB/s 基本不可用。我们的一版素材是待机 4.3 MB、口播 3.7 MB、每句 mp3 约 100 KB,全都要现拉——这是 1080×1920 的代价(待机 4.7 Mbps、口播 2.3 Mbps,对现场宽带偏高)。
| 现场描述 | 一行判据(远程可跑) | 根因 | 修法方向 |
|---|---|---|---|
| 嘴在动,但比声音慢约两秒 | 日志里的口型时序:出画面 是否 > 2.5s、primed 是否 False |
口型管线粒度(batch_size)+回声式 TTS 二次排队 |
调管线粒度,不调音画补偿 |
| 说一段之后,后半段嘴定格 | 可见视频元素 currentTime 是否贴住 duration |
口播素材不循环,本次累计朗读超过素材时长 | 素材循环 + 页面侧自愈看门狗 |
| 画面正常,叫它不理 | micStream 是否 false、麦克风 rms 是否长期 <0.01、ASR 是否输出空文本 |
弱网 / 麦克风没起来 / 上传全是噪声 | 先量网络,再看起麦,最后看识别 |
| 欢迎词不响,但聊天正常 | /motion 的 ok 与 frames 是否递增 |
摄像头黑帧(物理侧),或播报地址被写死 | 先分清黑帧/地址,别改 ASR |
为什么强调"先量三个数":这四类的修法互不通用。把"网络不行"当"模型坏了"去重装环境,是现场最常见的浪费;反过来把"素材不循环"当"音画不同步"去调补偿,嘴会越调越乱。
先拿三个数,别动手。口型准备阶段已经埋了计时,一次播报就够:
journalctl --user -u <口型服务> --no-pager | grep -a '口型时序' | tail -3
# 口型时序:等空闲 0.00s / 喂音频 0.54s / 出画面 1.59s(primed=True)
| 量 | 含义 | 健康值 | 故障值 |
|---|---|---|---|
| 等空闲 | 等上一轮口型落地才喂新音频 | 0~2.5s(首句 0) | — |
| 喂音频 | 上传 + 入队耗时 | 0.02~0.6s | — |
| 出画面 | 喂完到"嘴开始动" | 1.5~1.7s | 3.06s |
primed | 是否真的等到口型起来 | True | False |
primed=False 必须当故障看:它意味着等超时了就放声——客户先听到声音,画面三秒后才到,主观感受就是"延迟两秒"。
batch_size 就是"图像延迟"的量纲(主因)。管线粒度 = batch_size × 40ms。推理要凑满两轮才推画面,batch_size=16 时每轮 0.64s,两轮 1.28s 起。16 → 8 后粒度降到 0.32s。
但"越小越好"是错觉:batch_size=4 时推理帧率掉到 21.7~22.7 fps,低于 25 的输出帧率,画面会越播越落后,比延迟更糟。同一台机器上 8 是下限(26.5~26.8 fps,余量只有 5~6%)。判据是两端都看——延迟降了、帧率还得 > 25。
渲染主循环的积压刹车。发送队列积压 ≥5 帧时每轮 sleep(0.04×qsize×0.8),就是 0.16~0.26s,还把同一循环里的语音处理一起拖慢。阈值 5 → 60 后只在真积压时刹一次。
注意:单改这一条实测没有效果(仍是 3.08s,积压告警 0 条)。它不是主力,别在这儿死磕。
回声式 TTS 让同一条音频被合成两遍。走"文本 + type=echo"的路径每次 1.4~2.4s/句,且串行排队——说一句欠一秒,越说越欠。正解不是提速,是音频同源:把刚合成的那份 mp3 原样喂给渲染端,页面播的和驱动口型的是同一份文件,时长必然一致。
同源最硬的自证是逐句对齐到小数点后两位:24000: (110592,) = 4.608s 对 mp3 的 4.61s,(45504,) = 1.896s 对 1.90s。
| 修复前 | 修复后 | |
|---|---|---|
| 出画面 | 3.06s | 1.52~1.69s |
primed | False | True |
| 音画差(页面侧实测) | 约 2000ms | +227ms |
| 推理帧率 | 26.9 | 26.1~26.6 |
量"音画差"必须用头部 ROI,不能用全画帧间差分。全身数字人的待机素材本身一直在微动,全画差分会把"说话"和"待机"切得乱七八糟——我们实测过把偏移算成 +2794ms 和 +11643ms 两回,纯粹是配对错了。正确做法:只取头部区域,只统计变化超过 24 色阶的像素占比,能干净切出说话窗口。
还有一条现场纪律:验证时一个声音都不能出。展示机在客户面前,任何"为了量一下"而让它出声都是事故。做法是在页面侧勾住 HTMLMediaElement.prototype.play,真正播放前强制 muted=true; volume=0,全链路照常跑完再复位——字幕、口型都在动,但没有声音。
判读口径:+1500ms 以上 = 管线级故障;±150ms 内 = 同步良好;负值大 = 补偿调过头了(画面跑到了声音前面)。
这一类的关键差别在于哪种"不动",三种现象的修法完全不同:
| 现象 | 判据(在页面里量) | 根因 |
|---|---|---|
| 画面完全定格,时间轴在走 | currentTime 在涨,但 totalVideoFrames 不涨 | 解码/渲染停摆、页面被遮挡节流 |
| 人在动,只有嘴不动 | 可见层播的是待机素材(待机本身有轻微晃动,嘴是闭的) | 状态机没切到口播层 |
| 讲一段后后半段嘴不动 | 可见层是口播但 currentTime 贴住 duration | 口播素材不循环 + 累计朗读超过素材时长 |
第三种我们遇到过最典型的一例:为了让音尾对齐,口播素材被改成了不循环(时长 12.84s),而一次会话只在开场复位一次播放头,多句回答连续播同一条。于是单次累计朗读一旦超过 12.84s,视频必然播到末帧——这就是客户说的"回答完问题、重播欢迎语那一段嘴不动"。
修法不是"把素材做长",而是结果校验 + 自愈 + 上报。页面里挂 250ms 巡检,只要在讲话就校验四件事:可见层是口播素材、没有暂停、totalVideoFrames 在增长、没有贴住末帧。
currentTime = 0。素材首帧就是待机锚点帧,几乎看不出接缝,不打断嘴型;为避免误报,巡检要设三个闸门:冷却 1800ms、起播后 1400ms 内不判、淡化动画期间不判(交叉淡化时两层透明度都不是 0/1,此时判"哪一层在播"必然误判)。这三条落下后,正常朗读实测 0 误报,正常播放时帧增长约 25 帧/秒。
验证要能复现,别只说"好了":①正常朗读一次,帧率对、看门狗命中 0;②人为把 currentTime 设到末帧前 0.05s,应在一个巡检周期内被检出并回卷;③人为把状态改成"讲话中"但可见层停在待机素材,应被检出并重挂。
按顺序量三件事,每件 1 分钟内出数,不要跳步:
这是我们吃过最大的一次亏:设备搬到楼下换了网络,公网吞吐只有 391 B/s,页面几乎打不开。查下来不是宽带问题,是一块已经拔掉网线、留着静态 IP 的网卡仍然保留默认路由,而且度量值比无线还优——流量全被丢进黑洞,TCP 反复重传。
Get-NetRoute -DestinationPrefix 0.0.0.0/0 | Select InterfaceAlias,RouteMetric,InterfaceMetric
# 实测:以太网(Disconnected,169.254 地址) 1+5=6 < WLAN 0+55=55 → 有线赢了
Set-NetIPInterface -InterfaceAlias 以太网,WLAN -AddressFamily IPv4 -AutomaticMetric Enabled
# 立刻从 391 B/s → 30 KB/s(约 77 倍)
Disable-NetAdapter -Name 以太网 -Confirm:$false # 当下不用有线就直接停用
顺带一个判据:ping -l 1400 -n 15 <网关> 如果一跳就丢包 13%、平均 152ms、抖动 19~419ms,是射频/链路质量问题,与"网卡速率 72Mbps、信号 100%"并不矛盾。这种现场不要在软件侧补救,直接给三条路:插网线、换信号好的 AP、手机热点临时验证。
起麦通常只有三条触发路径:点麦克风按钮、手机接管释放、页面重新可见(且此前起过麦)。页面一 reload,那个"起过麦"的标志就复位了——页面看着完全正常,麦克风流却是 null,客户怎么喊都没反应。
一行判据:{micStream:!!d.micStream, vad:!!d.vad, status:document.getElementById('status').textContent}
micStream:false 配上 status:'online' 就是聋了(正常应该是 listening)。修法是加自动起麦:15s 节流,失败后 2.5s / 8s 各补一次,挂在连接建立、15s 保活、页面可见性变化、任意点击/触摸上。验收标准是——重载页面后什么都不做,20s 内自己恢复成 micStream=true / vad=true / status=listening。
先分清是"没检测到人"还是"检测到了却播不出来",这两条路的日志特征完全不同:
present=True、置信度很高,但 speak=skip(no-srv) / SRV_UNRESOLVED。根因是服务端地址被写死在局域网 IP,而且启动参数带了"禁用页面探测",于是候选地址只剩局域网那一个——设备一离开内网就永远播不出来。/motion 一直 present=false、ok=false、失败计数飙升,而聊天通常是好的(音频与视频是两条独立链路)。别一看到"没反应"就查语音识别。铁律:任何"写死服务端地址"的参数都是搬家即哑。正确顺序是:显式参数 → 局域网兜底 → 从页面同源地址推断。后者让终端自己找到入口,换网不用改配置。
如果 /motion 拿不到画面,按代价从低到高排:
黑帧的判据很干脆:像素均值 ≈0.01、标准差 ≈1、最大值 255 只有几颗噪点。这时候软件侧已经到头了:
我们后来找到一条容易被忽略、但决定"能不能一次治好"的物理根因:摄像头和 USB 无线网卡挂在同一个根集线器上,而机器另有空闲的一组。二者抢供电与带宽;而且不少 USB 摄像头在 USB3 控制器上"能枚举、不给流",插 USB2 反而稳。把摄像头挪到另一个根集线器、换一根短线,与"昨天换口即恢复"的现象互相印证。
防复发可以远程做:关闭 USB 选择性暂停(电源计划里那一项),防止设备被系统断电后醒不来。
无人值守的机器,交付时把下面六条做成定时巡检,任何一条不满足就报警——它们覆盖了上面四类故障的早期信号:
| # | 检查项 | 通过判据 |
|---|---|---|
| 1 | 人像页可达 | HTTP 200,且页面里的 status 是 listening |
| 2 | 麦克风活着 | micStream=true、vad=true |
| 3 | 人体感应活着 | /motion 的 ok=true 且 frames 递增 |
| 4 | 口型链路活着 | 一次播报的"出画面"<2.5s、primed=true |
| 5 | 音画同步 | 无声音测量,偏移在 ±150ms 内 |
| 6 | 网络余量 | 媒体下载 >50 KB/s |
还有一个必须提前处理的连带风险:页面上的"退出"如果只有一个按钮、且直接杀掉浏览器,那一声被误识别的"关闭/退出"就能让大屏永久黑掉。交付前给它加二次确认,或者加自动回归——这是"看起来能用"和"能无人值守"的分界线。
数字人现场故障几乎都能归到四句话:声音是节拍器(画面跟声音走,宁晚不早);检测要自己找到服务端;麦克风要自己起来;黑帧永远在物理侧。
凡是靠"人在现场点一下"才能恢复的环节,客户现场就一定会出哑场。