From 6f96f3de875d3711e6c2d4c8fc3fbdcdb00c9173 Mon Sep 17 00:00:00 2001
From: cai <cai@nbcai.cc>
Date: Sat, 08 Aug 2026 18:24:36 +0800
Subject: [PATCH] chore: bind helper metadata candidate manifest

---
 README.md |   43 +++++++++++++++++++++++++++++++++++++++----
 1 files changed, 39 insertions(+), 4 deletions(-)

diff --git a/README.md b/README.md
index 9ea88e6..6e38c05 100644
--- a/README.md
+++ b/README.md
@@ -54,9 +54,12 @@
 ```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 已通过。
 
 ## 本机运行
 
@@ -98,6 +101,10 @@
 
 `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}`。
@@ -123,6 +130,34 @@
 `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 口径。
 
 ## 本机固定问候音频
 
@@ -173,7 +208,7 @@
 <callId>-greeting-target.wav
 ```
 
-其中 `source` 是原始主动问候音频,`target` 是推送给 LiveKit 前的 `48kHz/mono/16-bit` WAV。文件只用于本地回听排查,不进入 Git、不写入协作事件正文,不上传到线上环境。
+其中 `source` 是原始主动问候音频,`target` 是推送给 LiveKit 前的目标 profile WAV。默认是 `16kHz/mono/16-bit`;只有显式配置 `CV_BOT_AUDIO_PROFILE=livekit-48k` 时才会生成 `48kHz/mono/16-bit` target。文件只用于本地回听排查,不进入 Git、不写入协作事件正文,不上传到线上环境。
 
 ## 用户上行音频观测
 
@@ -207,7 +242,7 @@
 export CV_ENABLE_SIMPLE_VAD=true
 ```
 
-首版 VAD 不引入外部模型,只基于上行 PCM frame 的 RMS / peak 做保守阈值判断。配置了 Java runtime turn bridge 后,helper 会在有效 `vad_speech_end` 后把本轮 PCM 写成 `user.wav` artifact,并调用 Java 内部 bridge;helper 仍不做 ASR / LLM / TTS / 消息落库。
+首版 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 / 消息落库。
 
 可调参数:
 
@@ -215,8 +250,8 @@
 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=700
-export CV_VAD_MIN_SPEECH_MS=300
+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

--
Gitblit v1.9.3