# simul-subtitle **Repository Path**: yang_tao_code/simul-subtitle ## Basic Information - **Project Name**: simul-subtitle - **Description**: No description available - **Primary Language**: Unknown - **License**: MIT - **Default Branch**: main - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-11-13 - **Last Updated**: 2026-11-13 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 同传字幕 · 离线中文实时字幕(定制版 v2.4.0) > 把**周围环境的声音**或**手机输出的音频**实时变成中文字幕。**识别与纠错全程在手机本地完成,不翻译、不上传**;只有「下载模型」这一步会联网一次,之后完全离线。 > 基于 [KorenKrita/simul-subtitle](https://github.com/KorenKrita/simul-subtitle)(MIT)定制。 > 本仓库: **当前版本**:`v2.4.0`(versionCode `205`) · **安装包**:[`dist/simul-subtitle-v2.4.0.apk`](dist/simul-subtitle-v2.4.0.apk) · 约 11 MB(APK 不含任何模型) --- ## 0. 一句话说明这个版本最重要的变化 **v2.4.0 起,模型不再打包进 APK。** 安装包里只有识别引擎(约 11 MB),模型在 App 内点「下载」按钮**一键获取**(仅这一步联网,国内镜像自动切换),也保留「手动导入」给已有模型文件的用户;下载/导入一次后一直离线可用。这样既把安装包从 240 MB 降到 11 MB,又不用自己传文件,换模型也不用重装 App,还能同时存多套随时切换。 > 如果你拿到的是 v2.2.0 及更早的 APK,那些版本把 229 MB 模型塞在安装包里,属于旧方案,见 `archive/`(本地留存,未提交到仓库)。 --- ## 1. 这个 App 做什么 只做一件事:**中文声音 → 中文字幕**,两条输入路径。 | 输入源 | 采集方式 | 典型场景 | 手机静音时 | |---|---|---|---| | **周围环境的声音** | 麦克风 `VOICE_COMMUNICATION` | 身边人说话、会议现场、外放音箱 | 无关 | | **手机输出的声音** | 系统播放音内录 `AudioPlaybackCapture` + `MediaProjection` | 视频、直播、语音消息、网课 | **仍可出字幕** | ### 1.1 「手机输出的声音」到底是什么(重要) 它指的是**手机正在播放的音频数据流**,不是麦克风发出来的声音,也不是「用麦克风去听扬声器」。 - 实现是 Android 10+ 的 `AudioPlaybackCaptureConfiguration`,只捕获 `USAGE_MEDIA` / `USAGE_GAME` / `USAGE_UNKNOWN` 三类播放音; - 捕获的是系统混音的**数字音频流**,与扬声器和铃声音量无关,所以**铃声静音、勿扰模式、插耳机、调成振动都不影响**; - 麦克风在这一路里**完全不打开**;麦克风只用于另一路「周围环境的声音」; - 开始时会弹出「屏幕共享」确认,这是 Android 对播放音内录的强制授权,**App 不录屏、不截屏、不读画面、不上传任何内容**。 ### 1.2 静音时为什么还能采集,以及做了哪些兜底 - 铃声音量 / 勿扰 / 插耳机:不影响,内录取的是数字流。 - **媒体音量为 0**:部分厂商机型在这种状态下只给捕获方全 0 的静音数据。v2.2.0 起加了「静音补偿」——开始字幕时若 `STREAM_MUSIC` 在最低档,临时调到最小非零一档,结束字幕自动恢复;若你中途自己调过音量,则不会覆盖。开关在 `设置 → 识别 → 手机输出的声音(内录)`。 - **连续 8 秒没采到任何音频能量**:字幕窗口会显示「没检测到手机输出的音频」并给出排查提示;恢复出声后自动收起。这样「一个 App 禁止被内录」和「根本没在播放」能被区分出来,而不是一直空白。 --- ## 2. 核心能力 | 能力 | 说明 | |---|---| | 离线识别 | 仅「下载模型」会联网一次;识别、纠错、字幕渲染全程离线,声音与文字都不出设备 | | 中文识别 | SenseVoice int8 离线模型,`language="zh"` 固定中文,开启 ITN(自动标点、数字规范化) | | 本地纠错 | 内置默认纠错词表(含大量医疗同音词),可编辑、可一键「恢复默认 / 下载更新」;改完从下一句生效 | | 模型外置 | App 内**一键下载**(国内镜像自动切换 + SHA-256 校验)或手动导入;多套共存、点选切换、删除;APK 不含任何 `.onnx` | | 悬浮字幕 | 可拖动、可改大小、草稿灰色 → 定稿白色、字号/配色/背景透明度可调、可锁定 | | 两路采集 | 麦克风 / 系统播放音内录,通知栏明确标出当前在听哪一路 | | 过载保护 | 30 秒停止采集、60 秒排空、VAD 分句、字幕板保留历史 | | 记录(可选) | 默认关闭;打开后仅在本机留文字记录,可手动分享 | 明确**不做**:翻译、目标语言选择、云端 LLM / Jev、术语生成、应用内更新、文件转写。仅「下载模型」会联网一次,其余全离线。 --- ## 3. 快速开始(直接用 APK) ### 3.1 安装 ```bash adb install -r dist/simul-subtitle-v2.4.0.apk ``` - 最低 Android 10(API 29),目标 API 35,仅 `arm64-v8a`。 - 使用本地 `debug.keystore` 签名。**从上游官方版升级需先卸载官方版**(签名不同);从本定制版 v2.x 升级可直接覆盖(同签名、versionCode 递增)。 - 首次启动首页会显示「还差模型」,这是正常的。 ### 3.2 在 App 内一键下载模型(推荐,仅这一步联网) 打开 App → 右上齿轮 → **识别** → **模型管理**,页面里有「模型下载(一键下载,仅这一步联网)」分组,按顺序点两个「下载」按钮即可: 1. **SenseVoice 中文识别(int8 量化)** → 点「下载(约 228 MB)」:自动从国内镜像(hf-mirror)拉取 `model.int8.onnx` + `tokens.txt`,下载完做 SHA-256 校验,再安装到应用私有目录并设为当前模型。 2. **Silero VAD 语音活动检测** → 点「下载(约 1 MB)」:从本仓库 Gitee raw 拉取 `silero_vad.onnx` 安装。 > 两个都要下:识别模型负责把声音转成文字,VAD 负责判断哪一段有说话声、用来分句。任一文件所有镜像都失败才会报错,校验失败会自动换镜像重试。下载过程请停留在页面;完成后页面显示「✅ 已就绪」,回首页显示「可以开始了」。**只有这一步会联网**,之后识别、纠错、字幕渲染全部在手机本地,声音与文字都不上传。 下载到的文件与 SHA-256(与 App 内校验基准一致): | 文件 | 用途 | 大小 | SHA-256 | |---|---|---|---| | `model.int8.onnx` | 中文 SenseVoice 识别模型 | 239,233,841 B | `c71f0ce00bec95b07744e116345e33d8cbbe08cef896382cf907bf4b51a2cd51` | | `tokens.txt` | 词表 | 315,894 B | — | | `silero_vad.onnx` | 语音活动检测(VAD) | 643,854 B | `9e2449e1087496d8d4caba907f23e0bd3f78d91fa552479bb9c23ac09cbb1fd6` | ### 3.3 手动导入(高级,给已有模型文件的用户) 如果你电脑上已经下载好了模型文件,也可以用「手动导入(高级)」分组,跳过 App 内下载: 1. 把准备好的 3 个文件传到手机(数据线、`adb push`、微信/QQ 传文件、局域网均可)。例如: ```bash adb push models/asr/model.int8.onnx /sdcard/Download/ adb push models/asr/tokens.txt /sdcard/Download/ adb push models/vad/silero_vad.onnx /sdcard/Download/ ``` 2. 打开 App → 右上齿轮 → **识别** → **模型管理** → 「手动导入(高级)」: - 点「**导入识别模型(.onnx + tokens.txt)**」→ 在文件选择器里**一次同时选中两个文件**(长按可多选)→ 等待复制完成(约 230 MB,几十秒,过程中请停留在页面); - 点「**导入 VAD 模型(silero_vad.onnx)**」→ 选中 `silero_vad.onnx`。 3. 页面显示「✅ 已就绪」,回首页显示「可以开始了」。 > 在哪里自己拿到模型文件?见 [`docs/model-guide.md`](docs/model-guide.md)(国内镜像 `setup.cn.sh`、备用源与手工校验)。导入原理:通过系统文件选择器(SAF)拿到 `content://`,复制到应用私有目录 `filesDir/models/`,之后 sherpa-onnx 按绝对路径加载;卸载 App 时模型副本一并清除,手机里的原文件不受影响。 ### 3.4 授权并开始 | 权限 | 用途 | 何时申请 | |---|---|---| | `RECORD_AUDIO` | 麦克风采集;系统播放音内录也要求此权限 | 首次开始 | | `SYSTEM_ALERT_WINDOW` | 悬浮字幕窗 | 首次开始,跳系统设置开启 | | 屏幕共享授权 | Android 对播放音内录的强制要求 | 选「手机输出的声音」并开始 | | `POST_NOTIFICATIONS` | 前台服务通知(Android 13+) | 首次开始 | | `MODIFY_AUDIO_SETTINGS` | 静音补偿时临时微调媒体音量 | 安装即授予,无弹窗 | 然后:首页选输入源 → 点「开始字幕」→ 回到视频/直播即可看到悬浮字幕。点一下字幕出现工具栏,可暂停、调样式、停止。 ### 3.5 切换 / 删除模型 `设置 → 识别 → 模型管理` 里列出已导入的所有识别模型,`●` 表示当前使用,点名称即可切换,右侧「删除」只删 App 内的副本。**切换后要停止字幕再重新开始才生效**(运行中的会话持有已加载的模型)。 --- ## 4. 架构与数据流 ```mermaid flowchart TB subgraph IN["采集层(Android)"] MIC["麦克风 VOICE_COMMUNICATION
周围环境的声音"] PB["AudioPlaybackCapture + MediaProjection
手机输出的音频"] end subgraph CORE["core · 纯 JVM Kotlin"] VAD["Silero VAD
逐帧语音概率"] SEG["Segmenter
分句 / 强制切分"] PIPE["Pipeline
ASR 单线程 + 定稿链路"] BOARD["CaptionBoard
历史 + 当前草稿"] end subgraph UI["表现层(Android)"] OVER["CaptionOverlayView
悬浮字幕窗"] end MIC --> VAD PB --> VAD VAD --> SEG --> PIPE --> BOARD --> OVER ``` > Gitee 若未渲染 Mermaid,可直接打开离线图集 [`docs/analysis/diagrams.html`](docs/analysis/diagrams.html)(内置 `mermaid.min.js`,双击即可),或看 [`docs/analysis/diagrams.md`](docs/analysis/diagrams.md)。 链路细节:`AudioSource` 统一输出 16 kHz / 单声道 / float → Silero VAD → Segmenter 分句 → Pipeline 内的 `SherpaAsr` 整句解码 + `LocalCorrector` 本地纠错 → CaptionBoard → 悬浮窗。`core` 模块不依赖 Android SDK;Android / Desktop 只实现平台差异(采集与渲染)。 ### 关键文件索引 | 文件 | 职责 | |---|---| | `app/.../ModelManager.kt` | **v2.3.0 新增**:模型导入、列表、切换、删除、就绪校验 | | `app/.../Engines.kt` | `SherpaAsr`(SenseVoice 文件路径加载)、`SileroProb`、`LocalCorrector` | | `app/.../AudioSources.kt` | 麦克风采集、`AudioRecord` 统一包装、每帧 RMS 能量回调 | | `app/.../CaptionService.kt` | 前台服务、播放音内录、静音补偿、无音频诊断、加载模型 | | `app/.../MainActivity.kt` | 首页、输入源选择、模型管理 UI、文件选择器、权限 | | `app/.../Prefs.kt` | 设置持久化(含 `activeModelId`、`corrections`、`muteCompensation`) | | `app/.../CaptionOverlayView.kt` | 悬浮字幕窗渲染与手势 | | `core/.../CaptionSession.kt` | 会话编排、状态行、过载保护 | | `core/.../Pipeline.kt` | ASR 结果定稿链路(保留 `Mt` 接口做原文透传) | | `core/.../OfflineMt.kt` | 纯 ASR 原文透传,无任何网络请求 | | `setup.cn.sh` | 国内镜像下载 AAR + 模型(不写入 APK) | --- ## 5. 从源码构建 ### 5.1 环境 | 项 | 版本 | |---|---| | JDK | 17 | | Android SDK | platform `android-35`,build-tools `34.0.0` | | Gradle | 8.11.1(wrapper 指向官方源,境内不可达时改用本地解压版) | | AGP | 8.7.3 | | Kotlin | 项目 `build.gradle.kts` 指定 | | NDK | **不需要**(AAR 自带 `libsherpa-onnx-jni.so`) | ### 5.2 步骤 ```bash git clone https://gitee.com/yang_tao_code/simul-subtitle.git cd simul-subtitle # 1) 下载构建依赖:sherpa-onnx AAR + 模型(都用国内镜像、带 SHA-256 校验) bash setup.cn.sh # 2) 指定本机 SDK(或设 ANDROID_HOME) echo "sdk.dir=D:/path/to/android-sdk" > local.properties # 3) 构建 Release export JAVA_HOME=/path/to/jdk-17 ./gradlew :app:assembleRelease # 产物:app/build/outputs/apk/release/app-release.apk ``` `settings.gradle.kts` 已把 Maven / Plugin 仓库改为阿里云镜像优先: ```kotlin maven("https://maven.aliyun.com/repository/google") maven("https://maven.aliyun.com/repository/public") maven("https://maven.aliyun.com/repository/gradle-plugin") ``` > 注意域名是 `maven.aliyun.com/repository/*`,不是 `mirrors.aliyun.com/repository/*`(后者 404)。 ### 5.3 两个已踩过的坑 1. **中文路径会让 AGP 失败**:AGP 8.7.3 拒绝在含非 ASCII 字符的项目路径下构建;`android.overridePathCheck=true` 只能绕过第一层,`aapt2` / R8 等 native 工具仍可能受编码影响。实测可靠的做法是把源码复制到**纯 ASCII 路径**(如 `D:/workbuddy_data/v2build/simul-subtitle`)再构建,APK 复制回交付目录。本机所有版本都是这样出的包。 2. **Gradle 官方发行版境内不可达**:`services.gradle.org` 直连超时;实测华为云 `mirrors.huaweicloud.com/gradle/gradle-8.11.1-bin.zip` 约 2.25 MB/s,腾讯云约 1.48 MB/s,TUNA 对应路径 404。下载后用本地 `gradle` 命令加 `-p <项目路径>` 构建。 ### 5.4 校验 ```bash aapt dump badging app/build/outputs/apk/release/app-release.apk | head -9 apksigner verify --verbose app/build/outputs/apk/release/app-release.apk python -c "import zipfile; z=zipfile.ZipFile('...apk'); print([n for n in z.namelist() if n.endswith('.onnx')])" # 期望:versionCode=205 / versionName=2.4.0 / 含 INTERNET 权限(仅下载模型用)/ onnx 列表为空 ``` 签名沿用本机 `~/.android/debug.keystore`。**不同机器生成的 debug key 不同**,因此换机器构建的包不能覆盖安装旧包;若要长期可升级,请把签名证书固定下来并妥善保管。 --- ## 6. 版本历史 | 版本 | versionCode | 主要变化 | APK 体积 | |---|---|---|---| | v2.0 | 200 | 离线可启动(去掉 BYOK 硬守卫)、新增麦克风 / 文件转写入口、移除应用内更新 | 89,694,677 B | | v2.0.1 | 201 | **移除整个翻译能力**,收敛为两路实时采集;删除 `INTERNET` / 明文流量 / 状态灯 | 89,369,577 B | | v2.1.0 | 202 | 换中文 SenseVoice int8 离线模型、固定 `language="zh"`、ITN;云端 Jev 改为纯本地 `LocalCorrector` | 251,650,787 B | | v2.2.0 | 203 | 明确「手机输出的声音 = 系统播放音内录」;静音补偿 + 8 秒无音频诊断;文案消歧 | 251,654,539 B | | **v2.3.0** | **204** | **模型外置**:App 内选择导入、多模型切换;APK 移除全部 `.onnx` | **11,656,929 B** | | **v2.4.0** | **205** | **App 内一键下载模型**(替代手动导入文件);内置默认纠错词表(开源混淆集 + 医疗同音词)并支持「下载更新」;加 `INTERNET` 仅用于下载模型 | ~11 MB(APK 不含 `.onnx`) | 详见 [`CHANGELOG.md`](CHANGELOG.md)。版本规划与问题复盘见 [`docs/v2-roadmap.md`](docs/v2-roadmap.md)。 --- ## 7. 已知限制 - **未做真机 / 模拟器运行验证**。模型加载路径、AAR 的 `newFromFile()` 分支、文件选择器导入流程都做了静态核对与编译校验,但中文实际字准率、静音补偿在各厂商机型上的表现仍需实机确认。 - **播放方 App 可以禁止被内录**(`ALLOW_CAPTURE_BY_NONE`,常见于有版权保护的视频、DRM、部分银行应用)。这是 Android 系统限制,普通应用无法绕过;此时字幕窗口会提示「没检测到手机输出的音频」。 - 通话语音、闹钟、通知音等不属于 `MEDIA / GAME / UNKNOWN` 用途,不在可内录范围内;同时有其他 App 在放声音时,它的声音也可能被一起识别。 - 部分 OEM / 勿扰策略会拒绝 `setStreamVolume`,静音补偿会降级为提示而非强行改音量。 - **模型导入期间请停留在页面**:复制 230 MB 需要时间,Activity 被销毁可能导致导入中断(失败会清理临时文件,不会留下坏模型)。 - 切换模型、修改纠错词表、改显示方式对运行中会话的生效时机不同:词表/显示从下一句生效,模型需停止后重新开始。 - 只做中文,不翻译、不联网;只支持 `arm64-v8a`;最低 Android 10。 - `core` 里仍保留 `Mt` / `ByokService` 等上游类型名,但 Android 运行链路**恒定使用 `OfflineMt` 原文透传**,不构造任何网络请求。这是为了复用 VAD → 分句 → 草稿/定稿 → 字幕板机制而保留的适配层,详见 [`docs/upstream/`](docs/upstream/) 中的上游文档。 --- ## 8. 隐私与权限 - **仅在「下载模型」时联网**:清单新增 `INTERNET` 权限,但**只用于 App 内下载模型这一步**;下载完、识别、纠错、字幕渲染全程不联网,音频与文字不出设备。其余权限:`RECORD_AUDIO`、`SYSTEM_ALERT_WINDOW`、`FOREGROUND_SERVICE(_MEDIA_PROJECTION/_MICROPHONE)`、`POST_NOTIFICATIONS`、`MODIFY_AUDIO_SETTINGS`。 - 识别、纠错、字幕渲染全部在设备本地;音频原始数据不写盘、不出设备。 - 「记录每次使用」默认关闭;打开后仅在应用私有外部目录写**文字**记录(不含音频),只有你主动分享才会离开手机。 - 模型文件导入后存放在应用私有目录,其他 App 无法读取;卸载即清除。 --- ## 9. 目录结构 ``` simul-subtitle/ ├── README.md ← 本文件(定制版总说明) ├── README.upstream.md ← 上游原 README(溯源) ├── README.upstream.en.md ├── CHANGELOG.md ← 定制版版本历史 ├── LICENSE ← 上游 MIT ├── setup.cn.sh ← 国内镜像下载 AAR + 模型(不写 APK) ├── setup.sh ← 上游原脚本 ├── app/ ← Android 端(本定制版主战场) ├── core/ ← 纯 JVM Kotlin 核心 ├── desktop/ ← 上游桌面端(未纳入本定制范围) ├── docs/ │ ├── model-guide.md ← 模型获取、校验、导入专题 │ ├── v2-roadmap.md ← v2 开发方向与问题复盘 │ ├── analysis/ ← 5 张 Mermaid 图 + 离线 HTML 图集 + 工具调研 │ └── upstream/ ← 上游历史文档(日英翻译版,与本版行为不同) ├── dist/ ← 交付 APK(仅当前版) ├── vendor/ ← 自托管的模型文件(如 silero_vad.onnx),供 App 内「下载」走 Gitee raw ├── corrections/ ← 默认纠错词表(开源混淆集 + 医疗同音词),App 内「下载更新」也取这里 └── models/ ← 本地下载的模型(.gitignore 忽略,不提交、不进 APK) ``` `archive/`(历史大体积 APK)保留在本地工作区,未提交到仓库。 --- ## 10. FAQ **Q:装完点「开始字幕」提示「先下载模型」?** A:正常。v2.4.0 起安装包不含模型,按第 3 节在「模型管理」里点两个「下载」按钮即可,只需一次(仅这一步联网)。 **Q:模型去哪下载?会不会要翻墙?** A:推荐**在 App 内直接点「下载」按钮**(「设置 → 识别 → 模型管理 → 模型下载」),自动走国内镜像(hf-mirror)+ SHA-256 校验,最省事;下载这一步会联网一次,下完就完全离线。若想自己准备模型文件,也可 `bash setup.cn.sh` 走国内镜像下载到电脑再导入,备用链接与手工下载见 [`docs/model-guide.md`](docs/model-guide.md)。 **Q:能换别的识别模型吗?** A:只要是 sherpa-onnx `OfflineSenseVoiceModelConfig` 能加载的 SenseVoice 系 `.onnx` + 对应 `tokens.txt` 即可,App 里可同时存多套并点选切换。解码语言固定 `zh`、开启 ITN,因此中文场景最合适。Paraformer / Transducer 等非 SenseVoice 模型需要改 `Engines.kt` 的配置分支。 **Q:为什么需要「屏幕共享」授权,会不会录屏?** A:这是 Android 对 `AudioPlaybackCapture` 的强制要求。App 只用 `MediaProjection` 拿音频捕获能力,**不创建 `ImageReader`、不读屏幕像素、不录屏、不上传**。 **Q:手机静音还能出字幕吗?** A:能。内录取的是数字音频流,铃声音量/勿扰/插耳机都不影响;媒体音量为 0 时会自动临时调到最低一档并在结束时恢复。唯一例外是播放方 App 主动禁止内录,这属于系统限制。 **Q:能覆盖安装旧版本吗?** A:本定制版 v2.x 之间同签名、versionCode 递增,可直接覆盖。上游官方版签名不同,需先卸载。换机器重新构建的包签名也可能不同。 --- ## 11. 致谢与许可 - 上游项目:[KorenKrita/simul-subtitle](https://github.com/KorenKrita/simul-subtitle),MIT,见 [`LICENSE`](LICENSE)。 - 推理引擎:[sherpa-onnx 1.13.8](https://github.com/k2-fsa/sherpa-onnx)(AAR 未提交,由 `setup.cn.sh` 下载)。 - ASR 模型:SenseVoice `sherpa-onnx-sense-voice-zh-en-ja-ko-yue-int8-2024-07-17`;VAD 模型:Silero VAD。模型各自的许可证随模型包分发(`models/asr/LICENSE`、`models/vad/LICENSE`),**模型文件不在本仓库内**。 - 上游历史文档位于 `docs/upstream/`,描述的是日英翻译 + 云端 LLM 的 v1.x 行为,**与本定制版不一致**,请以本 README 为准。