earshot-cli 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,25 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ jobs:
9
+ quality:
10
+ runs-on: ubuntu-latest
11
+ steps:
12
+ - uses: actions/checkout@v4
13
+ - uses: astral-sh/setup-uv@v5
14
+ with:
15
+ python-version: "3.12"
16
+ - name: Install
17
+ run: uv sync
18
+ - name: Lint
19
+ run: |
20
+ uv run ruff check .
21
+ uv run ruff format --check .
22
+ - name: Type check
23
+ run: uv run mypy
24
+ - name: Test
25
+ run: uv run pytest
@@ -0,0 +1,26 @@
1
+ name: Publish to PyPI
2
+
3
+ # Trusted Publishing(OIDC):PyPI 侧注册 pending publisher 后免长期 token。
4
+ # 注册路径:pypi.org/manage/account/publishing → owner=helloworldtang
5
+ # repo=earshot workflow=publish.yml environment=pypi project=earshot-cli
6
+ on:
7
+ release:
8
+ types: [published]
9
+ workflow_dispatch: {}
10
+
11
+ permissions:
12
+ id-token: write # OIDC
13
+
14
+ jobs:
15
+ publish:
16
+ runs-on: ubuntu-latest
17
+ environment: pypi
18
+ steps:
19
+ - uses: actions/checkout@v4
20
+ - uses: astral-sh/setup-uv@v5
21
+ with:
22
+ python-version: "3.12"
23
+ - name: Build
24
+ run: uv build
25
+ - name: Publish
26
+ run: uv publish
@@ -0,0 +1,17 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ .venv/
5
+ .pytest_cache/
6
+ .mypy_cache/
7
+ .ruff_cache/
8
+ dist/
9
+ build/
10
+ *.egg-info/
11
+
12
+ # 模型与录音产物(不进仓)
13
+ models/
14
+ *.wav
15
+ *.m4a
16
+ *.mp3
17
+ *.aac
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Jackie
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,140 @@
1
+ Metadata-Version: 2.5
2
+ Name: earshot-cli
3
+ Version: 0.1.0
4
+ Summary: 旁听剪藏:跨平台桌面端系统声录制 → 本地转写 → 投递知识库(默认 ExoMind,可插拔)
5
+ Project-URL: Homepage, https://github.com/helloworldtang/earshot
6
+ Project-URL: Repository, https://github.com/helloworldtang/earshot
7
+ Project-URL: Issues, https://github.com/helloworldtang/earshot/issues
8
+ Author: Jackie
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: asr,bilibili,cli,knowledge-base,sensevoice,subtitle,transcription
12
+ Classifier: Environment :: Console
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Natural Language :: Chinese (Simplified)
16
+ Classifier: Operating System :: MacOS
17
+ Classifier: Operating System :: Microsoft :: Windows
18
+ Classifier: Operating System :: POSIX :: Linux
19
+ Classifier: Programming Language :: Python :: 3
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Topic :: Multimedia :: Sound/Audio :: Speech
22
+ Classifier: Topic :: Utilities
23
+ Requires-Python: >=3.12
24
+ Requires-Dist: httpx>=0.27
25
+ Requires-Dist: imageio-ffmpeg>=0.5
26
+ Requires-Dist: numpy>=1.26
27
+ Requires-Dist: onnxruntime<1.29,>=1.28.0
28
+ Requires-Dist: sherpa-onnx>=1.13.8
29
+ Requires-Dist: soundcard>=0.4.3
30
+ Requires-Dist: soxr>=0.5
31
+ Requires-Dist: yt-dlp>=2025.1
32
+ Description-Content-Type: text/markdown
33
+
34
+ # earshot
35
+
36
+ 旁听剪藏:**听到的,都能进知识库。**
37
+
38
+ 听 B站/视频号/播客/讲座时开着它,听完 `Ctrl-C`,录音自动转写为 Markdown 并投递到你的知识库(默认 [ExoMind 知识飞轮](https://exomind.ai-as.cn),可插拔任意 webhook)。纯端侧转写,无限流、零 API 费用。
39
+
40
+ ```
41
+ 系统声音 ──录制──> 16k wav ──silero VAD 切段──> SenseVoice 端侧识别(实时字幕) ──> Markdown ──> 投递(exomind / webhook / 仅存档)
42
+ 视频链接 ──yt-dlp──> 平台字幕(ai-zh/CC) ────────────────────────────┘(高信噪比路线)
43
+ ```
44
+
45
+ ## 安装(开箱即用)
46
+
47
+ ```bash
48
+ uv tool install earshot-cli # 或 pipx install earshot-cli;安装后命令名是 earshot
49
+ earshot doctor # 环境自检
50
+ ```
51
+
52
+ 源码安装请**锁版本 tag**,不要引滚动的 `main`(消费方 `uv sync`/`pip install` 会隐性升级到未审阅代码;升级应当是显式改版本号的决策):
53
+
54
+ ```bash
55
+ uv tool install "earshot-cli @ https://github.com/helloworldtang/earshot/archive/refs/tags/v0.1.0.tar.gz"
56
+ ```
57
+
58
+ - **零系统依赖**:ffmpeg 已随 Python wheel 内置([imageio-ffmpeg](https://pypi.org/project/imageio-ffmpeg),MoviePy 同款),无需 brew/apt。
59
+ - **模型首次运行自动下载**:SenseVoice int8 + silero VAD(~230MB,默认走 hf-mirror,`HF_ENDPOINT` 可覆盖),装完不再下。
60
+ - **投递免装 CLI**:默认直连 ExoMind HTTP API(复用 `~/.exomind/config.json` 凭据);没有凭据但有 exomind CLI 时自动回退;也支持任意 webhook。
61
+
62
+ ### 平台采集矩阵
63
+
64
+ | 平台 | 系统声采集 | 额外步骤 |
65
+ |---|---|---|
66
+ | Windows | WASAPI loopback(原生) | 无 |
67
+ | Linux | PulseAudio/PipeWire `.monitor` | 无 |
68
+ | macOS | 无原生环回 | 见下方「macOS 三级台阶」,零配置即可用 |
69
+
70
+ #### macOS 三级台阶(按门槛从低到高)
71
+
72
+ 1. **零配置·录屏路线**:听 Mac 上的内容时 `Cmd+Shift+5` 录屏(自带系统声),
73
+ 听完 `earshot audio 录屏.mp4` —— 不装任何东西,今天就能用。
74
+ 2. **零配置·麦录兜底**:直接 `earshot listen`,未配置环回时自动退回内置麦克风
75
+ 外放收音(豆包同款,音质一般但可用)。
76
+ 3. **一次性设置·内录(音质无损)**:`brew install blackhole-2ch`,在「音频 MIDI 设置」
77
+ 新建**多输出设备**(BlackHole 2ch + 扬声器)并设为系统输出 —— 边听边录,终身受用。
78
+ 装好后 earshot 自动选中,无需 `--device`。
79
+
80
+ > macOS 下默认逻辑 = 取「当前默认扬声器的同名输入设备」;把多输出设备设为默认输出即可被自动选中。也可 `--device` 指定。
81
+
82
+ ## 使用
83
+
84
+ ```bash
85
+ # 旁听模式:开录 → 听(实时字幕滚动) → Ctrl-C 完成 → 预览确认 → 投递
86
+ earshot listen
87
+ earshot listen --note "B站·xxx第3期"
88
+
89
+ # B站等视频链接直拉字幕(高信噪比:无环境噪音/打断;AI 字幕需登录态)
90
+ earshot video "https://www.bilibili.com/video/BVxxxx" --cookies-from-browser chrome
91
+
92
+ # 已有音视频文件(录屏 mp4 / 语音 m4a / 播客 mp3)
93
+ earshot audio recording.mp4 --title "通勤听书笔记" --deliver none
94
+
95
+ # 补投/重投 outbox 里的笔记(如环境问题当时没投出去)
96
+ earshot deliver ~/.earshot/outbox/20261030-xxxx.md
97
+
98
+ # 列出采集设备 / 环境自检 / 清理过期留档
99
+ earshot devices
100
+ earshot doctor
101
+ earshot clean --days 30
102
+ ```
103
+
104
+ 常用参数:`--title`(默认取首句截断40字)、`--tag`(可多次)、`--note`(来源备注写入脚注)、
105
+ `--engine server`(改走云端 dashscope 转写,复用 `~/.exomind/config.json` 凭据)、
106
+ `--deliver webhook --webhook <url>`(投递到任意知识库,POST JSON `{origin,title,content,tags}`)、
107
+ `--deliver none`(只落 `~/.earshot/outbox/` 存档)。
108
+
109
+ 笔记留档:`~/.earshot/outbox/YYYYMMDD-HHMMSS-<标题>.md`。
110
+
111
+ ### 文件生命周期
112
+
113
+ | 位置 | 内容 | 生命周期 |
114
+ |---|---|---|
115
+ | `~/.earshot/tmp/*.wav` | 录音中间产物 | 转写投递成功后**自动删除**(失败保留供排查) |
116
+ | `~/.earshot/outbox/*.md` | 转写笔记留档 | **永久保留**(审计/重投兜底),`earshot clean --days 30` 手动清理 |
117
+
118
+ 投递前会先在终端**预览识别内容**,回车投递、`q` 放弃(笔记仍留档);`--yes` 跳过确认。等待服务端回执时 Ctrl-C = 不再等待(内容已提交,job 在服务端继续)。
119
+
120
+ ## 设计要点
121
+
122
+ - **声音是最后的通用接口**:不依赖任何平台的开放性(B站字幕 API、视频号加密……都不碰),换平台零成本。
123
+ - **端侧优先**:SenseVoice-small int8 纯 CPU 转写,1 小时音频约 1-2 分钟,无"服务用户多请稍后"。
124
+ - **投递可插拔**:exomind CLI(复用其异步 job、轮询、client_key 幂等)/ webhook / 仅存档。
125
+ - **平台差异只允许存在于 `capture.py`**:换采集方案不动上层。
126
+
127
+ ## 开发
128
+
129
+ ```bash
130
+ uv sync # 含 dev 组(ruff/mypy/pytest)
131
+ uv run ruff check . && uv run ruff format --check .
132
+ uv run mypy
133
+ uv run pytest
134
+ ```
135
+
136
+ 测试不依赖模型与声卡;`tests/test_extract.py` 需要本机 ffmpeg(无则自动跳过)。
137
+
138
+ ## License
139
+
140
+ MIT
@@ -0,0 +1,107 @@
1
+ # earshot
2
+
3
+ 旁听剪藏:**听到的,都能进知识库。**
4
+
5
+ 听 B站/视频号/播客/讲座时开着它,听完 `Ctrl-C`,录音自动转写为 Markdown 并投递到你的知识库(默认 [ExoMind 知识飞轮](https://exomind.ai-as.cn),可插拔任意 webhook)。纯端侧转写,无限流、零 API 费用。
6
+
7
+ ```
8
+ 系统声音 ──录制──> 16k wav ──silero VAD 切段──> SenseVoice 端侧识别(实时字幕) ──> Markdown ──> 投递(exomind / webhook / 仅存档)
9
+ 视频链接 ──yt-dlp──> 平台字幕(ai-zh/CC) ────────────────────────────┘(高信噪比路线)
10
+ ```
11
+
12
+ ## 安装(开箱即用)
13
+
14
+ ```bash
15
+ uv tool install earshot-cli # 或 pipx install earshot-cli;安装后命令名是 earshot
16
+ earshot doctor # 环境自检
17
+ ```
18
+
19
+ 源码安装请**锁版本 tag**,不要引滚动的 `main`(消费方 `uv sync`/`pip install` 会隐性升级到未审阅代码;升级应当是显式改版本号的决策):
20
+
21
+ ```bash
22
+ uv tool install "earshot-cli @ https://github.com/helloworldtang/earshot/archive/refs/tags/v0.1.0.tar.gz"
23
+ ```
24
+
25
+ - **零系统依赖**:ffmpeg 已随 Python wheel 内置([imageio-ffmpeg](https://pypi.org/project/imageio-ffmpeg),MoviePy 同款),无需 brew/apt。
26
+ - **模型首次运行自动下载**:SenseVoice int8 + silero VAD(~230MB,默认走 hf-mirror,`HF_ENDPOINT` 可覆盖),装完不再下。
27
+ - **投递免装 CLI**:默认直连 ExoMind HTTP API(复用 `~/.exomind/config.json` 凭据);没有凭据但有 exomind CLI 时自动回退;也支持任意 webhook。
28
+
29
+ ### 平台采集矩阵
30
+
31
+ | 平台 | 系统声采集 | 额外步骤 |
32
+ |---|---|---|
33
+ | Windows | WASAPI loopback(原生) | 无 |
34
+ | Linux | PulseAudio/PipeWire `.monitor` | 无 |
35
+ | macOS | 无原生环回 | 见下方「macOS 三级台阶」,零配置即可用 |
36
+
37
+ #### macOS 三级台阶(按门槛从低到高)
38
+
39
+ 1. **零配置·录屏路线**:听 Mac 上的内容时 `Cmd+Shift+5` 录屏(自带系统声),
40
+ 听完 `earshot audio 录屏.mp4` —— 不装任何东西,今天就能用。
41
+ 2. **零配置·麦录兜底**:直接 `earshot listen`,未配置环回时自动退回内置麦克风
42
+ 外放收音(豆包同款,音质一般但可用)。
43
+ 3. **一次性设置·内录(音质无损)**:`brew install blackhole-2ch`,在「音频 MIDI 设置」
44
+ 新建**多输出设备**(BlackHole 2ch + 扬声器)并设为系统输出 —— 边听边录,终身受用。
45
+ 装好后 earshot 自动选中,无需 `--device`。
46
+
47
+ > macOS 下默认逻辑 = 取「当前默认扬声器的同名输入设备」;把多输出设备设为默认输出即可被自动选中。也可 `--device` 指定。
48
+
49
+ ## 使用
50
+
51
+ ```bash
52
+ # 旁听模式:开录 → 听(实时字幕滚动) → Ctrl-C 完成 → 预览确认 → 投递
53
+ earshot listen
54
+ earshot listen --note "B站·xxx第3期"
55
+
56
+ # B站等视频链接直拉字幕(高信噪比:无环境噪音/打断;AI 字幕需登录态)
57
+ earshot video "https://www.bilibili.com/video/BVxxxx" --cookies-from-browser chrome
58
+
59
+ # 已有音视频文件(录屏 mp4 / 语音 m4a / 播客 mp3)
60
+ earshot audio recording.mp4 --title "通勤听书笔记" --deliver none
61
+
62
+ # 补投/重投 outbox 里的笔记(如环境问题当时没投出去)
63
+ earshot deliver ~/.earshot/outbox/20261030-xxxx.md
64
+
65
+ # 列出采集设备 / 环境自检 / 清理过期留档
66
+ earshot devices
67
+ earshot doctor
68
+ earshot clean --days 30
69
+ ```
70
+
71
+ 常用参数:`--title`(默认取首句截断40字)、`--tag`(可多次)、`--note`(来源备注写入脚注)、
72
+ `--engine server`(改走云端 dashscope 转写,复用 `~/.exomind/config.json` 凭据)、
73
+ `--deliver webhook --webhook <url>`(投递到任意知识库,POST JSON `{origin,title,content,tags}`)、
74
+ `--deliver none`(只落 `~/.earshot/outbox/` 存档)。
75
+
76
+ 笔记留档:`~/.earshot/outbox/YYYYMMDD-HHMMSS-<标题>.md`。
77
+
78
+ ### 文件生命周期
79
+
80
+ | 位置 | 内容 | 生命周期 |
81
+ |---|---|---|
82
+ | `~/.earshot/tmp/*.wav` | 录音中间产物 | 转写投递成功后**自动删除**(失败保留供排查) |
83
+ | `~/.earshot/outbox/*.md` | 转写笔记留档 | **永久保留**(审计/重投兜底),`earshot clean --days 30` 手动清理 |
84
+
85
+ 投递前会先在终端**预览识别内容**,回车投递、`q` 放弃(笔记仍留档);`--yes` 跳过确认。等待服务端回执时 Ctrl-C = 不再等待(内容已提交,job 在服务端继续)。
86
+
87
+ ## 设计要点
88
+
89
+ - **声音是最后的通用接口**:不依赖任何平台的开放性(B站字幕 API、视频号加密……都不碰),换平台零成本。
90
+ - **端侧优先**:SenseVoice-small int8 纯 CPU 转写,1 小时音频约 1-2 分钟,无"服务用户多请稍后"。
91
+ - **投递可插拔**:exomind CLI(复用其异步 job、轮询、client_key 幂等)/ webhook / 仅存档。
92
+ - **平台差异只允许存在于 `capture.py`**:换采集方案不动上层。
93
+
94
+ ## 开发
95
+
96
+ ```bash
97
+ uv sync # 含 dev 组(ruff/mypy/pytest)
98
+ uv run ruff check . && uv run ruff format --check .
99
+ uv run mypy
100
+ uv run pytest
101
+ ```
102
+
103
+ 测试不依赖模型与声卡;`tests/test_extract.py` 需要本机 ffmpeg(无则自动跳过)。
104
+
105
+ ## License
106
+
107
+ MIT
@@ -0,0 +1,84 @@
1
+ [project]
2
+ name = "earshot-cli"
3
+ version = "0.1.0"
4
+ description = "旁听剪藏:跨平台桌面端系统声录制 → 本地转写 → 投递知识库(默认 ExoMind,可插拔)"
5
+ readme = "README.md"
6
+ license = "MIT"
7
+ authors = [{ name = "Jackie" }]
8
+ keywords = ["transcription", "asr", "sensevoice", "bilibili", "subtitle", "knowledge-base", "cli"]
9
+ classifiers = [
10
+ "Environment :: Console",
11
+ "Intended Audience :: Developers",
12
+ "License :: OSI Approved :: MIT License",
13
+ "Natural Language :: Chinese (Simplified)",
14
+ "Operating System :: MacOS",
15
+ "Operating System :: Microsoft :: Windows",
16
+ "Operating System :: POSIX :: Linux",
17
+ "Programming Language :: Python :: 3",
18
+ "Programming Language :: Python :: 3.12",
19
+ "Topic :: Multimedia :: Sound/Audio :: Speech",
20
+ "Topic :: Utilities",
21
+ ]
22
+ requires-python = ">=3.12"
23
+ dependencies = [
24
+ "numpy>=1.26",
25
+ "sherpa-onnx>=1.13.8",
26
+ # 上界钉死:sherpa-onnx 的 Linux wheel 链接 onnxruntime VERS_1.28.2 符号版本,
27
+ # 1.29+ 会在 import 时报 version `VERS_1.28.2' not found(macOS 不受影响,同钉为一致)
28
+ "onnxruntime>=1.28.0,<1.29",
29
+ "soundcard>=0.4.3",
30
+ "httpx>=0.27",
31
+ # 平台 wheel 内置 ffmpeg 二进制(~60MB):系统没装 ffmpeg 也能跑(MoviePy 同款做法)
32
+ "imageio-ffmpeg>=0.5",
33
+ # 实时字幕:录音流 48k→16k 在线重采样(流式,不能等录完再重采样)
34
+ "soxr>=0.5",
35
+ # video 命令:提取B站等平台的元信息与字幕轨(纯 Python,自包含)
36
+ "yt-dlp>=2025.1",
37
+ ]
38
+
39
+ [project.urls]
40
+ Homepage = "https://github.com/helloworldtang/earshot"
41
+ Repository = "https://github.com/helloworldtang/earshot"
42
+ Issues = "https://github.com/helloworldtang/earshot/issues"
43
+
44
+ [project.scripts]
45
+ earshot = "earshot.cli:main"
46
+
47
+ [dependency-groups]
48
+ dev = [
49
+ "mypy>=1.11",
50
+ "pytest>=8.3",
51
+ "ruff>=0.7",
52
+ ]
53
+
54
+ [build-system]
55
+ requires = ["hatchling"]
56
+ build-backend = "hatchling.build"
57
+
58
+ [tool.hatch.build.targets.wheel]
59
+ packages = ["src/earshot"]
60
+
61
+ [tool.ruff]
62
+ line-length = 120
63
+ target-version = "py312"
64
+ src = ["src", "tests"]
65
+
66
+ [tool.ruff.lint]
67
+ select = ["E", "W", "F", "I", "B", "C4", "UP", "SIM"]
68
+
69
+ [tool.mypy]
70
+ python_version = "3.12"
71
+ strict = true
72
+ files = ["src", "tests"]
73
+ # sherpa-onnx / soundcard 未随 wheel 提供 py.typed —— 与其假装,不如显式忽略
74
+ [[tool.mypy.overrides]]
75
+ module = ["sherpa_onnx.*", "soundcard.*", "onnxruntime.*", "imageio_ffmpeg.*", "soxr.*", "yt_dlp.*"]
76
+ ignore_missing_imports = true
77
+
78
+ [[tool.mypy.overrides]]
79
+ module = ["tests.*"]
80
+ disallow_untyped_defs = false
81
+
82
+ [tool.pytest.ini_options]
83
+ testpaths = ["tests"]
84
+ addopts = "-q"
@@ -0,0 +1,37 @@
1
+ #!/usr/bin/env bash
2
+ # 拉取 earshot 本地转写模型(int8 优先,不进仓)。
3
+ #
4
+ # 产物布局(earshot 默认读 ~/.earshot/models,可用 EARSHOT_MODEL_ROOT 覆盖):
5
+ # models/asr/model.int8.onnx SenseVoice-small int8(zh/en/ja/ko/yue,含标点+ITN)
6
+ # models/asr/tokens.txt
7
+ # models/vad/silero_vad.onnx
8
+ #
9
+ # 源:HF(默认走 hf-mirror,本机实测 GitHub release 直连不通)。
10
+ # 用法: ./scripts/download_models.sh [目标目录,默认 ~/.earshot/models]
11
+ # 可 HF_ENDPOINT=https://huggingface.co 覆盖
12
+ set -euo pipefail
13
+
14
+ HF="${HF_ENDPOINT:-https://hf-mirror.com}"
15
+ ROOT="${1:-$HOME/.earshot/models}"
16
+
17
+ ASR_REPO="csukuangfj/sherpa-onnx-sense-voice-zh-en-ja-ko-yue-2024-07-17"
18
+ VAD_REPO="onnx-community/silero-vad"
19
+
20
+ fetch() { # fetch <repo> <remote-file> <dest-relpath>
21
+ local repo="$1" remote="$2" dest="$ROOT/$3"
22
+ mkdir -p "$(dirname "$dest")"
23
+ if [ -f "$dest" ]; then echo "skip $3 (已存在)"; return; fi
24
+ echo "get $3"
25
+ curl -L --fail --retry 3 -o "$dest.part" "$HF/$repo/resolve/main/$remote"
26
+ mv "$dest.part" "$dest"
27
+ }
28
+
29
+ # ASR:SenseVoice int8(model ~230MB + tokens)
30
+ fetch "$ASR_REPO" model.int8.onnx asr/model.int8.onnx
31
+ fetch "$ASR_REPO" tokens.txt asr/tokens.txt
32
+
33
+ # VAD:silero
34
+ fetch "$VAD_REPO" onnx/model.onnx vad/silero_vad.onnx
35
+
36
+ echo "done: $ROOT"
37
+ du -sh "$ROOT"/* 2>/dev/null || true
@@ -0,0 +1,7 @@
1
+ """earshot:旁听剪藏 —— 听到的都能进知识库。
2
+
3
+ 跨平台桌面端:系统声环回录制(macOS 需 BlackHole)→ 端侧 VAD+ASR 转写
4
+ → 组装 Markdown → 投递(默认 exomind CLI,可插拔 webhook)。
5
+ """
6
+
7
+ __version__ = "0.1.0"
@@ -0,0 +1,148 @@
1
+ """系统声环回采集(soundcard):平台差异只允许存在于此模块。
2
+
3
+ - Windows: WASAPI loopback,免驱
4
+ - Linux: PulseAudio ``.monitor``,免驱
5
+ - macOS: 无原生环回,需安装 BlackHole 并(建议用多输出设备)设为输出
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import platform
11
+ import warnings
12
+ import wave
13
+ from collections.abc import Callable
14
+ from pathlib import Path
15
+ from typing import Any
16
+
17
+ import numpy as np
18
+
19
+ # soundcard 在 macOS import 时必发一条"不支持环回"警告,属已知事实(BlackHole 方案的前提),
20
+ # 压掉以免吓到用户 —— 必须在 import soundcard 之前过滤
21
+ warnings.filterwarnings("ignore", message=".*loopback recording functionality.*", category=Warning)
22
+ import soundcard as sc # noqa: E402
23
+
24
+ SAMPLE_RATE = 48000 # 环回设备主流采样率;重采样交给 ffmpeg
25
+ BLOCK_FRAMES = 4800 # 100ms/块,兼顾电平刷新频率与 CPU
26
+ BLACKHOLE_HINT = "blackhole"
27
+
28
+
29
+ def list_capture_devices() -> list[tuple[str, bool]]:
30
+ """列出全部输入设备。
31
+
32
+ Returns:
33
+ ``(设备名, 是否环回)`` 列表。
34
+ """
35
+ return [(m.name, bool(m.isloopback)) for m in sc.all_microphones(include_loopback=True)]
36
+
37
+
38
+ def blackhole_missing_hint() -> str | None:
39
+ """macOS 上未发现 BlackHole 设备时的安装指引,其他平台返回 None。"""
40
+ if platform.system() != "Darwin":
41
+ return None
42
+ names = " ".join(name for name, _ in list_capture_devices()).lower()
43
+ if BLACKHOLE_HINT in names:
44
+ return None
45
+ return (
46
+ "未检测到 BlackHole。安装: brew install blackhole-2ch;"
47
+ "再在「音频 MIDI 设置」建多输出设备(BlackHole+扬声器)并设为系统输出,"
48
+ "即可边听边录。"
49
+ )
50
+
51
+
52
+ def pick_loopback(hint: str | None = None) -> Any:
53
+ """选采集设备:默认取当前默认扬声器的同名环回/输入设备。
54
+
55
+ Args:
56
+ hint: 设备名子串,大小写不敏感;None 时走默认逻辑。
57
+
58
+ Returns:
59
+ soundcard 的 Mic 对象。
60
+
61
+ Raises:
62
+ LookupError: 找不到匹配设备。
63
+ """
64
+ mics = sc.all_microphones(include_loopback=True)
65
+ if hint:
66
+ low = hint.lower()
67
+ for m in mics:
68
+ if low in m.name.lower():
69
+ return m
70
+ msg = f"未找到匹配「{hint}」的输入设备(earshot devices 查看)"
71
+ raise LookupError(msg)
72
+ speaker = sc.default_speaker()
73
+ for m in mics:
74
+ if m.name == speaker.name: # macOS:BlackHole 输出即同名输入;Win/Linux:同名环回
75
+ return m
76
+ msg = f"默认扬声器「{speaker.name}」没有可采集的环回设备。{blackhole_missing_hint() or ''}"
77
+ raise LookupError(msg)
78
+
79
+
80
+ def pick_device(hint: str | None = None) -> tuple[Any, str]:
81
+ """选采集设备,带麦录兜底(零门槛优先于零音质损失)。
82
+
83
+ 优先级:hint 匹配 > 默认扬声器环回 > 任意环回 > 默认麦克风(外放收音,豆包同款)。
84
+
85
+ Args:
86
+ hint: 设备名子串;None 走默认回退链。
87
+
88
+ Returns:
89
+ ``(mic, mode_label)``,mode_label 用于向用户说明当前采集方式。
90
+
91
+ Raises:
92
+ LookupError: 全链条找不到可用设备。
93
+ """
94
+ try:
95
+ return pick_loopback(hint), "环回内录(音质无损)"
96
+ except LookupError:
97
+ if hint:
98
+ raise # 显式指定的设备找不到,不擅自换
99
+ for m in sc.all_microphones(include_loopback=True):
100
+ if m.isloopback:
101
+ return m, f"环回内录(音质无损,设备:{m.name})"
102
+ mic = sc.default_microphone()
103
+ if mic is None:
104
+ msg = "未找到任何可用输入设备(earshot devices 查看)"
105
+ raise LookupError(msg)
106
+ fallback = f"麦克风收音(外放收音,豆包同款;配 BlackHole 可内录)设备:{mic.name}"
107
+ return mic, fallback
108
+
109
+
110
+ def record_to_wav(
111
+ mic: Any,
112
+ out_wav: Path,
113
+ on_level: Callable[[float], None] | None = None,
114
+ on_audio: Callable[[np.ndarray], None] | None = None,
115
+ ) -> float:
116
+ """录制到 wav(pcm16/48k/mono),Ctrl-C 停止。
117
+
118
+ Args:
119
+ mic: pick_device 返回的设备。
120
+ out_wav: 输出路径。
121
+ on_level: 可选电平回调(rms,0~1 区间参考),用于终端电平条。
122
+ on_audio: 可选音频块回调(float32 mono),实时转写经此喂入。
123
+
124
+ Returns:
125
+ 实际录制时长(秒)。
126
+ """
127
+ total_frames = 0
128
+ with wave.open(str(out_wav), "wb") as w:
129
+ w.setnchannels(1)
130
+ w.setsampwidth(2)
131
+ w.setframerate(SAMPLE_RATE)
132
+ try:
133
+ with mic.recorder(samplerate=SAMPLE_RATE) as rec:
134
+ while True:
135
+ data = rec.record(numframes=BLOCK_FRAMES)
136
+ mono = data.mean(axis=1) if data.ndim > 1 else np.asarray(data, dtype=np.float32)
137
+ if mono.size == 0:
138
+ continue
139
+ if on_level is not None:
140
+ on_level(float(np.sqrt(np.mean(np.square(mono)))))
141
+ if on_audio is not None:
142
+ on_audio(mono)
143
+ pcm16 = (np.clip(mono, -1.0, 1.0) * 32767.0).astype("<i2")
144
+ w.writeframes(pcm16.tobytes())
145
+ total_frames += mono.size
146
+ except KeyboardInterrupt:
147
+ pass # Ctrl-C = "点完成"
148
+ return total_frames / SAMPLE_RATE