数字人 kiosk 无人值守:三个看似无关的故障,其实是同一个根因

2026-10-10 · 数字人部署 · 阅读约 12 分钟

把数字人搬到展厅做无人值守客服,最容易卡住的不是模型精度,也不是口形质量,而是几种看起来毫不相干的故障同时冒出来:手机连接二维码是个白框、客户走近屏幕欢迎词不播、状态一直卡在 loading、声音出来了但口播视频不切换。这四个现象分属二维码、摄像头感应、WebSocket、音频链路四块,按直觉会开四条排查线,一查就是几天。

本文记录一次真实部署的排障过程。结论有点反直觉:前三个故障其实是同一个根因,第四个是另一个独立但同样典型的根因。文中所有数字与现象都来自我们自己的部署记录,不是推测。

一、现象:四个互不相干的故障

上线当天的现场反馈是这样的:

注意第一条表述:反馈说人体感应没有反应。这个描述是准确的,但极易把排查引向错误方向——因为感应逻辑本身跑得好好的。我们曾为此白查了半天。

二、根因一:原版前端是个半成品,它要三个后端

关键线索来自无头浏览器抓到的网络记录与页面状态字段:HTTP 404 /api/qr、WS 404 /ws、status: loading。三个 404,三个症状,一一对应。

真实情况是:这套前端页面本身调用了三个后端接口,而这三个接口只存在于原生产环境,新部署的这台机器上没有。前端一行代码都不缺,缺的是后端。

前端调用点要什么缺了会怎样补齐后的实现
index.html → GET /api/qr二维码 PNG<img> 裂成白框用 qrcode 库现画 370×370 PNG
welcome.js → GET /api/speak感应命中后由服务端播欢迎词感应归感应,没人出声edge_tts 合成 mp3 后广播
display.js → WS /ws语音上下行主通道状态永远卡 loadingASR 到 LLM 到 TTS 全链在服务端跑
最重要的一条认知:welcome.js 只是个探测器,它自己不说话。它做摄像头帧差判断有没有人,命中后回调 /api/speak 让服务端去播。所以感应没反应的表象是准确的——探测一直在跑,只是后端 404 没人接。

正解不是改前端,而是写一个桥接服务补齐这三个协议,页面一行逻辑都不动:/api/qr、/api/speak、/ws,外加给守护脚本用的 /health。

三、定版做法:原版骨架 + 单点替换

发现缺后端之后,很容易生出干脆自己重写一套三态页面的冲动。这条路我们被明确否掉了,理由很实在:原版逻辑已经稳定运行,重写等于把已经踩平的坑再踩一遍。

定案的架构是原版骨架 + 单点替换:

  1. 原版四个文件一字未改地部署:index.html(落地改名)、display.js、display.css、welcome.js,素材按原版路径放 web/assets/。同源校验靠比对字节数——本项目实测待机视频 2210802 字节、口播视频 1547176 字节、欢迎视频 5113117 字节,与原站逐字节一致。
  2. 唯一增量是一个新脚本文件,页面里只多一行 <script src="..._live.js?v=1"></script>。

这个增量脚本只干三件事:把原版要的三个后端地址改指桥接服务;把舞台容器钉成竖屏单坐标系;问答时叠上半屏实时画面(欢迎与待机态不叠,保持原版整屏)。

删掉这一行 script,就 100% 回到原版。这是这套方案最大的价值——回滚设计本身就是最好的保险。

3.1 一条硬红线:绝对不要包装 getUserMedia

为了避开数字人自己的声音被前端语音活动检测当成客户插话,我们曾经有一版把浏览器的取音接口包装掉,换成一个静音音频流。

结果是把摄像头一起废了:原版的在场检测是帧差法,拿不到真实视频帧,帧差恒为 0,无人到有人的上升沿永远不触发——客户走近屏幕,欢迎词永远不播。

正解是:麦克风与摄像头原封不动交给原版,防自激的问题放到桥接侧解决(欢迎词广播时带上不打断标记,原版据此抑制打断)。这条红线值得写在部署清单第一行。

四、根因二:有声但画面不切换,是回声自激把状态机打断了

第四个故障最容易误诊为视频流坏了。实际情况恰恰相反——通过浏览器调试协议实测,视频元素是 1080×960、readyState=4、paused=false、srcObject=true,视频流完全健康。坏的是状态机被反复打断。

因果链是一条线,拆开看就会漏:

