# livelog **Repository Path**: remedo/livelog ## Basic Information - **Project Name**: livelog - **Description**: No description available - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-07-27 - **Last Updated**: 2026-07-28 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # livelog — 24 小时语音转写与查询 一个 Android 应用:24 小时不间断采集周围语音,按 VAD 或固定时长切片为音频文件,上传到局域网内由 mDNS 自动发现的 PC 转写服务,再把带时间戳的逐句文本与说话人聚类写入本地 SQLite,对外提供**只读 HTTP 查询接口**;原始音频在转写成功后**立即删除**,仅保留每句对应的 WAV 切片用于回放。 手机端不保存任何云端 ASR / LLM 凭据,所有转写与说话人 embedding 都在 PC 上跑。 ## 架构概览 - **录音**:前台服务 `RecordingService` 根据当前能否 ping 通 PC 端点,在两种模式间切换: - **在线 + PC 可达**:`VadRecorder` 以 16 kHz 单声道 PCM 持续采样,按 RMS 能量做 VAD,仅对有效语音段输出 WAV,由 `ClipProcessor` 内联转写,原始 chunk 转写成功即删,只保留每句的 WAV 切片。 - **离线**:`WavFileWriter` 连续录 PCM,按 `offlineClipMinutes`(默认 30,可调 5–240 分钟)滚动落盘为完整 WAV,登记为 `PENDING` 的 `audio_clip` 行,并由 `UploadClipWorker`(约束 UNMETERED_WIFI + BATTERY_NOT_LOW)排程上传。离线时静音段也保留,因为错过就补不回来。 - **PC 发现**:`NsdDiscovery` 封装 `NsdManager`,发现 PC 通过 zeroconf 广播的 `_livelog._tcp.`,把解析出的 (host, port) 写入 `TranscriptionConfig` 的 volatile 字段;录音循环据此实时切换在线 / 离线。可在设置里手填 `pcManualHost` / `pcManualPort` 覆盖自动发现。 - **转写**:`PcTranscribeClient` 以 multipart 把 WAV POST 到 `http://:/transcribe`(`language=zh`),解析 WhisperX 风格响应:每段含 `start`/`end`/`text` 以及一个 256 维 `embedding`。 - **说话人**:`ClipProcessor` 对每段 embedding 调 `TranscriptStore.assignSpeaker` 做增量余弦聚类——与已有质心比对,相似度 ≥ `clusterThresholdPermille / 1000`(默认 0.70,可调 0.30–0.999)则并入并更新质心,否则新建聚类,标签 `SPK_NN`;用户可在 `speaker_cluster` 表里覆盖显示名。 - **持久化**:`TranscriptStore`(SQLiteOpenHelper,schema v2)四张表: - `utterance`:每句一行,含绝对墙钟 `start_ms/end_ms`、`speaker_id`、`speaker`、`text`、`clip_id`、clip 内 `clip_offset_ms/clip_len_ms`、可回放的 `slice_path`。 - `audio_clip`:离线录音队列,状态 `pending`/`uploading`/`transcribed`/`failed`,带重试计数与错误信息。 - `speaker_cluster`:跨文件说话人聚类,存质心 embedding blob 与可选用户标签。 - `meta`:进程级键值,目前记录 `last_pc_contact_ms`。 - `WavSlicer` 为每句切出独立 WAV 片段供查询接口回放。 - **音频清理**:原始 clip 在 `ClipProcessor.process` 的 `finally` 里 `file.delete()`,转写成功、失败或文件缺失都会删,不保留原始录音。 - **查询接口**:`QueryServer`(内嵌 NanoHTTPD,默认端口 `8765`)对外只读 JSON: - `GET /health` - `GET /query?q=<关键词>&start=&end=`(全部可选) - `GET /byTime?start=&end=` - `GET /byContent?q=<关键词>` - `GET /audio?id=`(流式播放该句的 WAV 切片,支持 HTTP Range) - 时间均按设备时区的 ISO-8601,例如 `2026-07-18T09:00:00`。 - **UI**:Jetpack Compose 四个 Tab——录音(起停)、记录(最近条目)、查询(按内容 / 时间)、设置(PC 端点覆盖、离线片段时长、聚类阈值、VAD 阈值、开机自启)。 ## 数据流示意 ``` AudioRecord (16kHz PCM) | +-- 在线+PC --> VadRecorder --> vad_chunk WAV --+--> PcTranscribeClient POST /transcribe | | (PC: WhisperX ASR + speaker embedding) +-- 离线 --> WavFileWriter --> clip_*.wav | | | | v +-> audio_clip PENDING ----->+ segments (text + 256-d embedding) (UploadClipWorker) | | v | TranscriptStore.assignSpeaker (cosine cluster) | + WavSlicer (per-utt WAV slice) v v +-----------> SQLite: utterance / audio_clip / speaker_cluster / meta | +---> QueryServer HTTP :8765 (JSON + /audio) +---> Compose UI (4 tabs) ``` 原始 `clip_*.wav` / `vad_chunk` 在转写完成后即被 `file.delete()` 删除。 ## 构建 依赖:JDK 17、Android SDK(compileSdk 34)、Gradle 8.9+。首次先建 wrapper: ```bash gradle wrapper --gradle-version 8.9 # 或用 Android Studio 打开项目,它会自动生成 wrapper 并下载 SDK。 ``` 配置 PC 端点(二选一): 1. **mDNS 自动发现(默认,推荐)**:不写任何配置,PC 端广播 `_livelog._tcp.` 即可。 2. **手动覆盖**:复制 `app/secrets.properties.example` 为 `app/secrets.properties`(已 gitignore),填 `pcManualHost` / `pcManualPort`;或直接在 App 内「设置」Tab 填写,持久化在 DataStore,优先级高于构建期默认。 构建: ```bash ./gradlew assembleDebug # 产物:app/build/outputs/apk/debug/app-debug.apk ``` ## 运行权限 首次启动会请求 `RECORD_AUDIO`、`INTERNET`、`POST_NOTIFICATIONS`(以及 `ACCESS_WIFI_STATE` / `CHANGE_WIFI_MULTICAST_STATE` 用于 mDNS)。前台服务携带 `microphone` 类型;开机自启由 `BootReceiver` 处理,默认开启,可在设置中关闭。 ## 语音活动检测 (VAD) **设计**:手机端用 RMS 能量做轻量 VAD,跳过长静音段,把成本留在真正有语音的部分。ASR/说话人模型仍在 PC 上跑。 **参数**:采样 16 kHz 单声道 PCM,~32 ms(512 样点)一帧算 RMS;静音帧用 EMA(系数 0.03)跟踪噪声底;门限 = max(噪声底×3, 300);连续 3 帧判为有声进入 speaking;连续静音满 `vadSilenceMs` 或说话满 `vadMaxSpeechMs` 结束本段;短于 `vadMinSpeechMs` 的段丢弃。 | 参数 | 默认 | 说明 | ------|--------|------| | `vadEnabled` | true | 是否启用 VAD;关闭则离线模式按固定时长切片 | | `vadSilenceMs` | 1500 | 句尾静音多久结束本段 | | `vadMinSpeechMs` | 800 | 短于此长度的段视为噪声丢弃 | | `vadMaxSpeechMs` | 600_000 (10 分钟) | 单段最长,避免超长独白 | **注意**:VAD 只在「在线 + PC 可达」时启作用。离线模式下走 `WavFileWriter` 定时滚动,不做端点检测,因为离线时段错过就补不回来。 ## 备选:本地路由 查询接口监听 `http://<手机IP>:8765/`。若需在 PC 上访问,确保同一局域网并放行端口;模拟器可用 `adb forward tcp:8765 tcp:8765`。 ## 测试 `./gradlew test` 跑 JVM 单元测试(JUnit 4,测试方法使用反引号句子命名,如 `` `escapes percent and underscore` ``)。当前覆盖查询时间窗、LIKE 转义与聚类/切片的纯逻辑;新增纯逻辑优先补单元测试,涉及 Android 框架的再考虑 instrumented 测试。 ``` ```