From b0a4e2ea93fafc4d65d474f3ada616a0ad5e19d5 Mon Sep 17 00:00:00 2001
From: Ariver <ar@Arm1.local>
Date: Sun, 12 Jul 2026 11:39:35 +0800
Subject: [PATCH] feat: bind helper stream timing markers
---
README.md | 294 +++++++++++++++++++++++++++++++++++++++++++++++++---------
1 files changed, 249 insertions(+), 45 deletions(-)
diff --git a/README.md b/README.md
index 020355c..6e38c05 100644
--- a/README.md
+++ b/README.md
@@ -1,100 +1,304 @@
-# lm-livekit-helper
+# Combrabo Voice Runtime Helper
-`lm-livekit-helper` 是 Combrabo Voice 的 LiveKit 媒体 worker。它从 `lmrobot-app` 接收每次通话的 `CV_*` 运行参数,加入 LiveKit 房间,发布 bot 音轨,播放主动问候音频,并在 S7 阶段承接用户语音 turn 的媒体输入输出。
+这是 `lmrobot-app` Combrabo Voice 一阶段的最小 external runtime helper。它的长期定位是 LiveKit media worker:连接房间、发布 bot 音轨、订阅用户音轨、搬运 / 观测 PCM、输出脱敏诊断。
-它不是业务服务,不承担鉴权、数据库、订单、ASR/LLM/TTS 权威、消息落库或 diagnostics 聚合。业务权威仍在 `lmrobot-app`。
+当前职责分两层:
-## 当前职责
+一阶段固定主动问候职责有 4 件事:
-1. 读取 `lmrobot-app` 注入的 `CV_*` 环境变量;
-2. 使用 bot token 连接 LiveKit room;
+1. 读取 `CombraboVoiceRuntimeServiceImpl` 注入的 `CV_*` 环境变量;
+2. 使用 bot token 连接 local LiveKit room;
3. 发布 bot 本地音轨;
-4. 播放 WAV / MP3 主动问候音频;
-5. 在 smoke 模式下通过 LiveKit reliable Data Message 下发 `device_output`;
-6. S7 阶段通过 Java runtime turn bridge 复用后端 ASR / TextChat / TTS / Message 能力。
+4. 播放一段固定问候音频,并保持连接直到 `calls/end` 触发 stop。
+
+2026-06-24 起进入 helper 下一阶段入口:默认开启用户上行音频观测,先做远端用户音轨订阅和音频帧摘要日志,不做 ASR/LLM/TTS。
+
+它**不承担**鉴权、数据库、订单、旧 TRTC、角色 / 提示词、正式 ASR/LLM/TTS 编排、消息写入、计费或 diagnostics 聚合。上述业务能力继续由 `lmrobot-app` Java 后端复用现有体系承接。
+
+本定位已按 `cb-sdk` 真实链路校准:服务端 worker 负责 LiveKit 用户音频输入与 bot 音频输出,后端 speech-runtime 负责 ASR / Agent / TTS / turn 状态。`lmrobot` 当前 helper 只对齐 media worker / rtc 边界,不新建第二套业务后端。
## 本机构建
-```bash
-cargo build --release
+helper 编译固定分为“开发调试线”和“部署验证线”。详细口径见:
+
+```text
+doc/task/202606/0615-nativesdk-combrabo-voice-migration/29-helper编译与验证线路说明.md
```
-helper 目录内带 `.cargo/config.toml`,本机构建建议通过本仓库根目录执行,确保使用同一套 registry / retry 配置。
+本机开发调试优先复用已下载的 LiveKit WebRTC 预编译缓存:
+
+```bash
+export LK_CUSTOM_WEBRTC="$HOME/.cache/combrabo/livekit-webrtc/mac-arm64-release-webrtc-51ef663"
+```
+
+然后执行:
+
+```bash
+cargo fmt --manifest-path tools/combrabo-voice-runtime-helper/Cargo.toml --check
+cargo check --manifest-path tools/combrabo-voice-runtime-helper/Cargo.toml
+cargo test --manifest-path tools/combrabo-voice-runtime-helper/Cargo.toml
+```
+
+`LK_CUSTOM_WEBRTC` 指向的是 macOS host 开发调试缓存,不能直接用于 Linux Docker 镜像构建。
+
+```bash
+cargo build --manifest-path tools/combrabo-voice-runtime-helper/Cargo.toml --release
+```
+
+helper 目录内带 `.cargo/config.toml`,本机构建建议通过 `run-local.sh` 或进入 helper 目录执行,确保使用同一套 registry / retry 配置。
+
+## Turn stream 快速校验
+
+TTS streaming 相关改动优先跑一条不依赖 LiveKit / WebRTC native 编译链路的快速线:
+
+```bash
+node tools/validate-turn-stream-fixture.mjs fixtures/turn-stream-happy.ndjson
+node tools/validate-turn-stream-fixture.mjs fixtures/turn-stream-mp3-chunks.ndjson
+node tools/validate-asr-realtime-fixture.mjs fixtures/asr-realtime-happy.ndjson
+```
+
+这条快速线只校验 NDJSON contract、事件顺序和 `pcm_s16le` / `mp3` chunk 基本约束,用于提前发现 `replyPlaybackMode`、`reply_state`、`reply_audio_chunk`、`turn_completed` 等字段破坏。它不能替代 Docker 镜像构建、真实 LiveKit smoke 或 iPhone 真机验收。
+
+ASR realtime fixture 额外校验 `session_start -> audio_chunk -> vad_speech_end -> finish`、连续 `chunkSeq`、`pcm_s16le / 16000Hz / mono` 与 `24KiB` 单行上限。它只验证 helper 到 Java 的 wire contract,不代表 provider partial/final 已通过。
## 本机运行
推荐通过包装脚本启动:
```bash
-./run-local.sh
+./tools/combrabo-voice-runtime-helper/run-local.sh
```
-如果本地还没有 release 二进制,脚本会先执行构建。macOS 默认优先使用 Docker 模式,避免 host 侧 WebRTC native 依赖链路反复阻塞。
+如果本地还没有 release 二进制,脚本会先执行一次构建。
-可预先准备运行模式:
+开发调试时建议显式带上本机 WebRTC 缓存:
```bash
-./run-local.sh --prepare
-./run-local.sh --prepare --rebuild
+LK_CUSTOM_WEBRTC="$HOME/.cache/combrabo/livekit-webrtc/mac-arm64-release-webrtc-51ef663" \
+COMBRABO_VOICE_RUNTIME_HELPER_MODE=host \
+./tools/combrabo-voice-runtime-helper/run-local.sh --prepare
```
+
+## Service 模式
+
+`lmrobot-app` 线上形态不再适合每次通话直接用命令行拉起 helper。当前 helper 支持 service 外壳:
+
+```bash
+CV_HELPER_SERVICE_ENABLED=true \
+CV_HELPER_SERVICE_BIND=127.0.0.1:18080 \
+CV_HELPER_AUTH_TOKEN=local-helper-token \
+CV_RUNTIME_TURN_BRIDGE_TOKEN=local-turn-bridge-token \
+./target/release/combrabo-voice-runtime-helper
+```
+
+service 模式下,helper 只暴露控制面接口:
+
+- `GET /health`
+- `GET /internal/combrabo-voice/health`
+- `POST /internal/combrabo-voice/sessions/start`
+- `GET /internal/combrabo-voice/sessions/{callId}`
+- `POST /internal/combrabo-voice/sessions/{callId}/stop`
+
+`sessions/start` 收到 Java 传入的 LiveKit bot 入房材料后,会拉起现有 worker 子进程承接媒体链路。helper service 本身不做 ASR / LLM / TTS / 消息 / 计费,也不持久化业务数据。
+
+当 Java 在 `sessions/start.runtime` 下发 `asrRealtimeEnabled=true`、`asrRealtimeUrl` 与 `asrRealtimeChunkDurationMs=200` 时,worker 会从 VAD speech start 起接收 20ms 用户音频帧,聚合成 `16kHz / mono / 200ms` NDJSON chunk 持续上传给 Java;最后一块允许短于 200ms。helper 只负责音频传输和 fallback 编排,ASR provider session、partial/final 归一化、activity 和 `asrResultRef` 仍由 Java 管理;realtime 失败时只回退一次既有 final-only ASR stream。
+
+用户音轨由独立 drain task 持续读取 `NativeAudioStream`,并通过容量 `100` 的原始 PCM frame 队列交给 VAD / realtime ASR 消费;ASR final、turn bridge、LLM/TTS 或控制面等待不得阻塞 LiveKit 原生音频队列。drain 队列满时丢最新 frame,并低频记录累计接收/丢弃计数;VAD 使用 frame 捕获时的单调时间戳,ASR 始终消费原始 PCM,并保留 speech start 确认窗口中的 prefix frame。该结构参考 `cb-sdk/combrabo-platform origin/stag@a81f17c` 的音频排空与 raw PCM 修复经验。
+
+鉴权口径:
+
+- Java 调 helper 控制面使用 `Authorization: Bearer {helperAuthToken}`。
+- `authProfile` 只是非敏感 alias,首版允许 `default / local-dev / dev`。
+- helper 根据自身部署配置读取同名 profile 的 `turnBridgeToken`,再用于调用 Java internal turn bridge。
+- token 只能来自本机私有 `.env`、Jenkins credentials 或服务器 ENC 配置,不写入 Git。
+
+Docker service mode 本地启动时,`run-local.sh` 会把 `CV_HELPER_SERVICE_BIND=127.0.0.1:18080`
+映射为容器内 `0.0.0.0:18080` 并发布到宿主 `127.0.0.1:18080`。因此本机探活使用:
+
+```bash
+CV_HELPER_SERVICE_ENABLED=true \
+CV_HELPER_SERVICE_BIND=127.0.0.1:18080 \
+CV_HELPER_AUTH_TOKEN=local-helper-token \
+CV_RUNTIME_TURN_BRIDGE_TOKEN=local-turn-bridge-token \
+COMBRABO_VOICE_RUNTIME_HELPER_MODE=docker \
+./run-local.sh
+
+curl http://127.0.0.1:18080/health
+```
+
+`sessions/start` 会等待 worker 回报真实 readiness:worker 在 LiveKit connect 后输出
+`bot_participant_joined`,在 bot audio track 发布后输出 `bot_track_ready`。service 捕获这些
+结构化事件后才返回 `STARTED + botTrackReady=true`;如果 worker 失败或超时,则返回结构化
+`RUNTIME_START_FAILED` / `RUNTIME_START_TIMEOUT`。
+
+## Bot 音频输出 profile
+
+helper 的 bot 下行音频输出通过部署配置选择,不通过 `sessions/start` 临时传业务参数。本轮 Lmtest / Combrabo Voice 动态回复主链路按用户最新裁决收口为全链路 `16kHz / mono / pcm_s16le`:Java stream bridge 下发 `sampleRate=16000/channels=1`,helper 写入 LiveKit bot track 前也保持 16k,不在业务层显式转成 48k。
+
+```bash
+# 默认值和本轮目标链路:bot NativeAudioSource = 16kHz / mono。
+export CV_BOT_AUDIO_PROFILE=pcm-16k
+
+# 历史 / 紧急回退:bot NativeAudioSource = 48kHz / mono。
+# 不作为本轮 Lmtest 动态回复主链路或音质验收目标。
+export CV_BOT_AUDIO_PROFILE=livekit-48k
+
+# 调试值:显式指定采样率和声道。
+export CV_BOT_AUDIO_PROFILE=custom
+export CV_BOT_SAMPLE_RATE_HZ=16000
+export CV_BOT_NUM_CHANNELS=1
+```
+
+`bot_track_ready` activity 会回写 `audioProfile`、`sampleRate`、`numChannels`,这是 smoke 判断当前运行 profile 的准确信号。动态回复 `bot_reply_audio_write_finished` 会回写 source / target 元数据:
+
+- `sourceSampleRate` / `sourceChannels`:Java stream bridge 下发的 `reply_audio_chunk` 源格式。
+- `targetAudioProfile` / `targetSampleRate` / `targetChannels`:helper 写入 LiveKit bot track 前的目标格式。
+- `networkChunkCount`、`debugSourcePath`、`debugPcmWavPath`:用于脱敏排查 chunk/frame 对齐与听感问题。
+
+本轮验收要求动态回复 `stream-reply-{turnId}-source.wav` 与 `stream-reply-{turnId}-target.wav` 都是 `16000Hz/mono/pcm_s16le`。如果 target 仍是 `48000Hz`,视为未完成全链路 16k 目标。
+
+当前只把 bot 下行输出 profile 做成可配置。用户上行采集、VAD 和 turn artifact 首版仍保持 `48kHz / mono`,避免牵动 ASR 输入、turn bridge 和历史 smoke 口径。
## 本机固定问候音频
-helper 支持 WAV / MP3 音频自动识别。主动问候 `prepare` 生成的 MP3 可以直接进入 helper;本地 smoke 仍可使用 WAV fixture 作为固定兜底样本。
+helper 当前 MVP 支持 **WAV / MP3** 音频自动识别。主动问候 `prepare` 生成的 MP3 可以直接进入 helper;本地 smoke 仍可使用 WAV fixture 作为固定兜底样本。
-在 macOS local 环境,可先生成一份本机固定问候 WAV fixture:
+在 macOS local 环境,推荐先生成一份本机固定问候 WAV fixture:
```bash
-./generate-local-fixture.sh
+./tools/combrabo-voice-runtime-helper/generate-local-fixture.sh
```
默认输出:
```text
-.local/greeting-local.wav
+tools/combrabo-voice-runtime-helper/.local/greeting-local.wav
```
-## lmrobot-app 接线示例
+## 推荐 launch-command
+
+`lmrobot-app` local 推荐设置:
```bash
-export COMBRABO_VOICE_RUNTIME_WORKDIR=/opt/lmrobot/lm-livekit-helper
-export COMBRABO_VOICE_RUNTIME_LAUNCH_COMMAND=/opt/lmrobot/lm-livekit-helper/run-local.sh
-export COMBRABO_VOICE_RUNTIME_HELPER_MODE=docker
-export COMBRABO_VOICE_RUNTIME_HELPER_IMAGE=registry.example.com/lm-livekit-helper:git-sha
-export COMBRABO_VOICE_RUNTIME_FALLBACK_GREETING_AUDIO_PATH=/opt/lmrobot/lm-livekit-helper/.local/greeting-local.wav
+export COMBRABO_VOICE_RUNTIME_WORKDIR=/Users/ar/Applications/combrabo/wwww
+export COMBRABO_VOICE_RUNTIME_LAUNCH_COMMAND=./tools/combrabo-voice-runtime-helper/run-local.sh
+export COMBRABO_VOICE_RUNTIME_FALLBACK_GREETING_AUDIO_PATH=/Users/ar/Applications/combrabo/wwww/tools/combrabo-voice-runtime-helper/.local/greeting-local.wav
```
-`CV_LIVEKIT_URL`、`CV_LIVEKIT_ROOM_ID`、`CV_LIVEKIT_BOT_TOKEN`、`CV_LIVEKIT_BOT_PARTICIPANT_IDENTITY` 等连接材料由 `lmrobot-app` 在每次 `calls/start` 时生成并注入,不写入仓库。
+## 主动问候音频质量诊断
-## LiveKit Data 设备输出 smoke
+helper 会在播放主动问候前输出脱敏音质摘要,覆盖:
-需要验证 NativeSDK 是否能收到设备输出 Data Message 时,在启动 `lmrobot-app` 前打开:
+- 源格式、源字节数、源采样率、源声道数;
+- 解码样本数、解码时长、目标采样率、目标声道数;
+- 20ms frame 数、理论播放时长、实际推帧 wall duration、推帧漂移;
+- RMS、peak、削波样本数、静音比例;
+- MP3 `SkippedData` / `InsufficientData` 计数。
+
+本地排查音质问题时,可以额外设置 debug dump 目录:
```bash
-export CV_DEVICE_OUTPUT_SMOKE_ENABLED=true
+export CV_AUDIO_DEBUG_DUMP_DIR=/tmp/combrabo-voice-audio-debug
```
-helper 会在 bot 进房并发布音轨后,向 `CV_LIVEKIT_USER_PARTICIPANT_IDENTITY` 指向的 SDK client 发送 reliable Data Message:
+开启后 helper 会保留两类文件:
-- `topic`: `device_output`
-- `type`: `device_output`
-- `schemaVersion`: `1.0`
-- `commandCode`: 例如 `vibration.start`
-- `params`: 设备参数
+```text
+<callId>-greeting-source.<wav|mp3>
+<callId>-greeting-target.wav
+```
-如需指定接收方,可使用英文逗号分隔:
+其中 `source` 是原始主动问候音频,`target` 是推送给 LiveKit 前的目标 profile WAV。默认是 `16kHz/mono/16-bit`;只有显式配置 `CV_BOT_AUDIO_PROFILE=livekit-48k` 时才会生成 `48kHz/mono/16-bit` target。文件只用于本地回听排查,不进入 Git、不写入协作事件正文,不上传到线上环境。
+
+## 用户上行音频观测
+
+helper 默认开启下一阶段入口观测:
```bash
-export CV_DEVICE_OUTPUT_DESTINATION_IDENTITIES=client-identity-a,client-identity-b
+export CV_ENABLE_USER_AUDIO_OBSERVER=true
```
-如果没有指定接收方且没有 `CV_LIVEKIT_USER_PARTICIPANT_IDENTITY`,helper 会广播到 room。设备控制建议继续使用 reliable/ordered Data Message;ACK / 执行结果首版建议走 HTTP,便于落库、重试和排查。
+如需临时回退为纯固定问候播放器,可关闭:
-## 文档
+```bash
+export CV_ENABLE_USER_AUDIO_OBSERVER=false
+```
-- [Runtime Contract](docs/runtime-contract.md)
-- [Jenkins Build And Deploy Runbook](docs/jenkins-build-deploy.md)
+开启后 helper 会监听 LiveKit `TrackSubscribed` 事件,并对远端用户音频轨道输出脱敏日志:
-## 安全要求
+- `runtime helper user_track_subscribe_requested`
+- `runtime helper user_track_subscribed`
+- `runtime helper user_audio_frame_received`
+- `runtime helper user_audio_frame_summary`
+- `runtime helper user_audio_stream_ended`
-仓库内只允许提交 `.env.example` 这类脱敏样例。禁止提交真实 LiveKit token、API key、secret、roomId、participantIdentity、用户语音内容、ASR 文本、LLM 回复全文或完整 prompt。
+这些日志只记录 `call_id`、`trace_id`、脱敏 participant alias、脱敏 track sid、采样率、声道、帧数和时间摘要。禁止记录 token、room secret、真实 participantIdentity、完整 roomId、音频内容、ASR 文本、用户语音内容或 AI 回复文本。
+
+## 轻量 VAD / turn detection
+
+在确认 helper 能收到用户上行音频帧后,helper 默认开启轻量 VAD:
+
+```bash
+export CV_ENABLE_SIMPLE_VAD=true
+```
+
+首版 VAD 不引入外部模型,只基于上行 PCM frame 的 RMS 做保守阈值判断,peak 只作为诊断字段记录。这个口径对齐 cb-sdk 的能量阈值分段思路,避免单个尖峰噪声反复打断静音窗口,使用户 turn 拖到 `max_turn_ms`。配置了 Java runtime turn bridge 后,helper 会在有效 `vad_speech_end` 后把本轮 PCM 写成 `user.wav` artifact,并调用 Java 内部 bridge;helper 仍不做 ASR / LLM / TTS / 消息落库。
+
+可调参数:
+
+```bash
+export CV_VAD_RMS_THRESHOLD=0.012
+export CV_VAD_PEAK_THRESHOLD=0.08
+export CV_VAD_START_FRAMES=5
+export CV_VAD_END_SILENCE_MS=400
+export CV_VAD_MIN_SPEECH_MS=250
+export CV_VAD_MAX_TURN_MS=10000
+export CV_VAD_INITIAL_IGNORE_MS=500
+export CV_VAD_GATE_UNTIL_GREETING_DONE=true
+export CV_VAD_POST_GREETING_DELAY_MS=800
+```
+
+Java turn bridge 相关配置:
+
+```bash
+export CV_RUNTIME_TURN_BRIDGE_URL=http://127.0.0.1:11102/internal/sdk/combrabo-voice/runtime/turns
+export CV_RUNTIME_TURN_BRIDGE_TOKEN=local-dev-token
+export CV_RUNTIME_TURN_ARTIFACT_DIR=/tmp/combrabo-voice-runtime/turn-artifacts
+export CV_RUNTIME_SESSION_NONCE=runtime-session-nonce
+```
+
+这些值由 `lmrobot-app` 启动 runtime helper 时注入。Docker 模式会透传 env,并把 `CV_RUNTIME_TURN_ARTIFACT_DIR` 挂载进容器。日志不得打印 `CV_RUNTIME_TURN_BRIDGE_TOKEN`。
+
+本机 Docker 模式下,`run-local.sh` 会自动把 `CV_RUNTIME_TURN_BRIDGE_URL` 中的 `127.0.0.1` / `localhost` 改写为 `host.docker.internal`,避免 helper 容器把 Java internal turn bridge 误解析为容器自身。
+
+开启后新增脱敏事件:
+
+- `runtime helper vad_disabled_greeting`
+- `runtime helper vad_enable_scheduled`
+- `runtime helper vad_enabled`
+- `runtime helper vad_ignored_before_enabled`
+- `runtime helper vad_speech_start`
+- `runtime helper vad_speech_end`
+- `runtime helper vad_speech_too_short`
+- `runtime helper vad_no_speech_summary`
+- `runtime helper turn_artifact_written`
+- `runtime helper turn_bridge_completed`
+- `runtime helper turn_bridge_failed`
+- `runtime helper turn_bridge_skipped`
+
+这些事件只记录 `call_id`、`trace_id`、脱敏 participant alias、脱敏 track sid、turn 序号、起止时间、帧数、样本数、RMS / peak 摘要、结束原因、artifact 字节数、bridge HTTP / reasonCode 摘要。它们不记录用户音频内容、ASR 文本、LLM 回复、TTS URL、bridge token 或本机绝对 artifact 路径。
+
+默认启用 `CV_VAD_GATE_UNTIL_GREETING_DONE=true`,即主动问候播放完成前不产生 `vad_speech_start/end`。问候播放结束后,helper 会按 `CV_VAD_POST_GREETING_DELAY_MS` 延迟开启 VAD;门禁期间收到的用户音频帧只输出 `vad_ignored_before_enabled` 摘要,用于证明上行音频仍在,但不会形成用户 turn。
+
+当前 local 真机复测默认 `CV_VAD_POST_GREETING_DELAY_MS=800`,用于在主动问候实际写完后保留短尾缓冲,避免 15 秒固定禁听窗口吞掉用户首句。若需要做受控回声隔离实验,可以临时调大该值,但不得作为产品化默认值。
+
+## 当前边界
+
+- `CV_GREETING_AUDIO_FILE`:优先读取本地 WAV / MP3 文件;
+- `CV_GREETING_AUDIO_URL`:支持下载后按 WAV / MP3 解析;
+- 没有问候音频时,helper 仍会发布 bot track 并保持连接,但不会主动播放音频;
+- 如果传入了问候音频路径/URL,但内容不是可解析的 WAV / MP3,helper 会启动失败并把错误返回给 `calls/start`。
+- 用户上行音频观测只证明 helper 能订阅用户音轨并收到帧,不代表 ASR/LLM/TTS 动态对话已经完成。
+- 轻量 VAD 在 bridge 配置完整时会生成 `user.wav` artifact 并调用 Java internal turn bridge;bridge 配置缺失时只记录 `turn_bridge_skipped`,不阻断 helper 进程。
+- 当前阶段只完成用户 turn artifact 和 Java bridge 请求;第二段 AI 回复音频写回 bot track 属于下一阶段。
+- 主动问候、固定 TTS 和后续动态回复必须统一写入 helper 发布的 LiveKit bot 音轨;不允许另起本地播放旁路。
--
Gitblit v1.9.3