扬声器播数字人的声音
 → 麦克风拾取(原版在 TTS 期间刻意保持麦克风开启,为的是支持客户插话)
 → 原版回声中抑失效(它依赖把 audio 接进 AudioContext 取回声参考,本环境没建立)
 → 原版判成客户插话 → 触发打断:audio.pause()、currentTime=0、清空音频队列
 → 声音只响一小截;而字幕按 audio.currentTime 逐字渲染 → 恒为 0 → 字幕永远空
 → 回声同时被上行到桥接 → 语音识别出文本 → 桥接当成新问题 → 开新一轮
 → 桥接没有并发保护 → 多轮交错 → 任一轮收尾就广播结束状态 → 把别轮正在说话清掉
 → 叠加层熄灭、口播层没机会停留

可直接复用的判据:字幕元素持续为空 = 音频元素没在播。这是区分没收到消息和收到了但被打断的决定性证据。

4.1 治法:说话期间忽略上行 + 全局只允许一轮

桥接侧加一个单调时钟时间戳 SPEAK_UNTIL 与一把全局回合锁:麦克风路径在自己在说或锁被占时直接丢弃(这段必然是自己的声音被收回来了);文本路径(屏幕键盘或人工输入)必须排队等锁,不能丢——我们曾用占用判据去丢文本输入,结果自己的测试问题全被吃掉,还以为是桥接没工作。

页面侧的叠加层要用闩锁,既不能用距上条音频 N 毫秒的时间窗,也不能读 #actor 的透明度:原版会把各句音频排队慢慢播,最后一条音频消息远早于播放结束就到了,窗口会在回答中途提前失效(实测:回答播到 53 秒,1.8 秒的窗口在 27 秒就掉了);而 #actor 只是过场层,约 400 毫秒后内容就被迁移回待机层并归零。

代价要说清楚:这套治法会让数字人说话期间客户无法插话。展厅客服场景可以接受;若要恢复插话,需要音量明显高于回声参考加最短字数的双条件。

4.2 顺带修掉的一个隐性 bug

收尾函数的第一行被写成了对自己的调用,形成无限递归,而这个异常被一个宽泛的 except: pass 吞掉——那两条收尾消息从来没发出去过。写兜底收尾这类函数时,务必先确认它不是自调用;宽泛的异常吞掉会把这种错彻底藏起来。

五、探活铁律:有副作用的读接口不能当健康探针

欢迎词接口看着是个 GET,很适合当健康探针,但它有副作用:它会合成音频、广播消息、并把画面切到欢迎视频。拿它当探针就是自伤,实测后果两条:

后果说明
当众出声展示机把探针里的字面文字念了出来,每小时一次
画面闪跳广播带不打断标记,原版切到全屏欢迎视频,约 1 秒后又收回

正解是给这个接口加一个静音探活分支:真的跑一遍语音合成(所以探活测到的仍是真实链路),但产物当场删除、不广播、不切画面。而且这个分支的判定必须放在忙不忙的判定之前——探活要永远测得到真东西。调用侧还要断言回包里的静音标记,防止新脚本配老后端的组合把噪声打回线上。

通用规则:任何读接口在动手前先问一句——它有没有副作用?有副作用(出声、切画面、占状态机、写文件)的接口,探活必须走专用分支,不能复用生产路径。

六、两个启动即崩的根因,与免 root 常驻

部署阶段还有两个必然踩到的坑:

  1. 头像源图目录混入非编号文件:加载器会把该目录下所有图片按文件名数字排序,混进一个非数字命名的文件就会抛 ValueError: invalid literal for int() with base 10。源图必须移出目录,只留编号帧。
  2. 进程清理命令自匹配自杀:用匹配进程名的方式清理服务时,命令会匹配到执行它的那个 shell 自己。用方括号技巧规避,或干脆不用这类命令。

免 root 常驻用用户级 systemd 最省事:写 ~/.config/systemd/user/<name>.service,然后 systemctl --user enable --now;关键一步是开启 loginctl enable-linger,这样无人登录时服务也能自启。端口、模型、人设全部可通过环境变量注入,改话术只动配置不动代码。

七、远端取证:CDP over SSH 隧道

展示机的浏览器带着调试端口,但它只监听本机回环地址,远端打不开。解法是用端口转发把它隧道过来,即可对正在运行的那个页面执行任意脚本:

用密钥建立连接 → 打开一个直连通道指向 127.0.0.1:9222 → 必须设通道超时
(不设超时会让读取永久阻塞)→ 页面列表可直接用命令行取回
→ 建立调试协议连接时省略 Origin 头(带上会被拒绝)

