Commit f6ea102833863940533286f2d51374d41c2aaa20
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 | +``` |