唤醒、识别、对话与播报¶
本页面向排查语音链路、接入新业务和维护 Ali flavor 的工程师,描述当前 Ali + Headless 路径。安装准备见快速开始,跨进程展示见 Omni 接入。
当前实现与历史描述¶
Ali 不是只有占位代码。AliASRAdapter 仍是返回空流的兼容适配器,但实际实时识别通过 AliDialogueRepositoryImpl 内的 MultiModalDialog 完成;TTS 使用 SDK 产生的音频,再由语音助手本地播放器播放。
同样,工厂注释中的“Stub 实现”不能代表整条 Ali 链路。判断能力时应沿实际依赖注入与 DialogueRepository 调用路径追踪。
| 组件 | 当前职责 |
|---|---|
KWSCoordinator |
常驻唤醒/命令词监听与分发 |
DialogueCoordinator |
初始化、启动和结束会话,处理外部播报请求 |
DialogueStateManager |
事件订阅、会话与录音衔接、TTS 播放调用 |
AliDialogueRepositoryImpl |
SDK 会话、音频输入、回调处理、后端路由与文本播报 |
AliCallbackAdapter / AliEventMapper |
将 SDK 回调转换成统一 ChatEvent |
A2aBridge |
最终 ASR 的后端落域请求与业务结果转换 |
DialogueEventBus |
将事件分发给展示、控制与播放链路 |
GrpcEventBroadcaster |
向 Omni 推送文字与状态 |
AppCtrlDispatcher |
按业务结果协调应用启动与待执行命令 |
一次完整对话¶
sequenceDiagram
participant U as 用户
participant K as KWS / Service
participant D as 对话协调与录音
participant A as Ali SDK
participant B as A2aBridge
participant P as 本地 TTS 播放器
participant O as Omni Launcher
U->>K: 唤醒词
K->>K: 校验登录状态
K->>D: 启动对话会话
D->>A: 建立 SDK 会话
D->>A: 连续上送 PCM
A-->>D: ASR partial / final
D-->>O: 用户文本与 THINKING
D->>A: 中断当前轮自动回复
D->>B: 最终 ASR / 上下文
B-->>D: 业务结果 / 命令 / 播报文本
D-->>O: 业务文字 / 动作卡片 / AppCtrl
D->>A: 请求文本播报
A-->>P: TTS 开始 / PCM / 流结束
P-->>D: 本地播放完成
D->>A: sendResponseEnded
D-->>O: SDK 可聆听后进入 LISTENING
图中表示主路径,不代表每一轮都会产生 TTS 或应用控制。车控、媒体、本地支付代理等按业务结果分别执行,详见业务能力。
1. 唤醒与会话启动¶
VoiceAssistantService 以麦克风前台服务承载常驻逻辑。在创建阶段绑定 KWS、ASR 协调器,启动业务分发和 gRPC 广播,并初始化对话协调器。
KWS 分发前检查登录态:未登录时发出登录引导并消费唤醒词,后续对话不会照常启动。排查“已检测到唤醒但没有会话”时应先确认这个前置条件。
当前 KWS 引擎由 KWS_ENGINE_PROVIDER 决定。Gradle 默认值为 CANGHAI,Ali 工厂也支持 ALI_NATIVE;构建属性可覆盖默认值。Ali 对话配置中的云端 KWS 默认关闭,因此不要把云端 KWS 开关与本地唤醒引擎混为一谈。
唤醒触发会话时先建立会话,再播放唤醒应答。对已有会话再次发出唤醒词会走重启路径,避免旧的“已启动”标记吞掉新一轮唤醒。TTS 播放期间 KWS 保持监听。
核心启动顺序是:
- 收集对话事件,确保新回调有消费者。
- 调用
startDialogueUseCase()建立后端会话。 - 启动录音并上送 PCM。
不能颠倒第二步与第三步:仓库会忽略非活跃会话收到的音频,先录音可能丢掉用户第一段话。
Headless 的含义¶
HEADLESS_MODE 默认是 true。OverlayWindowCoordinator.showWindow() 在该模式只标记逻辑会话,不调用悬浮窗 addView;关闭时清除标记。
Headless 仍保留录音、业务处理、TTS 和事件消费,不意味着“只开 gRPC”或“无本地声音”。默认展示依赖 Omni,调试时不要以语音助手没有悬浮窗判定启动失败。
2. 录音与 Ali 配置¶
当前 AliDataModule 配置如下;这是本仓实现值,不是所有 Ali SDK 项目的通用要求。
| 参数 | 当前值/行为 |
|---|---|
| 链路模式 | WEBSOCKET |
| 对话模式 | DUPLEX |
| 上行类型 | AudioOnly |
| 上行格式/采样率 | pcm / 16000 Hz |
| 下行格式/采样率 | pcm / 24000 Hz |
| 中间文本 | intermediateText="transcript" |
| 对话超时默认值 | 30000 毫秒 |
| 云端 KWS | enabled=false |
| ASR 断句相关配置 | semanticPunctuationEnabled、maxSentenceSilence、multiThresholdModeEnabled 当前为 null,沿用服务端默认值 |
配置对象支持 RTC 等模式,但当前注入路径选定 WebSocket。URL、鉴权信息、工作空间和应用标识由项目配置注入,公开文档不提供可用凭据。
录音数据经过有界队列交给发送协程,队列容量当前为 100 个音频块。发送队列关闭或满时增加丢帧计数并记录告警;队列满意味着发送侧落后,不能仅靠忽略日志处理。
使用 DialogueInput.AudioChunk 上送录音,使用 RefAudioChunk 上送参考音频,TextMessage 承载文本请求。未启动会话时输入会被日志提示并忽略,因此应等待会话建立,不依靠发送调用返回证明输入已被云端接受。
3. SDK 回调到识别结果¶
SDK 状态回调和识别内容回调有不同含义:
| 输入 | 内部事件 | 用途 |
|---|---|---|
DIALOG_LISTENING |
Asr.SpeechStarted |
SDK 已准备聆听,不证明用户已经开口 |
| 识别中内容 | Asr.TranscriptUpdate |
更新用户识别文字 |
| 完成识别内容 | Asr.TranscriptCompleted |
触发最终文本处理 |
DIALOG_THINKING / DIALOG_RESPONDING |
Llm.MessageStart |
对话输出阶段信号 |
DIALOG_IDLE |
Session.Stop |
SDK 会话空闲/停止信号 |
识别内容按会话、是否完成及文本去重。非空最终文本进入后端路由;如果最终文本只是 KWS 关键词,仓库会屏蔽落域并中断当前回复,避免把纯唤醒词当作业务问题。
与 Launcher 对接时,非空 final 会推动 ASSISTANT_STATE_THINKING。不要使用 DIALOG_LISTENING 推测真实 VAD 起点;更完整的事件字段见接口参考。
4. 后端落域与业务执行¶
主路径收到最终 ASR 后调用 forwardFinalAsrToBackend()。它先中断 Ali 当前轮自动回复,再通过 A2aBridge.handleFinalAsr() 进行后端落域,避免同一输入同时产生两套业务结果。
suspend fun handleFinalAsr(
asrText: String,
contextId: String?,
dialogId: String?,
): A2aBridgeResult
每次请求生成新的本地请求标记,取消前一个任务;返回后再次检查是否仍是当前请求,旧请求结果不会覆盖新一轮上下文。桥接结果可包含业务事件、播报文本、延迟命令以及新的上下文标识。
- 业务事件进入事件总线,由展示和控制组件各自消费。
- 非空播报文本通过 Ali 文本播报路径输出,当前轮已经中断过,不再为这次播报重复中断。
- 桥接的延迟命令与
Business.Result.pendingCommands的具体执行路径应分别追踪,不能仅以“收到了业务文字”证明外部应用已执行。 asrForwardEnabled=false的调试路径会跳过后端路由,不会自动回退为另一套真实业务路由。
桥接请求失败会产生 ChatEvent.Error;gRPC 广播将错误转为通用用户提示,结束当前展示态。详细排查依据仍是本地对应轮次日志,而不是只看用户提示文本。
5. TTS 传输与真实播放完成¶
音频回调产生 Tts.AudioChunk,DialogueStateManager 调用 TTS 用例播放。Ali 的 AliTTSAdapter 委托给 DefaultTTSPlayerAdapter,所以本地播放器是音频最终输出环节。
flowchart TD
Start[Tts.StreamStarted] --> Speaking[进入 SPEAKING]
Chunk[Tts.AudioChunk] --> Queue[本地音频缓冲与播放]
End[Tts.StreamCompleted] --> Drain[表示上游流已结束]
Queue --> Finished[Tts.PlayFinished]
Finished --> Gate[检查播放完成与 SDK Listening]
Listening[SDK Listening 回调] --> Gate
Gate --> Ready[两项均满足后进入 LISTENING]
StreamCompleted 只说明服务器音频传完,播放缓冲可能仍有内容。仓库在 onPlaybackFinished() 中调用 SDK 的 sendResponseEnded(),再发出 PlayFinished。
播报输出同时可作为参考音频交给 SDK。跟随聆听需要本地播放结束与 SDK Listening 两项同时满足;若只到达一项,Launcher 继续保持播报态。
当前 GrpcEventBroadcaster 不广播 TTS PCM,因此 Omni 不应重复播放音频。音频是否存在应检查本地播放与 SDK 回调,不应仅观察 gRPC 音频字段。
6. 中断、停止与调试输入¶
interrupt() 请求中断 SDK 当前回复;完整停止路径还需要停止本地 TTS、会话及录音。DialogueStateManager.stopDialogueSession() 负责将这些动作串联,单独停止 UI 不代表网络或录音已停止。
静默超时可能落后于真实语音活动。当前仓库使用单调时钟判断最近活动,过滤过期 SpeechTimeout;点单签约和仅文本播报阶段也有专门处理,不能把所有超时都等同于用户离开。
维护者还需要区分三种验证路径:
| 路径 | 会验证什么 | 不代表什么 |
|---|---|---|
| 真实唤醒 + 真实 ASR | 设备录音、SDK 识别、业务与展示 | 仍需核验各业务外部应用结果 |
| Mock 最终 ASR | 文本事件、后端路由和后续展示 | 不证明麦克风与识别正常 |
| Mock LLM 结果 | 事件解析、卡片和控制链路 | 不要求建立真实 Ali 会话 |
startDialogueSessionWithoutRecording() 用于仅文本播报,不启动录音。Mock LLM 管线可以只初始化事件观察;测试成功时必须注明输入路径,避免把注入结果当作真实语音端到端验收。
排障顺序与验收¶
| 现象 | 首先检查 |
|---|---|
| 唤醒无反应 | KWS 引擎配置、初始化、录音权限和登录拦截 |
| 第一段话缺字 | 是否先建立会话再启动录音;是否出现会话非活跃/丢帧日志 |
| 有 ASR 无业务 | 纯 KWS 过滤、asrForwardEnabled、A2a 请求及过期结果过滤 |
| 有文字无声音 | 文本是否需要播报、SDK TTS 回调、本地播放器、设备音频路由 |
| 声音结束仍显示播报 | PlayFinished 和 SDK Listening 是否都到达 |
| 会话提前关闭 | 当前轮活动时间、旧超时、会话中断事件和错误日志 |
单轮排查可沿 ASR_PARTIAL_FIRST → ASR_FINAL → A2A_FORWARD_ENTRY → A2A_BRIDGE_DONE → TTS_STREAM_STARTED → TTS_PLAY_FINISHED 关联时间线;无 TTS 的业务轮不会出现所有节点。
验收至少包含首次唤醒、连续两轮、播报中再次唤醒、空识别、后端失败、结束会话,以及 Headless 下 Omni 正常展示。不同板端的真实音频表现需要设备验证,静态代码核对不能替代实测。日志与音频诊断入口见调试与维护。