两个实践要点:一是把触发提问加按 400 毫秒采样若干秒写成一个返回 Promise 的脚本,用 await 模式一次取回整段日志,比注入采样器再外部等待再读稳得多(后者会被页面重载和陈旧定时器互相清掉搞坏)。二是自己用裸连接旁听广播通道,打印每条消息的类型与序号——这是判断页面收到的到底是不是我以为的最直接的办法,并发交错问题就是这样看出来的。

另外还有一条页面主动上报的通道:桥接里加一个诊断接口,向所有连接广播一个请求,页面收到后把当前状态(状态字段、二维码尺寸、画面源的实时程度等)回传。运维只要请求一次就能拿到现场快照,不用人站到机器前。注意跨域上报要回显来源并明确列头,用通配符在部分内核上会让预检失败,上报永远是 0 条。

还有一个死角要记住:没有活动桌面会话时,页面根本没起来,此时上报 0 条是机器没登桌面,不是页面坏了。同理,也不要拿命令查到的分辨率判断展示机是不是竖屏——无桌面会话时拿到的是驱动默认值,我们曾被这个假信息误导过半小时。设备是竖屏展示机,这是既定事实。

八、素材同源与竖屏铁律

切态时最容易出现人突然长个儿或歪头,根源是素材不同源。做法是用抽帧比对切换前后的首末帧是否为同一取景:本项目实测待机视频首末帧的人物横向占位是 322 到 745,而欢迎视频末帧是 315 到 762,宽了约 6%、头顶也偏。所以头像源图必须取自待机视频首帧的上半部分,绝不能取欢迎帧。

竖屏方面只有一条规则:设备一律按 1080×1920 单一坐标系,不做多比例适配。所有图层共用一个等比系数,就不会出现视口不是正好 1080×1920 时人像高度对不上的老 bug——那个 bug 的成因正是各层各按自己的规则缩放。展示机入口先用窗口态打开,再由人点页面内的全屏开关,不要一开就吞掉工具栏。

九、可复用的六条原则

  1. 现象独立不等于根因独立:三个 404 造成三个互不相干的症状,先看网络请求再定位,能省几天。
  2. 原版骨架加单点替换:把改动压缩成一个文件、一行引用,回滚就是删一行。
  3. 别碰原版的输入设备封装:包装取音接口会把依赖真实视频帧的在场检测一起废掉。
  4. 把字幕空当判据:区分没收到消息与收到了被打断,比看画面可靠。
  5. 探活前先问有没有副作用:有副作用的读接口必须走静音分支。
  6. 远端可取证:隧道到浏览器调试端口,把动态验证做成一次请求返回整段日志。

这套流程跑通之后,数字人在展厅可以连续无人值守运行,日常巡检只靠一个探活接口加一份状态快照就够了。

需要数字人 kiosk 部署与无人值守守护?看服务清单 →

常见问题

二维码是个白框,是图片没加载吗?

不是图片坏了,是二维码接口 404。前端那张图指向一个后端接口,原生产环境有、新机器上没有,所以图片元素裂成了白框。补齐这个接口(用二维码库现画 PNG)即可。

客户走近屏幕,为什么欢迎词不播?

先分清两件事:在场检测和播报是分离的。检测脚本只负责判断有没有人,命中后调用服务端的播报接口,真正出声的是后端。所以检测没反应的表象通常意味着后端接口 404,而不是检测逻辑坏了。

数字人在说话,但画面不切到口播视频,怎么查?

先看视频流本身是不是健康的(分辨率、就绪状态、是否暂停、是否已挂流)。如果流是好的,那就是状态机被回声自激反复打断:扬声器声音被麦克风收回、被判成客户插话、触发打断并清空音频队列。判据是字幕元素是否持续为空。

为什么不能拿播报接口做健康检查?

因为它有副作用:会合成音频、广播消息、切换画面。用它探活会让展示机当众把探针文字念出来,画面还会闪一下。要给这个接口加静音探活分支,产物当场删除,并且判定要放在忙判定之前。

远端怎么确认页面的实际状态?

展示机的浏览器调试端口只监听本机回环地址,用端口转发隧道过来即可对正在运行的页面执行脚本;再配合一条页面主动上报的通道,一次请求就能拿到现场快照,不用人到机器前。注意无桌面会话时页面没起来,上报为空属正常。