# lifelog-recorder — HANDOFF > Pixel 8 Pro 上 24/7 录音 + 本地中文 ASR + 时间轴 lifelog 应用。 > 接手前先把下面 §零 看完,免得踩坑。 --- ## §零 不可以的事 / 必读约束 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**)。 2. **不要把模型文件提交到仓库**。`model.int8.onnx` 是 232 MB,首次启动从 hf-mirror 自动下载到 `app.filesDir/models/paraformer-zh/`。app 内只内嵌 silero VAD(629 KB)+ sample.wav(3 MB)。 3. **不要从 assets 加载 onnx**:sherpa-onnx native 代码对 asset-mmap 有兼容问题(v1.13.2 上 silero VAD 直接 SIGSEGV)。已改成「assets 拷到 filesDir 后用 file path 加载」,见 `RecorderService.runPipeline()` 里 vadFile 那段。 4. **Tensor G3 线程甜点 = 4**。实测 paraformer t=4 比 t=8 快(8 时 1×X3 + 4×A715 协调开销大于收益)。改 `ParaformerEngine.load(numThreads=4)` 之前先 bench。 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`。 6. **keys.properties 是 gitignored**。模板在 `keys.properties.example`。讯飞凭证从 [console](https://console.xfyun.cn/services/iat) 申请,写进去就行。 7. `local.properties` 也 gitignored。`sdk.dir=/opt/homebrew/share/android-commandlinetools`(macOS brew 路径,换机改这个)。 --- ## §一 这是什么 24/7 后台录音 → VAD 切段 → 本地 paraformer 中文 ASR → 入库 → 时间轴显示 → (未来)LLM 纪要。 **完全本地,零云端依赖**(讯飞客户端只作云复核 fallback)。 ### 实测性能(Pixel 8 Pro / Tensor G3 / Android 16 BP2A) ``` 模型冷启动: 1.3 s(一次性) VAD 延迟: <1 ms/30ms 帧 ASR: 20-32× realtime(典型段 1-10s → 50-300ms 转写) APK 大小: 62 MB(含 34 MB sherpa-onnx .so + 0.6 MB silero VAD + 3 MB sample.wav) 模型下载: 232 MB(首次启动,hf-mirror.com,10-30 秒) ``` --- ## §二 核心文件(接手只看这些) ``` app/src/main/kotlin/com/lemon/lifelog/ ├── LifelogApp.kt — Application,建 NotificationChannel ├── MainActivity.kt — Compose UI,时间轴 + 搜索 + 声纹 enroll dialog ├── service/ │ ├── RecorderService.kt — 前台服务,pipeline 主体 ←★ 改 ASR 流程改这里 │ ├── RecorderState.kt — StateFlow 把服务状态推到 UI │ └── BootReceiver.kt — BOOT_COMPLETED 自启 (Android 14 有 caveat 见 §七) ├── audio/ │ ├── AudioCapture.kt — AudioRecord wrapper,emits FloatArray frames │ ├── SegmentWriter.kt — Opus encode via MediaCodec + Ogg muxer,~17× 压缩 │ └── SegmentPlayer.kt — MediaPlayer 包装,Opus 段原音播放(系统原生支持) ├── asr/ │ ├── ParaformerEngine.kt — sherpa-onnx paraformer 包装 ←★ 主 ASR │ ├── SpeakerEmbedder.kt — sherpa-onnx 3dspeaker campplus 192-d 嵌入 + cosineSim │ ├── SpeakerMatcher.kt — 回填工具:用当前 profile 集重算所有段的 speakerId │ ├── ModelDownloader.kt — 首次启动下模型(paraformer 232 MB + speaker 28 MB) │ └── XfyunIatClient.kt — 讯飞大模型 WS 客户端,仅 fallback / 云复核用 └── data/ ├── Entities.kt — Segment / Transcript / Summary / SpeakerProfile / TranscriptFts ├── Daos.kt — observeTimeline + searchTimeline (FTS4) + SpeakerDao ├── AppDatabase.kt — Room v2,destructive migration(v0.3 schema 大改) └── Repository.kt ``` **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 文件,**不要手改**(升级时整目录替换)。 **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)。 --- ## §三 改代码 → 装机器(开发内循环) 前置:Mac brew 装 `openjdk@17`(AGP 8.7.3 要 JDK 17)+ Android SDK(`/opt/homebrew/share/android-commandlinetools`)。 ```bash cd /Users/yibo/Documents/aipcrm/lifelog-recorder export JAVA_HOME=/opt/homebrew/opt/openjdk@17 export PATH=$JAVA_HOME/bin:$PATH # build (3-10s 增量, 60-90s 冷) ./gradlew :app:assembleDebug --no-daemon # install + 授权 + 启动 adb install -r app/build/outputs/apk/debug/app-debug.apk adb shell pm grant com.lemon.lifelog android.permission.RECORD_AUDIO adb shell pm grant com.lemon.lifelog android.permission.POST_NOTIFICATIONS adb shell am start -n com.lemon.lifelog/.MainActivity # 看 pipeline 日志 adb logcat -v time -s RecorderService:V ParaformerEngine:V AudioCapture:V # 手动启 service(绕开 UI 点按) adb shell 'su -c "am start-foreground-service -n com.lemon.lifelog/.service.RecorderService"' ``` **adb shell + su 走 Magisk root**(Pixel 8 Pro 已 root,见 `~/.claude/projects/-Users-yibo-Documents-aipcrm/memory/` 里的 root 笔记)。 ### 跳过模型下载(debug 加速) 模型 232 MB 第一次下要 10-30s。dev 时可以预先 push: ```bash # 拷到 app 私有目录 adb push /tmp/pixel-root/sherpa/models/paraformer-zh.int8.onnx /sdcard/Download/m.onnx adb push /tmp/pixel-root/sherpa/models/paraformer-zh-tokens.txt /sdcard/Download/t.txt adb shell 'su -c " APP_UID=\$(stat -c %u /data/data/com.lemon.lifelog) mkdir -p /data/data/com.lemon.lifelog/files/models/paraformer-zh 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 install -o \$APP_UID -g \$APP_UID -m 600 /sdcard/Download/t.txt /data/data/com.lemon.lifelog/files/models/paraformer-zh/tokens.txt chown -R \$APP_UID:\$APP_UID /data/data/com.lemon.lifelog/files/models rm /sdcard/Download/m.onnx /sdcard/Download/t.txt "' ``` --- ## §四 架构链路 ``` RecorderService.onCreate() ├─ startForeground(NOTIFICATION_ID, notification, TYPE_MICROPHONE) ├─ wakeLock.acquire() └─ scope.launch { runPipeline() } │ ├─ ModelDownloader.ensure() — 232 MB 模型在 filesDir 就 skip ├─ ParaformerEngine.load(numThreads=4) — 加载 ~1.3s 一次性 ├─ Vad(silero, 500ms silence, 300ms min, 30s max) ├─ capture.start(scope) — AudioRecord 16k mono PCM └─ capture.frames.collect { samples -> vad.acceptWaveform(samples) while (!vad.empty()) { val seg = vad.front(); vad.pop() handleSegment(seg.samples) } } │ handleSegment(samples): ├─ segWriter.write(samples) → WAV 到 filesDir/segments/YYYY-MM-DD/HH-MM-SS-MS.wav ├─ engine.transcribeFloat(samples) → text ├─ repo.segments.insert(Segment(..., audioPath, durationMs)) ├─ repo.transcripts.insert(Transcript(segmentId, text, "paraformer-zh-on-device")) └─ RecorderState.onSegment(text, audioMs, wallMs) → UI 实时刷新 ``` VAD 参数(在 `RecorderService.runPipeline` 里调): - `threshold=0.5` — Silero 阈值 - `minSilenceDuration=0.5s` — 500ms 静音才切段 - `minSpeechDuration=0.3s` — 短于 300ms 的丢 - `maxSpeechDuration=30s` — 强切上限 - `windowSize=512` — Silero 输入窗口(32ms) --- ## §五 §六 待办(按优先级) ✅ **已完成**: - v0.2 (2026-06):Opus 编码 / BootReceiver / 电池白名单 - v0.3 (2026-06):声纹识别(3dspeaker campplus)/ FTS4 中文搜索 - v0.4 (2026-06):段播放 (MediaPlayer Opus) / bottom sheet 操作菜单 / enroll 后自动回填老段 - v0.5 (2026-06):hermes api_server 端点 + SummaryWorker + 纪要 UI section 剩下: 1. **存储 TTL**:删超过 N 天的 .opus,文字永留。Periodic WorkManager job。 2. **设置页**:存储用量、模型切换、provider 切换、暂停时段配置(v1 定的 MVP 是「一键暂停就够」,但 v2 可能要细化)。 3. **多 anchor 平均**:现在 enroll 是单段单 anchor,可以「连选多段都标 我」自动算平均 embedding 提高匹配率。 4. **回链 lemonPCRM**:Transcript 提到的人名 / 项目 → GraphStore `MENTIONED_IN` 边。 5. ✅ **hermes gateway 自启**(2026-06-04 完成):用 Magisk `service.d` 脚本开机拉起(Termux:Boot 未装,root 方案更稳),见下方「§ 自启(Magisk service.d)」。 ### hermes gateway 设置(v0.5 关键前置) ```bash # 一次性:~/.hermes/.env 末尾追加 API_SERVER_ENABLED=true API_SERVER_HOST=127.0.0.1 API_SERVER_PORT=8642 API_SERVER_KEY= API_SERVER_MODEL_NAME=kimi-for-coding # anthropic SDK:NousResearch hermes-agent 的 .[termux-all] 已自带(v0.15.1 = anthropic 0.87); # 旧版若缺再补:~/.hermes/hermes-agent/venv/bin/pip install 'anthropic>=0.39.0' # 启动:必须用 setsid + disown 才能在 adb shell 之外存活 cat > ~/start_gw.sh <<'EOF' #!/data/data/com.termux/files/usr/bin/bash export PATH=/data/data/com.termux/files/usr/bin:$PATH export HOME=/data/data/com.termux/files/home pkill -f 'hermes.*gateway' 2>/dev/null; sleep 2 set -a; . $HOME/.hermes/.env; set +a : > $HOME/.hermes/logs/gateway.log setsid hermes gateway run $HOME/.hermes/logs/gateway.log 2>&1 & disown $! EOF chmod 755 ~/start_gw.sh bash ~/start_gw.sh ``` lifelog 的 `keys.properties` 里 `hermes.apiUrl`(http://127.0.0.1:8642) / `hermes.apiKey` 要跟 `API_SERVER_KEY` 对得上才能调通。`network_security_config.xml` 已开 127.0.0.1 cleartext 豁免。 > [!IMPORTANT] hermes 的 Kimi provider 配置(v0.5 实测) > hermes-agent(NousResearch)原生支持 Kimi Coding Plan,配成 `model.provider=kimi-coding` + > `model.base_url=https://api.kimi.com/coding` + `model.default=kimi-for-coding`,`.env` 放 > `KIMI_API_KEY=sk-kimi-...`。运行时自动切 Anthropic 协议 + 注入 claude-code UA。 > 完整流程见 `wechat_plain/scripts/local-testkit/docs/HERMES_AGENT_PIXEL_SETUP.md`。 ### ⚠️ 必做:精简 api_server 的 token 开销(2026-06-04 实测) api_server **默认走 hermes 完整 agent**,每次纪要 prompt 注入 tools(37.8KB/17工具) + skills index(9.1KB) = **~12,500 token**,纯属浪费(纪要只要纯文本总结)。只对 api_server 关掉 tools+skills(不影响 CLI/微信 bot): ```yaml # ~/.hermes/config.yaml platform_toolsets: api_server: [] # 关 tools skills: platform_disabled: api_server: [<列出 ~/.hermes/skills 下所有 skill 名>] # 关 skills index ``` 用 hermes 自己的 config 模块改最稳(`load_config`/`save_config`,保留其他配置)。改完重启 gateway。 **实测:prompt 12,500 → 399 token(-97%),纪要正常生成。** ### 自启(Magisk service.d) Termux:Boot 未装(且无匹配签名 APK);已 root,用 Magisk service.d 更稳(不需开 Termux app)。 `/data/adb/service.d/start-hermes-gw.sh`(root, 755):等 `boot_completed` → sleep 25 → 动态推导 Termux uid + su 路径(`/system_ext/bin/su`,**不是** `/system/bin/su`)→ 以 Termux 用户 (带 `-G 3003 -G 9997 -G 20 -G 50`)跑 `~/start_gw.sh`。实测手动执行 `launched rc=0`, gateway 以 `u0_a302` 属主起来、api_server 正常。**冷启动慢 ~3–5 分钟**(初始化时连被墙的 openrouter/nous metadata 超时),对开机服务可接受。 > 验证端到端:`adb shell "su -g 10302 -G 3003 -G 9997 -G 20302 -G 50302 10302 -c '…venv/bin/python ~/test_api.py'"` > 或 lifelog 纪要 tab「现在跑一次」(需最近 1h 有 ≥60s 语音)。 ### 存储预算(v0.2 后) - Opus 16 kbps mono = ~2 KB/s - VAD 后实际有效语音 ≈ 8 h/天 - → ~57 MB/天,~1.7 GB/月 - 转写文字(UTF-8 中文)≈ 30 字/秒 × 8h × 3600 ≈ 不到 1 MB/月 ### Android 14+ BOOT_COMPLETED FG service caveat `RecorderService` 的 `foregroundServiceType="microphone"` 是受限 FG type。从 BOOT_COMPLETED 启动**不保证总是成功**——Android 14+ 的 `ForegroundServiceStartNotAllowedException` 可能扔出来(特别是首次重启、还没用过 app 的情况)。`BootReceiver.onReceive` 已 try/catch 兜底,失败时 log + no-op,用户开 app 手动点开始就能恢复。 实测 Pixel 8 Pro Android 16 BP2A 上**重启后能自动续上**(前提:app 已经至少打开过一次 + RECORD_AUDIO 已授)。 --- ## §七 调试陷阱清单(踩过的坑) | 症状 | 原因 | 修法 | |---|---|---| | `Java_com_k2fsa_sherpa_onnx_Vad_newFromAsset` SIGSEGV | onnx asset 在 APK 内被 aapt 压缩,native mmap 不开 | 已加 `androidResources.noCompress.add("onnx")`,但保险起见 VAD 还是走 file path(runPipeline 里 vadFile 那段拷贝) | | `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` | | paraformer t=8 比 t=4 慢 | Tensor G3 是 1×X3 + 4×A715 + 4×A510,跨集群协调开销大 | `numThreads=4` | | hf-mirror.com 偶尔 truncate | HF mirror 限速 / 中断 | `ModelDownloader` 已带断点续传 | | `am start-foreground-service` 返回 "Requires permission not exported" | 服务 `android:exported="false"`,adb shell 没权 | 用 `su -c "am ..."` 提权 | | `adb shell 'su 10288 -c "..."` 网络全断 | Magisk su 没传 supplementary groups | 加 `-G 3003 -G 9997 -G 20 -G 50` | --- ## §八 重要外部依赖(断网 / 升级时关注) | 资源 | URL | 用途 | 替代 | |---|---|---|---| | sherpa-onnx Android | github.com/k2-fsa/sherpa-onnx/releases/v1.13.2 | jniLibs + Kotlin 绑定 | 自 fork | | paraformer-zh model | hf-mirror.com/csukuangfj/sherpa-onnx-paraformer-zh-2023-09-14 | 主 ASR 模型 | huggingface.co 直连 / modelscope | | silero_vad.onnx | github.com/k2-fsa/sherpa-onnx/releases/asr-models | VAD 模型(已内嵌) | 自 build silero-vad export | | Tsinghua Termux 源 | mirrors.tuna.tsinghua.edu.cn/termux | (Pixel 端 root + Termux 套件用,跟本 app 无关) | bfsu / ustc | --- ## §九 给下一个接手 agent 的 prompt ``` 你接手 lifelog-recorder(Pixel 8 Pro 上的 24/7 录音 + 本地中文 ASR app), 目录 /Users/yibo/Documents/aipcrm/lifelog-recorder/。 第一步按顺序读: 1. HANDOFF.md(本文件)—— 完整地图 + 避雷 2. app/src/main/kotlin/com/lemon/lifelog/service/RecorderService.kt —— pipeline 主体 3. app/src/main/kotlin/com/lemon/lifelog/asr/ParaformerEngine.kt —— 核心 ASR 环境: - macOS + brew openjdk@17 + Android SDK at /opt/homebrew/share/android-commandlinetools - Pixel 8 Pro 已 root (Magisk),adb 直连 - 用户在 ~/.zshrc 有 CLAUDE_CODE_OAUTH_TOKEN bot 现在跑得起来(v0.1 闭环已通),但还差几件让它能脱离 dev 环境 24/7 跑的事,按 §五 §六 优先级做。改完任何 .kt 要: ./gradlew :app:assembleDebug --no-daemon adb install -r app/build/outputs/apk/debug/app-debug.apk adb logcat -v time -s RecorderService:V ParaformerEngine:V 读完先回我:(a) bot 当前在不在跑(adb shell dumpsys activity services com.lemon.lifelog),(b) 你建议先做 §五 里哪 1-2 项,理由。 ```