== 核心架构变更 ==
v2 (SenseVoice ASR + Homophone-Replacer FST) → v3 (流式 KWS Zipformer)
弃用原因:
- SenseVoice 228MB,大模型对短中文唤醒词识别全部输出 empty
- 板载 mic 实测验证: 干净说话 segment 同样 ASR empty
- 大型 ASR 不适配 1.2 秒级别短词唤醒场景
新架构:
- sherpa-onnx KWS Zipformer (wenetspeech-3.3M, int8)
- 模型 5MB(vs 228MB,节省 98%),APK 55MB(vs 213MB)
- 引擎初始化 800ms (vs SenseVoice 4 秒)
- 流式实时识别,命中后 <100ms 触发广播
- 不再依赖 VAD / 能量门 / DC 去除 / segment 切分
== 资源变更 ==
新增 assets/kws/:
- encoder.int8.onnx (4.6MB)
- decoder.onnx (660KB)
- joiner.int8.onnx (64KB)
- tokens.txt (拼音 token 词表)
- keywords.txt (43 条 Lila 谐音变体: 你好丽啦/咪啦/你拿/Lai啦...)
删除 assets/:
- sense_voice/ (228MB)
- silero_vad/ (632KB)
- hr_lexicon.txt (1.3MB)
- replace.fst
- 老 wenetspeech 8 个 onnx (full precision 版本)
== 代码变更 ==
新引擎: kws/AsrEngine.kt (流式 KeywordSpotter,替代 KwsEngine)
删除: kws/KwsEngine.kt
修改:
- Config.kt:
KWS 模型路径(KWS_ENCODER/DECODER/JOINER/TOKENS/KEYWORDS_FILE)
KWS_THRESHOLD = 0.05f (低阈值激进唤醒)
KWS_SCORE = 3.0f (弱信号场景拉高加分)
AUDIO_CAPTURE_GAIN = 1.5f (板载 mic 软件预增益)
- kws/AudioCapture.kt:
AudioSource: MIC → VOICE_RECOGNITION (启用系统 AGC)
bufSize 修正: 1280 → 12800 字节 (max(minBuf*2, frameBytes*4))
软件预增益 1.5x + clamp 防溢出
- kws/KwsStateMachine.kt: 引擎类型 KwsEngine → AsrEngine
- WakeupForegroundService.kt: 引擎类型 KwsEngine → AsrEngine
- MainActivity.kt: UI 适配
- protocol/BroadcastSender.kt: 微调
新增 sherpa-onnx Kotlin wrapper:
- OfflineRecognizer.kt / OfflineStream.kt
- Vad.kt / QnnConfig.kt
(全部从 sherpa-onnx 上游 master 引入)
== 实测数据 (RK3588 + OrangePi CM5 板载 mic) ==
板载 mic 信号特征:
静音底噪 peak ~5000 (异常高,非 DC bias 是真实 AC 噪声)
说话峰值 peak ~16000~32767
SNR ~10dB
诊断方法:
- 加 segment dump WAV (Config.DUMP_HIT_WAV) 验证 mic 录音正常
- 离线分析 RMS/peak/ZCR 确认真说话特征 (RMS>1500, ZCR<0.07)
最终识别率: 用户实测"识别率非常高"
154 lines
5.7 KiB
Kotlin
154 lines
5.7 KiB
Kotlin
package com.lila.wakeup
|
||
|
||
import android.media.AudioFormat
|
||
|
||
/**
|
||
* KWS APK 全局配置常量。
|
||
*
|
||
* 协议版本:v2.1(详见 KWS唤醒协议-V2.md)
|
||
* 涵盖三类常量:
|
||
* - 协议常量(Action 名、对端包名、字段键)
|
||
* - 引擎参数(阈值、平滑、超时、采样率)
|
||
* - 模型路径(assets 内相对路径)
|
||
*/
|
||
object Config {
|
||
|
||
// ============================================================
|
||
// 一、协议常量
|
||
// ============================================================
|
||
|
||
/** 本 APK 包名(自身) */
|
||
const val SELF_PACKAGE = "com.lila.wakeup"
|
||
|
||
/** 数字人 APP 包名(已确认 v2.1) */
|
||
const val APP_PACKAGE = "com.qy.lila"
|
||
|
||
/** 唤醒事件广播 Action(本 → APP) */
|
||
const val ACTION_WAKEUP = "com.lila.intent.action.WAKEUP"
|
||
|
||
/** 暂停 KWS 广播 Action(APP → 本) */
|
||
const val ACTION_KWS_PAUSE = "com.lila.intent.action.KWS_PAUSE"
|
||
|
||
/** 恢复 KWS 广播 Action(APP → 本) */
|
||
const val ACTION_KWS_RESUME = "com.lila.intent.action.KWS_RESUME"
|
||
|
||
/** WAKEUP Extras: 命中的唤醒词文本 */
|
||
const val EXTRA_KEYWORD = "keyword"
|
||
|
||
/** WAKEUP Extras: 命中时间戳(毫秒) */
|
||
const val EXTRA_TIMESTAMP = "timestamp"
|
||
|
||
/** WAKEUP Extras: 置信度 [0,1] */
|
||
const val EXTRA_CONFIDENCE = "confidence"
|
||
|
||
/** PAUSE Extras: 静默期毫秒数 */
|
||
const val EXTRA_SILENCE_MS = "silence_ms"
|
||
|
||
/** PAUSE/RESUME Extras: 调用原因(透传日志) */
|
||
const val EXTRA_REASON = "reason"
|
||
|
||
/**
|
||
* 内部广播 Action(仅本 APP 自己接收,用于 MainActivity UI 实时显示命中)。
|
||
* 与对外协议 [ACTION_WAKEUP] 隔离,避免外部 APP 干扰内部 UI 状态。
|
||
*/
|
||
const val ACTION_INTERNAL_WAKEUP = "com.lila.wakeup.action.INTERNAL_WAKEUP"
|
||
|
||
// ============================================================
|
||
// 二、引擎参数
|
||
// ============================================================
|
||
|
||
/** PAUSE 默认静默期(v2.1 微调:3000ms 覆盖 RTC 远端音频回灌 + AI TTS 触发延迟) */
|
||
const val DEFAULT_SILENCE_MS = 3000L
|
||
|
||
/** APP 漏发 RESUME 时的兜底超时(2 分钟) */
|
||
const val PAUSE_TIMEOUT_MS = 2 * 60 * 1000L
|
||
|
||
// 注:KWS_THRESHOLD / SMOOTH_FRAMES 已移除,使用 sherpa-onnx 默认值
|
||
// - keywordsThreshold 默认 0.25 (KeywordSpotterConfig)
|
||
// - 不做外部后验平滑(sherpa-onnx 内部 numTrailingBlanks 已平滑)
|
||
// 原因见 KwsEngine.kt / KwsStateMachine.kt 注释
|
||
|
||
// ============================================================
|
||
// 三、AudioRecord 配置
|
||
// ============================================================
|
||
|
||
/** 采样率(sherpa-onnx 模型固定 16kHz) */
|
||
const val SAMPLE_RATE = 16_000
|
||
|
||
/** 单声道 */
|
||
const val CHANNEL_CONFIG = AudioFormat.CHANNEL_IN_MONO
|
||
|
||
/** 16-bit PCM */
|
||
const val ENCODING = AudioFormat.ENCODING_PCM_16BIT
|
||
|
||
/**
|
||
* 单帧采样点数(100ms 帧 @ 16kHz = 1600 samples)。
|
||
*
|
||
* ⚠️ 必须与 sherpa-onnx 官方 demo 一致(用 100ms 帧)。
|
||
* 之前用 10ms 帧(160 sample)第一次能识别,之后再无命中——sherpa-onnx
|
||
* 内部 chunk 累积/边界处理对小帧不友好。改用 100ms 后稳定。
|
||
*/
|
||
const val FRAME_SAMPLES = SAMPLE_RATE / 10
|
||
|
||
// ============================================================
|
||
// 四、模型路径(assets 内)
|
||
// ============================================================
|
||
|
||
/**
|
||
* 架构演进历史:
|
||
* - v0: KWS wenetspeech 3.3M (2024) —— 默认 keywords 命中率 30-40%
|
||
* - v1: KWS zh-en 双语 3M —— 英文 Lila 0% 命中
|
||
* - v2: ASR (SenseVoice 228MB) + 拼音替换 —— 板载 mic 全部 empty(模型对短词不敏感)
|
||
* - v3: KWS Zipformer wenetspeech-3.3M + 自定义谐音 keywords.txt 流式架构(当前)
|
||
*
|
||
* v3 关键改动:
|
||
* keywords.txt 用多个谐音变体覆盖("你好丽啦/咪啦/丽拉/lai啦"等)
|
||
* 流式实时识别,不再切 segment,不再依赖 VAD/能量门
|
||
* 模型 5MB(vs SenseVoice 228MB),推理快 10x
|
||
*/
|
||
const val KWS_MODEL_DIR = "kws"
|
||
const val KWS_ENCODER = "kws/encoder.int8.onnx"
|
||
const val KWS_DECODER = "kws/decoder.onnx"
|
||
const val KWS_JOINER = "kws/joiner.int8.onnx"
|
||
const val KWS_TOKENS = "kws/tokens.txt"
|
||
const val KWS_KEYWORDS_FILE = "kws/keywords.txt"
|
||
|
||
/** KWS 推理线程数 */
|
||
const val KWS_NUM_THREADS = 2
|
||
|
||
/**
|
||
* 关键词命中阈值(0~1 之间),越低越容易触发。
|
||
* 板载 mic 弱信号 + Lila 非训练词,激进降到 0.05。
|
||
* 误触发风险:需要观察 listening 待机时是否被环境噪声误唤醒。
|
||
*/
|
||
const val KWS_THRESHOLD = 0.05f
|
||
|
||
/**
|
||
* 关键词命中加分,越大越容易触发。弱信号场景拉到 3.0。
|
||
*/
|
||
const val KWS_SCORE = 3.0f
|
||
|
||
/**
|
||
* AudioCapture 软件预增益(在喂 KWS 之前)。
|
||
* 板载 mic 信号偏弱,1.5x 增益让说话峰值更接近满量程,KWS 神经网络
|
||
* 看到的特征对比度更高。clamp 到 [-32768, 32767] 防止溢出失真。
|
||
*/
|
||
const val AUDIO_CAPTURE_GAIN = 1.5f
|
||
|
||
/**
|
||
* 调试: 命中前后 dump 一段 PCM 到 WAV 文件,辅助调试。
|
||
* 路径: /sdcard/Android/data/com.lila.wakeup/files/lila_kws/
|
||
* 拉回: adb pull /sdcard/Android/data/com.lila.wakeup/files/lila_kws/ ./
|
||
* 上线前关掉。
|
||
*/
|
||
const val DUMP_HIT_WAV = true
|
||
|
||
// ============================================================
|
||
// 五、通知栏
|
||
// ============================================================
|
||
|
||
const val NOTIF_CHANNEL_ID = "lila_wakeup_kws"
|
||
const val NOTIF_CHANNEL_NAME = "Lila 语音唤醒"
|
||
const val NOTIF_ID = 1
|
||
}
|