lifelog-recorder — HANDOFF
Pixel 8 Pro 上 24/7 录音 + 本地中文 ASR + 时间轴 lifelog 应用。 接手前先把下面 §零 看完,免得踩坑。
§零 不可以的事 / 必读约束
- 不要把 Whisper 当默认 STT。Tensor G3 上 whisper-small 实测 RTF 1.76(0.57× realtime),跑不动。已确定走 sherpa-onnx + paraformer-zh int8,Tensor G3 实测 RTF 0.04(25× realtime)。
- 不要把模型文件提交到仓库。
model.int8.onnx是 232 MB,首次启动从 hf-mirror 自动下载到app.filesDir/models/paraformer-zh/。app 内只内嵌 silero VAD(629 KB)+ sample.wav(3 MB)。 - 不要从 assets 加载 onnx:sherpa-onnx native 代码对 asset-mmap 有兼容问题(v1.13.2 上 silero VAD 直接 SIGSEGV)。已改成「assets 拷到 filesDir 后用 file path 加载」,见
RecorderService.runPipeline()里 vadFile 那段。 - Tensor G3 线程甜点 = 4。实测 paraformer t=4 比 t=8 快(8 时 1×X3 + 4×A715 协调开销大于收益)。改
ParaformerEngine.load(numThreads=4)之前先 bench。 -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。- keys.properties 是 gitignored。模板在
keys.properties.example。讯飞凭证从 console 申请,写进去就行。 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 的 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 下 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)。
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:
# 拷到 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 后自动回填老段
剩下:
- 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)。 - 存储 TTL:删超过 N 天的 .opus,文字永留。Periodic WorkManager job。
- 设置页:存储用量、模型切换、provider 切换、暂停时段配置(v1 定的 MVP 是「一键暂停就够」,但 v2 可能要细化)。
- 多 anchor 平均:现在 enroll 是单段单 anchor,可以「连选多段都标 我」自动算平均 embedding 提高匹配率。
- 回链 lemonPCRM:Transcript 提到的人名 / 项目 → GraphStore
MENTIONED_IN边。
存储预算(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<n> -G 50<n> |
§八 重要外部依赖(断网 / 升级时关注)
| 资源 | 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 项,理由。