Commit f6ea102833863940533286f2d51374d41c2aaa20

Authored by yibo
1 parent 48cc6305

docs(lifelog-recorder): 加 HANDOFF.md — 接手地图 + 避雷清单

§零 环境铁律:whisper 别当默认 STT、模型别提交、asset 不走 mmap、
线程甜点是 4、AID_INET 必须传、keys.properties gitignored。

§二 核心文件清单:bot 实跑只看 RecorderService.kt + ParaformerEngine.kt
+ MainActivity.kt,sherpa-onnx Kotlin 绑定整目录原样塞别动。

§三 开发内循环、§四 pipeline 链路图、§五§六 优先级排序的 7 项待办、
§七 踩过的坑清单(VAD 资产 mmap SIGSEGV / silero_vad.onnx LFS 拦截 /
paraformer t=8 反慢 / hf-mirror 截断 / am 启动非 exported 服务 /
su 切 uid 丢 AID_INET)、§九 接手 prompt。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Showing 1 changed file with 212 additions and 0 deletions
lifelog-recorder/HANDOFF.md 0 → 100644
  1 +# lifelog-recorder — HANDOFF
  2 +
  3 +> Pixel 8 Pro 上 24/7 录音 + 本地中文 ASR + 时间轴 lifelog 应用。
  4 +> 接手前先把下面 §零 看完,免得踩坑。
  5 +
  6 +---
  7 +
  8 +## §零 不可以的事 / 必读约束
  9 +
  10 +1. **不要把 Whisper 当默认 STT**。Tensor G3 上 whisper-small 实测 RTF 1.76(0.57× realtime),跑不动。已确定走 **sherpa-onnx + paraformer-zh int8**,Tensor G3 实测 RTF 0.04(**25× realtime**)。
  11 +2. **不要把模型文件提交到仓库**。`model.int8.onnx` 是 232 MB,首次启动从 hf-mirror 自动下载到 `app.filesDir/models/paraformer-zh/`。app 内只内嵌 silero VAD(629 KB)+ sample.wav(3 MB)。
  12 +3. **不要从 assets 加载 onnx**:sherpa-onnx native 代码对 asset-mmap 有兼容问题(v1.13.2 上 silero VAD 直接 SIGSEGV)。已改成「assets 拷到 filesDir 后用 file path 加载」,见 `RecorderService.runPipeline()` 里 vadFile 那段。
  13 +4. **Tensor G3 线程甜点 = 4**。实测 paraformer t=4 比 t=8 快(8 时 1×X3 + 4×A715 协调开销大于收益)。改 `ParaformerEngine.load(numThreads=4)` 之前先 bench。
  14 +5. **`-G 3003`(AID_INET)**:跨设备 root + adb push 调试时,用 `su 10288 -c` 切换到 Termux uid 跑命令必须带 `-G 3003`,否则 DNS 全断。详见 `~/.claude/projects/-Users-yibo-Documents-aipcrm/memory/magisk-su-android-inet-group.md`。
  15 +6. **keys.properties 是 gitignored**。模板在 `keys.properties.example`。讯飞凭证从 [console](https://console.xfyun.cn/services/iat) 申请,写进去就行。
  16 +7. `local.properties` 也 gitignored。`sdk.dir=/opt/homebrew/share/android-commandlinetools`(macOS brew 路径,换机改这个)。
  17 +
  18 +---
  19 +
  20 +## §一 这是什么
  21 +
  22 +24/7 后台录音 → VAD 切段 → 本地 paraformer 中文 ASR → 入库 → 时间轴显示 → (未来)LLM 纪要。
  23 +
  24 +**完全本地,零云端依赖**(讯飞客户端只作云复核 fallback)。
  25 +
  26 +### 实测性能(Pixel 8 Pro / Tensor G3 / Android 16 BP2A)
  27 +
  28 +```
  29 +模型冷启动: 1.3 s(一次性)
  30 +VAD 延迟: <1 ms/30ms 帧
  31 +ASR: 20-32× realtime(典型段 1-10s → 50-300ms 转写)
  32 +APK 大小: 62 MB(含 34 MB sherpa-onnx .so + 0.6 MB silero VAD + 3 MB sample.wav)
  33 +模型下载: 232 MB(首次启动,hf-mirror.com,10-30 秒)
  34 +```
  35 +
  36 +---
  37 +
  38 +## §二 核心文件(接手只看这些)
  39 +
  40 +```
  41 +app/src/main/kotlin/com/lemon/lifelog/
  42 +├── LifelogApp.kt — Application,建 NotificationChannel
  43 +├── MainActivity.kt — Compose UI,时间轴 LazyColumn + 测试按钮
  44 +├── service/
  45 +│ ├── RecorderService.kt — 前台服务,pipeline 主体 ←★ 改 ASR 流程改这里
  46 +│ └── RecorderState.kt — StateFlow 把服务状态推到 UI
  47 +├── audio/
  48 +│ ├── AudioCapture.kt — AudioRecord wrapper,emits FloatArray frames
  49 +│ └── SegmentWriter.kt — 写 16k mono WAV 到 filesDir(待换 Opus)
  50 +├── asr/
  51 +│ ├── ParaformerEngine.kt — sherpa-onnx paraformer 包装 ←★ 主 ASR
  52 +│ ├── ModelDownloader.kt — 首次启动下模型 + 断点续传
  53 +│ └── XfyunIatClient.kt — 讯飞大模型 WS 客户端,仅 fallback / 云复核用
  54 +└── data/
  55 + ├── Entities.kt — Segment / Transcript / Summary
  56 + ├── Daos.kt — 含 observeTimeline() 给 UI 流
  57 + ├── AppDatabase.kt — Room 1.0
  58 + └── Repository.kt
  59 +```
  60 +
  61 +**sherpa-onnx Kotlin 绑定**:copy 自 [k2-fsa/sherpa-onnx v1.13.2](https://github.com/k2-fsa/sherpa-onnx) 的 `android/SherpaOnnxAar/sherpa_onnx/src/main/java/com/k2fsa/sherpa/onnx/`,整个目录原样塞进 `app/src/main/kotlin/com/k2fsa/sherpa/onnx/`,22 个 .kt 文件,**不要手改**(升级时整目录替换)。
  62 +
  63 +**JNI .so**:`app/src/main/jniLibs/arm64-v8a/`,4 个文件共 34 MB。从 [sherpa-onnx releases](https://github.com/k2-fsa/sherpa-onnx/releases) 下 `sherpa-onnx-v1.13.2-android.tar.bz2`,拷 `jniLibs/arm64-v8a/` 进来。**只装 arm64-v8a**(在 `app/build.gradle.kts` 里 abiFilters 限制了,避免 APK 多出 100 MB)。
  64 +
  65 +---
  66 +
  67 +## §三 改代码 → 装机器(开发内循环)
  68 +
  69 +前置:Mac brew 装 `openjdk@17`(AGP 8.7.3 要 JDK 17)+ Android SDK(`/opt/homebrew/share/android-commandlinetools`)。
  70 +
  71 +```bash
  72 +cd /Users/yibo/Documents/aipcrm/lifelog-recorder
  73 +export JAVA_HOME=/opt/homebrew/opt/openjdk@17
  74 +export PATH=$JAVA_HOME/bin:$PATH
  75 +
  76 +# build (3-10s 增量, 60-90s 冷)
  77 +./gradlew :app:assembleDebug --no-daemon
  78 +
  79 +# install + 授权 + 启动
  80 +adb install -r app/build/outputs/apk/debug/app-debug.apk
  81 +adb shell pm grant com.lemon.lifelog android.permission.RECORD_AUDIO
  82 +adb shell pm grant com.lemon.lifelog android.permission.POST_NOTIFICATIONS
  83 +adb shell am start -n com.lemon.lifelog/.MainActivity
  84 +
  85 +# 看 pipeline 日志
  86 +adb logcat -v time -s RecorderService:V ParaformerEngine:V AudioCapture:V
  87 +
  88 +# 手动启 service(绕开 UI 点按)
  89 +adb shell 'su -c "am start-foreground-service -n com.lemon.lifelog/.service.RecorderService"'
  90 +```
  91 +
  92 +**adb shell + su 走 Magisk root**(Pixel 8 Pro 已 root,见 `~/.claude/projects/-Users-yibo-Documents-aipcrm/memory/` 里的 root 笔记)。
  93 +
  94 +### 跳过模型下载(debug 加速)
  95 +
  96 +模型 232 MB 第一次下要 10-30s。dev 时可以预先 push:
  97 +
  98 +```bash
  99 +# 拷到 app 私有目录
  100 +adb push /tmp/pixel-root/sherpa/models/paraformer-zh.int8.onnx /sdcard/Download/m.onnx
  101 +adb push /tmp/pixel-root/sherpa/models/paraformer-zh-tokens.txt /sdcard/Download/t.txt
  102 +adb shell 'su -c "
  103 + APP_UID=\$(stat -c %u /data/data/com.lemon.lifelog)
  104 + mkdir -p /data/data/com.lemon.lifelog/files/models/paraformer-zh
  105 + install -o \$APP_UID -g \$APP_UID -m 600 /sdcard/Download/m.onnx /data/data/com.lemon.lifelog/files/models/paraformer-zh/model.int8.onnx
  106 + install -o \$APP_UID -g \$APP_UID -m 600 /sdcard/Download/t.txt /data/data/com.lemon.lifelog/files/models/paraformer-zh/tokens.txt
  107 + chown -R \$APP_UID:\$APP_UID /data/data/com.lemon.lifelog/files/models
  108 + rm /sdcard/Download/m.onnx /sdcard/Download/t.txt
  109 +"'
  110 +```
  111 +
  112 +---
  113 +
  114 +## §四 架构链路
  115 +
  116 +```
  117 +RecorderService.onCreate()
  118 + ├─ startForeground(NOTIFICATION_ID, notification, TYPE_MICROPHONE)
  119 + ├─ wakeLock.acquire()
  120 + └─ scope.launch { runPipeline() }
  121 + │
  122 + ├─ ModelDownloader.ensure() — 232 MB 模型在 filesDir 就 skip
  123 + ├─ ParaformerEngine.load(numThreads=4) — 加载 ~1.3s 一次性
  124 + ├─ Vad(silero, 500ms silence, 300ms min, 30s max)
  125 + ├─ capture.start(scope) — AudioRecord 16k mono PCM
  126 + └─ capture.frames.collect { samples ->
  127 + vad.acceptWaveform(samples)
  128 + while (!vad.empty()) {
  129 + val seg = vad.front(); vad.pop()
  130 + handleSegment(seg.samples)
  131 + }
  132 + }
  133 + │
  134 + handleSegment(samples):
  135 + ├─ segWriter.write(samples) → WAV 到 filesDir/segments/YYYY-MM-DD/HH-MM-SS-MS.wav
  136 + ├─ engine.transcribeFloat(samples) → text
  137 + ├─ repo.segments.insert(Segment(..., audioPath, durationMs))
  138 + ├─ repo.transcripts.insert(Transcript(segmentId, text, "paraformer-zh-on-device"))
  139 + └─ RecorderState.onSegment(text, audioMs, wallMs) → UI 实时刷新
  140 +```
  141 +
  142 +VAD 参数(在 `RecorderService.runPipeline` 里调):
  143 +- `threshold=0.5` — Silero 阈值
  144 +- `minSilenceDuration=0.5s` — 500ms 静音才切段
  145 +- `minSpeechDuration=0.3s` — 短于 300ms 的丢
  146 +- `maxSpeechDuration=30s` — 强切上限
  147 +- `windowSize=512` — Silero 输入窗口(32ms)
  148 +
  149 +---
  150 +
  151 +## §五 §六 待办(按优先级)
  152 +
  153 +1. **Opus 编码**(🔥 必须):WAV 在 24/7 跑下 ~2.8 GB/天。换 `MediaCodec OMX.google.opus.encoder` 12 kbps voice mode,缩到 ~130 MB/天。改 `SegmentWriter.kt`。
  154 +2. **自启动 + 电池白名单**(🔥 必须):BOOT_COMPLETED Receiver 拉起 RecorderService;首启弹 REQUEST_IGNORE_BATTERY_OPTIMIZATIONS。否则一重启就死、Doze 一介入就死。
  155 +3. **声纹识别 / speaker diarization**(用户当面提过 3 次):sherpa-onnx 已有 `Speaker.kt` + `SpeakerEmbeddingExtractorConfig.kt`。流程:(a) 「这是我」录音 → 建 anchor embedding 存 SQLite;(b) 每段算 embedding 算 cosine 找最近邻;(c) Transcript 加 `speakerHint` 列。需要下 `sherpa-onnx-3dspeaker-zh.onnx`(~38 MB)。
  156 +4. **FTS5 中文搜索**:Room + sqlite-jsqlite-with-icu。Schema 加 `TranscriptFts` 虚表用 ICU tokenizer。UI 加搜索框 + 高亮命中段。
  157 +5. **LLM 纪要 worker**:WorkManager cron。每小时 / 静音 30min 触发,按时间窗 group segments → POST 到 hermes gateway `http://127.0.0.1:8642/v1/chat/completions`(需要先把 hermes api_server platform 配置起来,见 `~/.hermes/config.yaml`)。
  158 +6. **存储 TTL**:删超过 N 天的 WAV,文字永留。Periodic WorkManager job。
  159 +7. **设置页**:存储用量、模型切换、provider 切换、暂停时段配置(这次定的 MVP 是「一键暂停就够」,但用户后面可能要细化)。
  160 +
  161 +---
  162 +
  163 +## §七 调试陷阱清单(踩过的坑)
  164 +
  165 +| 症状 | 原因 | 修法 |
  166 +|---|---|---|
  167 +| `Java_com_k2fsa_sherpa_onnx_Vad_newFromAsset` SIGSEGV | onnx asset 在 APK 内被 aapt 压缩,native mmap 不开 | 已加 `androidResources.noCompress.add("onnx")`,但保险起见 VAD 还是走 file path(runPipeline 里 vadFile 那段拷贝) |
  168 +| `Load model failed: Protobuf parsing failed` | `github.com/snakers4/silero-vad/raw/master/files/silero_vad.onnx` 直接 GET 返回 HTML(GitHub LFS 拦截) | 改用 `https://github.com/k2-fsa/sherpa-onnx/releases/download/asr-models/silero_vad.onnx` |
  169 +| paraformer t=8 比 t=4 慢 | Tensor G3 是 1×X3 + 4×A715 + 4×A510,跨集群协调开销大 | `numThreads=4` |
  170 +| hf-mirror.com 偶尔 truncate | HF mirror 限速 / 中断 | `ModelDownloader` 已带断点续传 |
  171 +| `am start-foreground-service` 返回 "Requires permission not exported" | 服务 `android:exported="false"`,adb shell 没权 | 用 `su -c "am ..."` 提权 |
  172 +| `adb shell 'su 10288 -c "..."` 网络全断 | Magisk su 没传 supplementary groups | 加 `-G 3003 -G 9997 -G 20<n> -G 50<n>` |
  173 +
  174 +---
  175 +
  176 +## §八 重要外部依赖(断网 / 升级时关注)
  177 +
  178 +| 资源 | URL | 用途 | 替代 |
  179 +|---|---|---|---|
  180 +| sherpa-onnx Android | github.com/k2-fsa/sherpa-onnx/releases/v1.13.2 | jniLibs + Kotlin 绑定 | 自 fork |
  181 +| paraformer-zh model | hf-mirror.com/csukuangfj/sherpa-onnx-paraformer-zh-2023-09-14 | 主 ASR 模型 | huggingface.co 直连 / modelscope |
  182 +| silero_vad.onnx | github.com/k2-fsa/sherpa-onnx/releases/asr-models | VAD 模型(已内嵌) | 自 build silero-vad export |
  183 +| Tsinghua Termux 源 | mirrors.tuna.tsinghua.edu.cn/termux | (Pixel 端 root + Termux 套件用,跟本 app 无关) | bfsu / ustc |
  184 +
  185 +---
  186 +
  187 +## §九 给下一个接手 agent 的 prompt
  188 +
  189 +```
  190 +你接手 lifelog-recorder(Pixel 8 Pro 上的 24/7 录音 + 本地中文 ASR app),
  191 +目录 /Users/yibo/Documents/aipcrm/lifelog-recorder/。
  192 +
  193 +第一步按顺序读:
  194 +1. HANDOFF.md(本文件)—— 完整地图 + 避雷
  195 +2. app/src/main/kotlin/com/lemon/lifelog/service/RecorderService.kt —— pipeline 主体
  196 +3. app/src/main/kotlin/com/lemon/lifelog/asr/ParaformerEngine.kt —— 核心 ASR
  197 +
  198 +环境:
  199 +- macOS + brew openjdk@17 + Android SDK at /opt/homebrew/share/android-commandlinetools
  200 +- Pixel 8 Pro 已 root (Magisk),adb 直连
  201 +- 用户在 ~/.zshrc 有 CLAUDE_CODE_OAUTH_TOKEN
  202 +
  203 +bot 现在跑得起来(v0.1 闭环已通),但还差几件让它能脱离 dev 环境 24/7
  204 +跑的事,按 §五 §六 优先级做。改完任何 .kt 要:
  205 +
  206 + ./gradlew :app:assembleDebug --no-daemon
  207 + adb install -r app/build/outputs/apk/debug/app-debug.apk
  208 + adb logcat -v time -s RecorderService:V ParaformerEngine:V
  209 +
  210 +读完先回我:(a) bot 当前在不在跑(adb shell dumpsys activity services
  211 +com.lemon.lifelog),(b) 你建议先做 §五 里哪 1-2 项,理由。
  212 +```