asr-polish 0.9.2__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.
- asr_polish-0.9.2/LICENSE +21 -0
- asr_polish-0.9.2/NOTICE +9 -0
- asr_polish-0.9.2/PKG-INFO +599 -0
- asr_polish-0.9.2/README.md +568 -0
- asr_polish-0.9.2/asr_polish/__init__.py +65 -0
- asr_polish-0.9.2/asr_polish/__main__.py +5 -0
- asr_polish-0.9.2/asr_polish/_atomic.py +50 -0
- asr_polish-0.9.2/asr_polish/bili.py +1145 -0
- asr_polish-0.9.2/asr_polish/cli.py +726 -0
- asr_polish-0.9.2/asr_polish/correction.py +290 -0
- asr_polish-0.9.2/asr_polish/crosscheck.py +191 -0
- asr_polish-0.9.2/asr_polish/data/corrections.json +102 -0
- asr_polish-0.9.2/asr_polish/export.py +176 -0
- asr_polish-0.9.2/asr_polish/lexicon.py +276 -0
- asr_polish-0.9.2/asr_polish/lexicon_io.py +251 -0
- asr_polish-0.9.2/asr_polish/py.typed +0 -0
- asr_polish-0.9.2/asr_polish/quality.py +205 -0
- asr_polish-0.9.2/asr_polish/server/__init__.py +54 -0
- asr_polish-0.9.2/asr_polish/server/app.py +416 -0
- asr_polish-0.9.2/asr_polish/server/jobs.py +371 -0
- asr_polish-0.9.2/asr_polish/server/store.py +516 -0
- asr_polish-0.9.2/asr_polish/server/worker.py +169 -0
- asr_polish-0.9.2/asr_polish/throttle.py +128 -0
- asr_polish-0.9.2/asr_polish.egg-info/PKG-INFO +599 -0
- asr_polish-0.9.2/asr_polish.egg-info/SOURCES.txt +39 -0
- asr_polish-0.9.2/asr_polish.egg-info/dependency_links.txt +1 -0
- asr_polish-0.9.2/asr_polish.egg-info/entry_points.txt +2 -0
- asr_polish-0.9.2/asr_polish.egg-info/requires.txt +7 -0
- asr_polish-0.9.2/asr_polish.egg-info/top_level.txt +1 -0
- asr_polish-0.9.2/pyproject.toml +48 -0
- asr_polish-0.9.2/setup.cfg +4 -0
- asr_polish-0.9.2/tests/test_basic.py +443 -0
- asr_polish-0.9.2/tests/test_ci_guard.py +186 -0
- asr_polish-0.9.2/tests/test_cli.py +88 -0
- asr_polish-0.9.2/tests/test_cookie_import.py +219 -0
- asr_polish-0.9.2/tests/test_export.py +180 -0
- asr_polish-0.9.2/tests/test_jobs_outcome.py +231 -0
- asr_polish-0.9.2/tests/test_lexicon_io.py +200 -0
- asr_polish-0.9.2/tests/test_risk.py +475 -0
- asr_polish-0.9.2/tests/test_server.py +643 -0
- asr_polish-0.9.2/tests/test_throttle.py +136 -0
asr_polish-0.9.2/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 angus-hu-gzy
|
|
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.
|
asr_polish-0.9.2/NOTICE
ADDED
|
@@ -0,0 +1,599 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: asr-polish
|
|
3
|
+
Version: 0.9.2
|
|
4
|
+
Summary: B站字幕批量抓取与本地任务队列:单视频 / UP主 / 合集 / 搜索批量导出 txt/srt/json/md,附带中文 ASR 术语订正与质量自评,零第三方依赖
|
|
5
|
+
Author: angus-hu-gzy
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/angus-hu-gzy/asr-polish
|
|
8
|
+
Project-URL: Repository, https://github.com/angus-hu-gzy/asr-polish
|
|
9
|
+
Project-URL: Changelog, https://github.com/angus-hu-gzy/asr-polish/blob/main/CHANGELOG.md
|
|
10
|
+
Project-URL: Issues, https://github.com/angus-hu-gzy/asr-polish/issues
|
|
11
|
+
Keywords: asr,speech-to-text,whisper,术语订正,转写,中文,nlp,bilibili,字幕,subtitle
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Operating System :: OS Independent
|
|
19
|
+
Classifier: Topic :: Text Processing :: Linguistic
|
|
20
|
+
Classifier: Typing :: Typed
|
|
21
|
+
Requires-Python: >=3.9
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
License-File: LICENSE
|
|
24
|
+
License-File: NOTICE
|
|
25
|
+
Provides-Extra: bilibili
|
|
26
|
+
Requires-Dist: yt-dlp; extra == "bilibili"
|
|
27
|
+
Provides-Extra: dev
|
|
28
|
+
Requires-Dist: ruff>=0.6; extra == "dev"
|
|
29
|
+
Requires-Dist: build>=1.0; extra == "dev"
|
|
30
|
+
Dynamic: license-file
|
|
31
|
+
|
|
32
|
+
# asr-polish
|
|
33
|
+
|
|
34
|
+
[](https://github.com/angus-hu-gzy/asr-polish/actions/workflows/ci.yml)
|
|
35
|
+
[](https://www.python.org/)
|
|
36
|
+
[](./LICENSE)
|
|
37
|
+
[]()
|
|
38
|
+
[](https://github.com/angus-hu-gzy/asr-polish/releases)
|
|
39
|
+
[](https://github.com/angus-hu-gzy)
|
|
40
|
+
|
|
41
|
+
**把 B站字幕批量取下来,并把抓取做成能排队的本地服务**;取到的文本顺手订正到可信。
|
|
42
|
+
纯 Python 标准库实现、**零第三方依赖**;仅「无字幕时走必剪云端 ASR」需要可选依赖 `yt-dlp`。
|
|
43
|
+
|
|
44
|
+
## 它解决什么
|
|
45
|
+
|
|
46
|
+
两件事,一前一后。
|
|
47
|
+
|
|
48
|
+
**一、字幕要能批量拿下来。** B站没有官方批量导出:单条视频的字幕要在播放器面板里一条条看,UP 主几百条投稿、合集几十集只能手点。而抓密了会触发 `-352 风控校验失败`,之后一段时间内**所有**接口一起拒,不是只拒被拦的那一条。手动抓的人因此陷入「越抓越慢、越慢越急」。
|
|
49
|
+
|
|
50
|
+
`asr-polish subs` 把这件事变成一条命令(单视频 / 多P / UP 主 / 合集 / 关键词搜索);`serve` 让它在后台排队跑,页面关掉任务也不丢;内置限速按域名把请求间隔拉开,不把账号抓进风控。
|
|
51
|
+
|
|
52
|
+
**二、拿下来的文本不一定能用。** ASR 在中文技术内容上有三个顽固问题,而现有工具都只做到「取到文本」这一步,**没人管取到的文本准不准**:
|
|
53
|
+
|
|
54
|
+
1. **术语被转成同音错字** —— `SQL` → `色考`、`Python` → `PASS`、`JSON` → `接生`、`Flask` → `福拉斯克`
|
|
55
|
+
2. **多引擎结果互相矛盾** —— 云端 ASR 和本地 whisper 各说各话,不知道该信谁
|
|
56
|
+
3. **转写完不知道质量** —— 不敢直接拿去总结,担心错字误导结论
|
|
57
|
+
|
|
58
|
+
两层可以分开用:批量抓(`subs` / `serve`)只负责**完整、快速、可批量**,不承诺修正内容;单条取文本(`fetch`)默认接上订正和打分,不想要就 `--no-correct`。下面按这个顺序讲。
|
|
59
|
+
|
|
60
|
+
## 核心能力
|
|
61
|
+
|
|
62
|
+
前四项是**抓取链路**:取一条 → 抓一批 → 排队跑 → 不被封。后三项是**文本加工**:订正、交叉验证、质检。两组可以只用其一。
|
|
63
|
+
|
|
64
|
+
### ① 从 B站视频取文本 `fetch`
|
|
65
|
+
|
|
66
|
+
一条命令,直接从 B站视频链接拿到可用的转写文本(字幕轨优先,无字幕走必剪云端 ASR),并自动接上订正 + 打分。
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
from asr_polish import fetch
|
|
70
|
+
|
|
71
|
+
result = fetch("https://www.bilibili.com/video/BVxxxxxxxxxx")
|
|
72
|
+
print(result.title) # 视频标题
|
|
73
|
+
print(result.source) # "字幕轨" 或 "必剪云端ASR"
|
|
74
|
+
print(result.text) # 转写文本(可直接接 correct_text / assess)
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
- **字幕轨优先**:有字幕直接取(纯标准库、秒级),无字幕自动切必剪云端 ASR(约 13 秒)
|
|
78
|
+
- **有 Cookie 能取到 AI 字幕**:传入 cookies.txt 路径可多取到 B站 AI 字幕轨
|
|
79
|
+
- **隐私**:无字幕时音频会上传至 B站服务器(公开视频音轨,风险低)
|
|
80
|
+
|
|
81
|
+
### ② 批量抓字幕 `subs`
|
|
82
|
+
|
|
83
|
+
把字幕**取下来**,落到文件里。这条路径不承诺修正内容——它的目标是完整、快速、可批量。
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
# 单个视频,一次导出四种格式
|
|
87
|
+
asr-polish subs BV1xx411c7mD -f txt,srt,json,md -o ./subs
|
|
88
|
+
|
|
89
|
+
# 多 P 视频只取其中三集
|
|
90
|
+
asr-polish subs BV1xx411c7mD --pages 1,2,3
|
|
91
|
+
|
|
92
|
+
# 某个 UP 主最新的 20 条投稿
|
|
93
|
+
asr-polish subs --up 329954990 --limit 20 -j 3
|
|
94
|
+
|
|
95
|
+
# 某个合集/系列
|
|
96
|
+
asr-polish subs --season 329954990 12345
|
|
97
|
+
|
|
98
|
+
# 按关键词搜一批再抓(--search-pages 控制搜几页,每页约 42 条)
|
|
99
|
+
asr-polish subs --search "Python教程" --search-pages 2 --limit 30
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
输出(`-f txt`):
|
|
103
|
+
|
|
104
|
+
```
|
|
105
|
+
subs/
|
|
106
|
+
├── BV1RXx169Exu-P1-【前言】Python从零入门到精通系统学习大纲.txt
|
|
107
|
+
├── BV1RXx169Exu-P2-【基础篇】Python解释器的安装.txt
|
|
108
|
+
└── BV1RXx169Exu-P3-【基础篇】PyCharm编辑器的安装.txt
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
- **四种格式**:`txt` 纯文本、`srt` 带时间轴字幕、`json` 结构化(含每句起止时间)、`md` 带标题/UP主/来源的笔记
|
|
112
|
+
- **多 P 命名**:自动带上 `P{n}` 和分 P 标题,不会因为总标题太长而一堆文件重名
|
|
113
|
+
- **默认不走 ASR**:无字幕就直接记为「无字幕」跳过,不会给每个视频白传一遍音频(需要兜底时加 `--asr`)
|
|
114
|
+
- **默认不订正**:抓下来的是字幕原文,保持忠实;要顺手做术语订正加 `--correct`
|
|
115
|
+
- **并发可调**:`-j 3` 默认,过高容易触发风控
|
|
116
|
+
|
|
117
|
+
`fetch` 与 `subs` 的分工:`fetch` 是「取一篇到 stdout,接订正和打分」;`subs` 是「按批取下来,落成文件」。
|
|
118
|
+
|
|
119
|
+
#### 必须配 Cookie
|
|
120
|
+
|
|
121
|
+
B站只向**登录态**下发字幕列表——游客请求 `player/wbi/v2` 拿到的是空数组,不是报错,很容易误判成「这个视频没字幕」。
|
|
122
|
+
|
|
123
|
+
把浏览器导出的 `cookies.txt`(Netscape 格式)放到以下任一位置即可自动加载:
|
|
124
|
+
|
|
125
|
+
```
|
|
126
|
+
~/.workbuddy/data/bilibili-video-summary/bilibili_cookies.txt
|
|
127
|
+
~/Downloads/www.bilibili.com_cookies.txt
|
|
128
|
+
~/.config/asr-polish/cookies.txt
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
或用 `--cookie /path/to/cookies.txt` 指定。
|
|
132
|
+
|
|
133
|
+
**怎么拿到这份 Cookie(不用装插件)**:
|
|
134
|
+
|
|
135
|
+
1. 浏览器打开 `https://www.bilibili.com`,确认右上角是自己的头像(已登录);
|
|
136
|
+
2. `F12` → **网络 / Network** → 按 `F5` 刷新页面;
|
|
137
|
+
3. 点列表里第一条请求 → 右侧 **请求标头 / Request Headers** → 找到 `cookie:` 那行,
|
|
138
|
+
右键 **复制值 / Copy value**。
|
|
139
|
+
|
|
140
|
+
然后:
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
asr-polish cookie set # 直接读剪贴板,写完立刻向 B站 确认登录态
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
也可以 `--file 某个.txt` 指定 cookies.txt 全文。写入是**合并**:只更新同名的那几条,
|
|
147
|
+
`buvid3` 这类设备指纹会保留。没有 `SESSDATA` 的内容直接拒绝——那是「未登录」的 Cookie,
|
|
148
|
+
写进去只会让人以为配好了。
|
|
149
|
+
|
|
150
|
+
随时用 `asr-polish cookie` 查看状态。**判断是否有效只认 `nav` 接口的 `isLogin`**:
|
|
151
|
+
文件在、字段齐、过期时间还剩半年,都可能已经失效(改密码、被踢下线后服务端会注销)。
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
### ③ 服务化:抓取变成任务队列 `serve`
|
|
155
|
+
|
|
156
|
+
批量抓 100 个 UP 主投稿要几分钟,同步接口扛不住:网关 30 秒就掐断连接,页面一关任务也废了。所以给同一个抓取能力加了一层**任务队列**:
|
|
157
|
+
|
|
158
|
+
```bash
|
|
159
|
+
asr-polish serve --port 8040 # 起服务(内含 2 个 worker)
|
|
160
|
+
asr-polish submit up:12345 --limit 50 --wait # 提交并按进度等它跑完
|
|
161
|
+
asr-polish status # 看队列与最近任务
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
浏览器打开 `http://127.0.0.1:8040` 就是任务面板,每 3 秒刷新。
|
|
165
|
+
|
|
166
|
+
| 接口 | 说明 |
|
|
167
|
+
|---|---|
|
|
168
|
+
| `POST /api/tasks` | 提交任务,立刻返回 `202` 和任务 id(不等执行) |
|
|
169
|
+
| `GET /api/tasks/{id}` | 查状态、进度、结果、失败原因 |
|
|
170
|
+
| `GET /api/tasks?status=&limit=` | 列表 |
|
|
171
|
+
| `POST /api/tasks/{id}/retry` | `failed` 重新排队;`running` 强制重启(旧执行的结果会被丢弃) |
|
|
172
|
+
| `POST /api/tasks/{id}/retry-failed` | 只重跑失败的条目:**新建一个任务**,带 `only`,已成功的文件不动 |
|
|
173
|
+
| `GET /api/health` | 存活与 worker 占用 |
|
|
174
|
+
|
|
175
|
+
三件事决定了它的可靠性,都在 `asr_polish/server/` 里:
|
|
176
|
+
|
|
177
|
+
- **请求只登记,执行交给 worker**。接口写一行 `pending` 就返回,长活跟请求彻底解耦。
|
|
178
|
+
- **状态在 SQLite,不在内存**。进程重启、页面关掉,任务都还在;失败可重试、能查卡在哪一条。
|
|
179
|
+
- **抢任务用 CAS,不靠运气**。`BEGIN IMMEDIATE` 先拿写锁再 `SELECT + UPDATE`,两个 worker 不可能领到同一行;再加心跳与僵尸回收,进程被 `kill -9` 留下的 `running` 也能被放回队列。
|
|
180
|
+
- **写回要持有所有权**。每次 `claim()` 把 `attempt` 加一,worker 之后的心跳和结论都带 `WHERE id=? AND attempt=?`,写不进去就说明自己已被接管。没有这一条,一个被误判僵尸、又醒过来的 worker 会和接管者同时写同一行——它甚至能把接管者**还在跑**的任务抢先标成完成。
|
|
181
|
+
|
|
182
|
+
### 部分成功不等于成功
|
|
183
|
+
|
|
184
|
+
一批 20 条里成了 18 条,和 20 条全成,以前在任务表上是同一个样子:状态 `done`,
|
|
185
|
+
只有 `result.failed` 一个数字不同。要补那 2 条只能整个任务重来,重来还会再吃一次风控。
|
|
186
|
+
|
|
187
|
+
现在结果里多一个 `outcome`,四态:
|
|
188
|
+
|
|
189
|
+
| `outcome` | 含义 | 任务状态 |
|
|
190
|
+
|---|---|---|
|
|
191
|
+
| `complete` | 每一条都取到字幕 | `done` |
|
|
192
|
+
| `partial` | 有产出,但差了几条(失败或没字幕) | `done` |
|
|
193
|
+
| `empty` | 零产出:全都没字幕,或指定的条目里没有可跑的 | `done` |
|
|
194
|
+
| `failed` | 零产出且全是错误 | `failed` |
|
|
195
|
+
|
|
196
|
+
`failed_items` 列出每一处失败:`bvid`、`page`、`error`,以及 `retry` ——
|
|
197
|
+
`retry` 就是重跑时该传的写法(`BV1xx` 或 `BV1xx#3`),面板和 CLI 直接照抄。
|
|
198
|
+
面板上每条任务带一个徽章和「N 处失败明细」,展开后能点「只重跑这 N 条」;
|
|
199
|
+
CLI 跑完会打印一条可直接复制的命令:
|
|
200
|
+
|
|
201
|
+
```bash
|
|
202
|
+
asr-polish subs --only BV1xx411c003 BV1xx411c011#3
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
`--only` 只按给定的 BV 号跑,**不会重新解析 target**——UP 主投稿一直在变,
|
|
206
|
+
重新解析拿到的是「现在最新的 20 条」,不是你要补的那几条。
|
|
207
|
+
|
|
208
|
+
```python
|
|
209
|
+
# asr_polish/server/store.py:抢任务的原子写法
|
|
210
|
+
conn.execute("BEGIN IMMEDIATE")
|
|
211
|
+
row = conn.execute("SELECT id FROM tasks WHERE status='pending' ORDER BY created_at LIMIT 1")
|
|
212
|
+
conn.execute("UPDATE tasks SET status='running', worker=?, started_at=? WHERE id=? AND status='pending'", ...)
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
**边界**:单机、单库、无鉴权,默认只监听 `127.0.0.1`——要对外必须先自己套反向代理。它不是分布式队列,多机部署得换 PostgreSQL 或 Redis。写它是为了让你看清这套骨架,不是为了替代 Celery。
|
|
216
|
+
|
|
217
|
+
### ④ 批量抓不被封:限速与风控
|
|
218
|
+
|
|
219
|
+
抓 `up:` 几十条投稿时,每条要打 3~4 个接口(`view` → `nav` → `player/wbi/v2` → 字幕 CDN),全速发就是几百个请求连着出去。B站对这类模式的反应是 `-352 风控校验失败`,而且**之后一段时间内所有接口都拒绝**,不是只拒被拦的那一条。
|
|
220
|
+
|
|
221
|
+
所以「自己偶尔抓一条」和「挂着长期跑」是两件事。这一版补了三样:
|
|
222
|
+
|
|
223
|
+
**① 请求间隔下限。** 默认同一域名 1 秒一个请求,带 ±30% 抖动(固定等间隔本身就是机器特征)。放在 `_http_json` 底层而不是批量循环里——循环里加 `sleep` 管不到翻页、wbi 密钥这些旁路请求。
|
|
224
|
+
|
|
225
|
+
```bash
|
|
226
|
+
asr-polish serve --min-interval 1.5 # 风控频繁时调到 2~3
|
|
227
|
+
asr-polish subs --up 12345 --min-interval 2
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
注意:速率由它控制,`subs -j` 的并发数不再影响 API 压力(只影响音频下载与本地处理)。
|
|
231
|
+
|
|
232
|
+
**② 遇风控整体退避,而不是记一条失败。** 风控是按 IP / 账号记的,继续打只会延长限制。现在是 60 → 180 → 300 秒退避后重试同一条,期间持续刷新心跳(不刷会被僵尸回收器当成卡死任务重新排队),面板上能看到「第 2 次被风控,退避 180s 后重试,还需 60s」。累计 9 分钟仍被拦才判失败。
|
|
233
|
+
|
|
234
|
+
**③ 区分「没登录」和「太快」。** 这两件事返回码都是 `-352`,处置却相反:前者换 Cookie 有用,后者等一下就好。遇到 `-352` 会先用 `nav` 探一次登录态(缓存 5 分钟),未登录就直接告诉你换 Cookie,不白等。
|
|
235
|
+
|
|
236
|
+
面板顶部那条横幅就是 Cookie 状态:**绿** = 有效,**黄** = 超过 30 天没更新,**红** = 失效或缺失。判断依据是 `nav` 接口的 `isLogin`,不是文件里的过期时间——SESSDATA **可能时间上没过期、服务端却已注销**(改密码、被踢下线),只看文件永远发现不了。点「检查登录态」强制重探。
|
|
237
|
+
|
|
238
|
+
```bash
|
|
239
|
+
curl "http://127.0.0.1:8040/api/health" # 默认就验证登录态
|
|
240
|
+
curl "http://127.0.0.1:8040/api/health?verify=0" # 只做本地检查,不联网
|
|
241
|
+
curl "http://127.0.0.1:8040/api/health?fresh=1" # 绕过缓存强制重探
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
### ⑤ 术语订正 `correction`
|
|
245
|
+
|
|
246
|
+
基于可扩展的词表,自动把 ASR 的同音/形近错字改回正确术语。
|
|
247
|
+
|
|
248
|
+
```python
|
|
249
|
+
from asr_polish import correct_text
|
|
250
|
+
|
|
251
|
+
raw = "我们今天讲色考和PASS,还有福拉斯克框架。"
|
|
252
|
+
result = correct_text(raw)
|
|
253
|
+
print(result.text) # 我们今天讲SQL和Python,还有Flask框架。
|
|
254
|
+
print(result.summary()) # 订正 3 处:色考 → SQL;PASS → Python;福拉斯克 → Flask
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
- **长词优先**:`Aging Decoding` 整体订正为 `Agentic Coding`,不会被子串抢先误替换
|
|
258
|
+
- **英文变体加词边界**:`PASS`(全大写)命中 `Python`,但英文常用词 `pass`、`compass` 不受影响(订正与打分用同一套判定)
|
|
259
|
+
- **变体级置信分级**:置信度标在「变体」上,而非整条词目。规则默认 ASCII 变体 high、中文变体 medium;**≤2 字的中文变体即使条目写了 `confidence: high` 也按 medium 处理**,确认安全的必须用 `high_variants` 逐个升级(两字中文与日常词碰撞率太高,`全占`/`给特`/`色口` 都会出现在正常句子里)。medium 变体(如 `一部`→`异步`)需要上下文佐证才订正,`我们看一部电影` 不会被误改成 `我们看异步电影`
|
|
260
|
+
- **文档级主题证据**:中文同音错(`酸法`/`算法`、`还存`/`缓存`)在 ±8 字内根本分不开,于是再给一条证据——**整篇在讲什么**。一篇讲算法的字幕里「算法」出现几十次、「酸法」只出现一两次,那几次几乎必然是转写错误;反过来讲腌菜的视频里「算法」不出现,就不会改。判定要求「正确写法 ≥2 次,且 ≥2×(变体次数+1)」。这条只在输入是整篇文本时生效(`correct_text(整篇)` / `correct 文件`),传单句时自动退化为纯上下文判定。1.03M 字真实 B 站 AI 字幕实测:medium 变体召回 2.3% → 72%,干净语料误伤 0。
|
|
261
|
+
**但这个数字要按正确口径读**:它测的是「把本来就写对的术语替换成变体,能不能改回来」,属于人造扰动下的上限。真实字幕是**系统性偏置**——同一个词被一直写错,正确写法在文里根本不出现,文档证据因此失效。用人工字幕做参照实测(`tools/eval_against_human.py`):5 个视频 46,218 字,订正器改动 **0 处**。见下文「已知边界」。
|
|
262
|
+
- **上下文分强弱**:`strong_context`(如「虚拟机」「序列化」)命中 1 个即可,`context` 弱词需命中 ≥2 个——`我们讲爪哇的虚拟机` 能订正为 Java,而 `杰森·斯坦森主演的动作片` 不会误判为 JSON
|
|
263
|
+
- **邻接保护 `guards` / `left_guards`**:给「会被更长正常词包含」的变体加负向前瞻(右邻)或负向后顾(左邻)。如 `全占(?!满|领|了)`、`给特(?!写|别)`、`买色口(?!红|腔)`,以及 `PASS` 前有空白时不订正(`6/6 PASS`、`全部 PASS` 是测试日志不是 Python)。注意它只覆盖词表里已登记的搭配,不是通用分词,未登记的新搭配仍可能误伤
|
|
264
|
+
- **每处订正可追溯**:返回原文、位置、类别、置信度;`summary()` 会给 medium 改动标注「待确认」,方便只审这类改动
|
|
265
|
+
|
|
266
|
+
### ⑥ 多引擎交叉验证 `crosscheck`
|
|
267
|
+
|
|
268
|
+
对同一段音频的多个转写结果做 token 级序列对齐,输出片段级置信度。
|
|
269
|
+
|
|
270
|
+
```python
|
|
271
|
+
from asr_polish import cross_check, confidence_report
|
|
272
|
+
|
|
273
|
+
cloud = "今天讲SQL和Python,还有Flask框架。"
|
|
274
|
+
local = "今天讲色考和PASS,还有福拉斯克框架。"
|
|
275
|
+
result = cross_check(cloud, local)
|
|
276
|
+
print(confidence_report(result))
|
|
277
|
+
# 字符一致率:71%
|
|
278
|
+
# 片段总数:7,低置信片段:3 个
|
|
279
|
+
# [低置信] (1/2 引擎一致) SQL
|
|
280
|
+
# [低置信] (1/2 引擎一致) Python
|
|
281
|
+
# [低置信] (1/2 引擎一致) Flask
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
> 上面的「字符一致率」是**按字符加权的片段平均一致度**(衡量多引擎有多一致),不是「71% 内容是对的」——这个例子里三个术语其实全错,只是各引擎各说各话。
|
|
285
|
+
|
|
286
|
+
- 多个引擎一致 → 高置信;互相矛盾 → 低置信
|
|
287
|
+
- 输出 `low_confidence_segments` / `conflict_segments`,供总结时对不确定片段谨慎处理
|
|
288
|
+
|
|
289
|
+
### ⑦ 转写质量自评 `quality`
|
|
290
|
+
|
|
291
|
+
四维打分,一眼看出这段文字能不能信。
|
|
292
|
+
|
|
293
|
+
```python
|
|
294
|
+
from asr_polish import assess
|
|
295
|
+
|
|
296
|
+
report = assess(text, duration_seconds=1316)
|
|
297
|
+
print(report.summary())
|
|
298
|
+
# 转写质量评分:96 / 100
|
|
299
|
+
# 乱码检测: 0.00% → 100分
|
|
300
|
+
# 可疑同音字: 1.00处 → 85分
|
|
301
|
+
# ...
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
四个维度(全部为负向/中性信号):乱码检测(硬伤)、重复片段、可疑同音字、字符密度。乱码或重复严重时**一票封顶**,总分不会及格的假象。
|
|
305
|
+
|
|
306
|
+
> 分数只用于「同一段内容的横向对比」(如对比两个引擎对同一音频的转写),不可跨内容比较。
|
|
307
|
+
|
|
308
|
+
## 实测效果
|
|
309
|
+
|
|
310
|
+
用一段 22 分钟的真实技术视频,对比「本地 faster-whisper」与「必剪云端 ASR」两份转写:
|
|
311
|
+
|
|
312
|
+
| 指标 | 本地 whisper | 必剪云端 |
|
|
313
|
+
|---|---|---|
|
|
314
|
+
| 术语订正命中 | 3 处 | 0 处 |
|
|
315
|
+
| 质量评分 | **81 分** | **96 分** |
|
|
316
|
+
| 交叉验证字符一致率 | 96% | |
|
|
317
|
+
|
|
318
|
+
质量评分客观地区分出了两个引擎的真实差距——本地 whisper 的错误变体(可疑同音字)明显更多。这正是「转写完不知道哪个准」的解法。
|
|
319
|
+
|
|
320
|
+
## 安装
|
|
321
|
+
|
|
322
|
+
**取字幕、订正、评分都不需要第三方依赖**,Python 3.9+ 即可:
|
|
323
|
+
|
|
324
|
+
```bash
|
|
325
|
+
pip install asr-polish # 全部能力,零第三方依赖
|
|
326
|
+
pip install "asr-polish[bilibili]" # 只在「无字幕视频要转写」时需要(引入 yt-dlp)
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
想改代码就从源码装:
|
|
330
|
+
|
|
331
|
+
```bash
|
|
332
|
+
git clone https://github.com/angus-hu-gzy/asr-polish.git
|
|
333
|
+
pip install -e ".[dev]"
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
> `yt-dlp` 仅在「视频没有字幕轨 → 转走必剪云端 ASR」时才被 import。有字幕的视频(含 B站 AI 字幕)不装它也能取。
|
|
337
|
+
|
|
338
|
+
装好后即可用命令行工具 `asr-polish`:
|
|
339
|
+
|
|
340
|
+
```bash
|
|
341
|
+
# 从 B站视频取文本(字幕轨优先,无字幕走必剪云端 ASR)
|
|
342
|
+
asr-polish fetch "https://www.bilibili.com/video/BVxxxxxxxxxx"
|
|
343
|
+
asr-polish fetch BVxxxxxxxxxx --out transcript.txt # 文本存文件
|
|
344
|
+
|
|
345
|
+
# 批量抓字幕到文件(txt / srt / json / md)
|
|
346
|
+
asr-polish subs BVxxxxxxxxxx -f txt,srt -o ./subs
|
|
347
|
+
asr-polish subs --up 329954990 --limit 20 -j 3 # 某 UP 主最新 20 条
|
|
348
|
+
|
|
349
|
+
# 术语订正
|
|
350
|
+
asr-polish correct "今天讲色考和PASS,用福拉斯克搭微服务,讲h t t p和k8s"
|
|
351
|
+
|
|
352
|
+
# 质量打分(--duration 传音频秒数,用于计算字符密度)
|
|
353
|
+
asr-polish assess "转写文本" --duration 1316
|
|
354
|
+
|
|
355
|
+
# 交叉验证两个转写文本文件
|
|
356
|
+
asr-polish crosscheck cloud.txt local.txt
|
|
357
|
+
|
|
358
|
+
# 词表管理
|
|
359
|
+
asr-polish lexicon list # 列出词表
|
|
360
|
+
asr-polish lexicon import my_words.txt # 批量导入
|
|
361
|
+
asr-polish lexicon export --out out.json # 导出
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
> `fetch` 文本走 stdout、元信息与评分走 stderr,方便 `asr-polish fetch <链接> > text.txt` 直接存文本。
|
|
365
|
+
|
|
366
|
+
## 快速开始
|
|
367
|
+
|
|
368
|
+
装好之后最常用的四条:
|
|
369
|
+
|
|
370
|
+
```bash
|
|
371
|
+
# 单个视频取文本(默认接上订正 + 打分)
|
|
372
|
+
asr-polish fetch BV1xx411c7mD
|
|
373
|
+
|
|
374
|
+
# 批量抓字幕,一次导出四种格式
|
|
375
|
+
asr-polish subs BV1xx411c7mD -f txt,srt,json,md -o ./subs
|
|
376
|
+
|
|
377
|
+
# 某个 UP 主最新 20 条投稿,并发 3
|
|
378
|
+
asr-polish subs --up 329954990 --limit 20 -j 3
|
|
379
|
+
|
|
380
|
+
# 起本地服务,批量任务交给它排队跑
|
|
381
|
+
asr-polish serve --port 8040
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
开发与验证(从源码目录执行):
|
|
385
|
+
|
|
386
|
+
```bash
|
|
387
|
+
# 直接运行示例(无需安装)
|
|
388
|
+
python examples/demo.py
|
|
389
|
+
|
|
390
|
+
# 运行全部测试
|
|
391
|
+
python -m unittest discover -s tests -v
|
|
392
|
+
# 或直接跑单个文件
|
|
393
|
+
python tests/test_basic.py
|
|
394
|
+
|
|
395
|
+
# 准确性门禁(CI 里跑的就是这两条)
|
|
396
|
+
python tools/eval_corrections.py --min-precision 0.98 --min-recall 0.95
|
|
397
|
+
python tools/eval_corrections.py --cases eval/holdout.jsonl --min-precision 0.98 --min-recall 0.90
|
|
398
|
+
|
|
399
|
+
# 用语料审计词表误伤(开发工具,不随包安装)
|
|
400
|
+
python tools/audit_corrections.py 语料.txt 或/语料目录
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
## 自定义词表
|
|
404
|
+
|
|
405
|
+
词表位于 `asr_polish/data/corrections.json`,结构如下:
|
|
406
|
+
|
|
407
|
+
```json
|
|
408
|
+
{
|
|
409
|
+
"entries": [
|
|
410
|
+
{ "correct": "SQL", "category": "编程语言",
|
|
411
|
+
"confidence": "high",
|
|
412
|
+
"variants": ["色考", "色口", "seq l"],
|
|
413
|
+
"guards": {"色口": "红"}, "note": "结构化查询语言" },
|
|
414
|
+
{ "correct": "异步", "category": "技术术语",
|
|
415
|
+
"high_variants": ["易步"],
|
|
416
|
+
"variants": ["一部", "易步"],
|
|
417
|
+
"strong_context": ["协程", "非阻塞"],
|
|
418
|
+
"context": ["线程", "协程", "回调", "并发"], "note": "Async" }
|
|
419
|
+
]
|
|
420
|
+
}
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
字段说明:
|
|
424
|
+
- `confidence`:整体指定 `high`/`medium`;中文变体默认 medium(安全优先),ASCII 变体默认 high。**≤2 字中文变体不受条目级 `high` 影响**,需在 `high_variants` 里逐个声明。
|
|
425
|
+
- `high_variants`:把确认「不会独立成词」的安全中文变体升级为 high(无条件订正),如 `色考`、`易步`。
|
|
426
|
+
- `strong_context`:medium 变体的强上下文词,±8 字内命中 **1 个**即订正(如「虚拟机」对「爪哇」)。
|
|
427
|
+
- `context`:medium 变体的弱上下文词,±8 字内需命中 **≥2 个**才订正。
|
|
428
|
+
- `ordinary_variants`:登记「本身就是正常中文词」的变体(如 `一部`、`爱的`),对它们**禁用文档级主题证据**,只认 ±8 字局部上下文——`一部` 在任何一篇里都可能是量词,同篇「异步」再高频也不能翻案。该登记哪些要在真实语料上量(看这个变体出现在多少篇里),工具:`python tools/audit_variants.py 语料目录`。
|
|
429
|
+
- `guards`:`{变体: 正则}`,给变体加负向前瞻做右邻保护——如 `"色口": "红|腔"` 让「买色口红色号」「买色口腔」不被误判。
|
|
430
|
+
- `left_guards`:`{变体: 正则}`,加负向后顾做左邻保护——如 `"PASS": "\\s"` 让「6/6 PASS」「全部 PASS」不被改成 Python。lookbehind 必须定长,变长正则会告警后丢弃。
|
|
431
|
+
- 列表字段与 `guards` / `left_guards` 都会在 `lexicon import` 时透传并做正则预校验,非法 guard 会告警后丢弃。
|
|
432
|
+
|
|
433
|
+
按你的领域增删条目即可,也支持加载外部词表:
|
|
434
|
+
|
|
435
|
+
```python
|
|
436
|
+
from asr_polish import Lexicon, correct_text
|
|
437
|
+
|
|
438
|
+
lexicon = Lexicon("/path/to/my_corrections.json")
|
|
439
|
+
result = correct_text(text, lexicon)
|
|
440
|
+
```
|
|
441
|
+
|
|
442
|
+
### 批量导入(不用手写 JSON)
|
|
443
|
+
|
|
444
|
+
手写 JSON 既慢又容易格式出错。`lexicon import` 支持三种人类友好的格式,自动识别:
|
|
445
|
+
|
|
446
|
+
**TXT**(每行一条,`#` 注释,分隔符支持 `=` / `->` / `:`):
|
|
447
|
+
|
|
448
|
+
```
|
|
449
|
+
# 医疗影像领域
|
|
450
|
+
DICOM = 迪康, 迪克姆, d i c o m
|
|
451
|
+
PACS = 帕克斯, p a c s
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
**CSV**(表头 `correct,category,variants,note`,变体用 `|` 或 `;` 分隔):
|
|
455
|
+
|
|
456
|
+
```csv
|
|
457
|
+
correct,category,variants,note
|
|
458
|
+
Vue,框架工具,"v u e|维优",前端框架
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
**JSON**(标准词表结构或其 `entries` 数组):
|
|
462
|
+
|
|
463
|
+
```json
|
|
464
|
+
[{"correct": "K8s", "variants": ["k 8 s", "库伯内特斯"]}]
|
|
465
|
+
```
|
|
466
|
+
|
|
467
|
+
导入时按 `correct` 去重合并:已存在的术语自动合并变体,新术语追加,并保留词表的名称/描述等元信息。
|
|
468
|
+
|
|
469
|
+
```bash
|
|
470
|
+
asr-polish lexicon import examples/import_sample.txt # 写入用户词表(~/.config/asr-polish/lexicon.json)
|
|
471
|
+
asr-polish lexicon import my.csv --out my_lexicon.json # 导入到指定文件
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
> 完整示例见 `examples/import_sample.txt` 和 `examples/import_sample.csv`。
|
|
475
|
+
|
|
476
|
+
## 目录结构
|
|
477
|
+
|
|
478
|
+
```
|
|
479
|
+
asr-polish/
|
|
480
|
+
├── pyproject.toml # 打包配置(pip install 可装,提供 asr-polish 命令)
|
|
481
|
+
├── CHANGELOG.md # 逐版本变更记录(含每版的实测数字)
|
|
482
|
+
├── CONTRIBUTING.md # 怎么跑测试、怎么改词表、怎么发版
|
|
483
|
+
├── LICENSE # MIT
|
|
484
|
+
├── NOTICE # 第三方声明(bcut-asr 参考)
|
|
485
|
+
├── asr_polish/
|
|
486
|
+
│ ├── __init__.py # 统一入口
|
|
487
|
+
│ ├── __main__.py # 支持 python -m asr_polish
|
|
488
|
+
│ ├── py.typed # PEP 561 类型标记
|
|
489
|
+
│ ├── cli.py # 命令行入口(fetch / subs / serve / submit / status / correct …)
|
|
490
|
+
│ ├── correction.py # 术语订正引擎
|
|
491
|
+
│ ├── crosscheck.py # 多引擎交叉验证
|
|
492
|
+
│ ├── quality.py # 转写质量自评
|
|
493
|
+
│ ├── bili.py # B站字幕抓取 + 取文本(字幕轨 → 必剪云端 ASR)
|
|
494
|
+
│ ├── export.py # txt / srt / json / md 四种导出
|
|
495
|
+
│ ├── lexicon.py # 词表加载
|
|
496
|
+
│ ├── lexicon_io.py # 词表批量导入/导出
|
|
497
|
+
│ ├── server/ # 服务化:任务队列 + HTTP(零依赖)
|
|
498
|
+
│ │ ├── store.py # SQLite 任务表 + 状态机(CAS 抢任务)
|
|
499
|
+
│ │ ├── worker.py # worker 池 + 心跳 + 僵尸回收
|
|
500
|
+
│ │ ├── jobs.py # 任务执行器(把抓取包装成可调度的 job)
|
|
501
|
+
│ │ └── app.py # HTTP 路由 + 面板
|
|
502
|
+
│ └── data/
|
|
503
|
+
│ └── corrections.json # 默认术语词表
|
|
504
|
+
├── eval/
|
|
505
|
+
│ ├── cases.jsonl # 开发集(调参用)
|
|
506
|
+
│ └── holdout.jsonl # 留出集(不参与调参,衡量泛化)
|
|
507
|
+
├── docs/
|
|
508
|
+
│ ├── 召回基准报告.md # 召回率怎么测的、为什么四个反证实验被否掉
|
|
509
|
+
│ └── 真实字幕错误分析.md # 人工字幕当真值,真实错误长什么样
|
|
510
|
+
├── examples/
|
|
511
|
+
│ ├── demo.py # 演示
|
|
512
|
+
│ ├── import_sample.txt # 批量导入示例(TXT)
|
|
513
|
+
│ └── import_sample.csv # 批量导入示例(CSV)
|
|
514
|
+
├── tools/ # 开发工具,不随包安装
|
|
515
|
+
│ ├── eval_corrections.py # 精确率/召回率门禁(CI 用)
|
|
516
|
+
│ ├── benchmark_recall.py # 人造扰动召回基准
|
|
517
|
+
│ ├── eval_against_human.py # 人工字幕 vs AI 字幕,算真实 CER
|
|
518
|
+
│ ├── collect_corpus.py # 采集 AI 字幕语料(给上面两个基准用)
|
|
519
|
+
│ ├── audit_corrections.py # 语料审计:误伤
|
|
520
|
+
│ ├── audit_variants.py # 语料审计:哪些变体其实是常用词
|
|
521
|
+
│ ├── ci_guard.py # 打标签前的 CI 闸门(0 绿 / 1 红或未完成 / 2 查不到)
|
|
522
|
+
│ └── hooks/pre-push # 推标签时自动跑 ci_guard,推分支不受影响
|
|
523
|
+
└── tests/
|
|
524
|
+
├── test_basic.py # 核心能力 + 误伤回归 + medium 用例
|
|
525
|
+
├── test_lexicon_io.py # 词表导入/导出
|
|
526
|
+
├── test_export.py # 四种导出格式 + 文件名清洗 + 原子写
|
|
527
|
+
├── test_risk.py # 风控识别、退避重试、Cookie 状态
|
|
528
|
+
├── test_throttle.py # 节流器
|
|
529
|
+
├── test_cli.py # 命令行健壮性(管道编码、--only)
|
|
530
|
+
├── test_cookie_import.py # Cookie 解析与写入(含 #HttpOnly_)
|
|
531
|
+
├── test_server.py # 状态机 / 并发抢任务 / 重试 / 僵尸回收 / HTTP
|
|
532
|
+
├── test_jobs_outcome.py # 批量任务四态 + 只重跑失败条目
|
|
533
|
+
└── test_ci_guard.py # 标签闸门:按提交判定、标签对象解引用、只拦标签
|
|
534
|
+
```
|
|
535
|
+
|
|
536
|
+
## 变更记录
|
|
537
|
+
|
|
538
|
+
完整逐版本记录见 [CHANGELOG.md](./CHANGELOG.md),每个版本都写明实测数字与判断依据。最近四版:
|
|
539
|
+
|
|
540
|
+
| 版本 | 一句话 |
|
|
541
|
+
|---|---|
|
|
542
|
+
| **0.9.1** | 结果区分全部成功 / 部分成功 / 无产出;列出失败明细,`retry-failed` 与 `subs --only` 只补失败的那几条 |
|
|
543
|
+
| **0.9.0** | 任务写回加所有权令牌:被接管(重试 / 僵尸回收)的旧执行无法再写状态,`retry` 对 `running` 从「复制一份」变成「真重启」 |
|
|
544
|
+
| **0.8.0** | 请求限速 + 风控退避 + Cookie 状态提示;批量抓不再把账号抓进风控,`cookie set` 一条命令导入登录态 |
|
|
545
|
+
| **0.7.1** | 修僵尸回收不看重试上限导致任务无限重跑;状态输出统一中文 |
|
|
546
|
+
|
|
547
|
+
> 0.5.0 有一个 P0 回归(`--out` 帮助文本里 `%APPDATA%` 未转义,argparse 崩溃,CLI 全不可用),
|
|
548
|
+
> 已在 0.5.1 修复。装到 0.5.0 的请直接升级。
|
|
549
|
+
|
|
550
|
+
|
|
551
|
+
## 许可证
|
|
552
|
+
|
|
553
|
+
[MIT](./LICENSE) · 作者 [angus-hu-gzy](https://github.com/angus-hu-gzy)
|
|
554
|
+
|
|
555
|
+
其他文档:[变更记录](./CHANGELOG.md) · [参与贡献](./CONTRIBUTING.md) · [测量报告](./docs)
|
|
556
|
+
|
|
557
|
+
## 诚实说明(边界)
|
|
558
|
+
|
|
559
|
+
- **只取公开可见的内容,且不保证每条都有**。视频得有字幕轨才能直接取(UP 主上传的 CC,或 B站自己的 AI 字幕);AI 字幕要带登录态 Cookie 才下发。完全没有字幕的视频,单条 `fetch` 会转走必剪云端 ASR(需 `yt-dlp`,音频上传至 B站服务器)。
|
|
560
|
+
- **限速能把风控概率压低,不是保证**。B站的判定按 IP / 账号记,长时间高频抓仍可能被拦;被拦后按 60 → 180 → 300 秒退避,通常能恢复,但恢复时间不由我们控制。
|
|
561
|
+
- **订正基于词表,不是万能**。它能抓词表里收录的错误;词表没覆盖的错字抓不到。因此每处订正都**留痕可回溯**(含置信度),重要场景请人工核对,尤其 `summary()` 标注「待确认」的 medium 改动。为降低误伤,英文变体加了词边界、中文变体默认 medium(需 ±8 字内强上下文命中 1 个或弱上下文命中 ≥2 个才订正)、易被正常词包含的变体另配 `guards` 邻接保护,「不可能独立成词」的安全变体经 `high_variants` 白名单升级,但仍可能有个别误判。
|
|
562
|
+
|
|
563
|
+
### 真实字幕上的实测(这条最重要)
|
|
564
|
+
|
|
565
|
+
上面的召回率是**人造扰入口径**:把文中本来就写对的术语替换成变体,再测能否改回来。
|
|
566
|
+
它回答「给定这个错法能不能改对」,**不等于真实字幕上的覆盖率**。
|
|
567
|
+
|
|
568
|
+
拿「同一视频的人工字幕 vs AI 字幕」当参照系测才是真值(`tools/eval_against_human.py`)。
|
|
569
|
+
13 个同时有两条轨的视频,剔除人工字幕与 AI 内容不成比例的 4 个,剩 9 个 / 62,231 字:
|
|
570
|
+
|
|
571
|
+
| 指标 | 实测 |
|
|
572
|
+
|---|---|
|
|
573
|
+
| 字错率(含大小写、数字风格差异) | **8.47%** |
|
|
574
|
+
| 真实错误(中文同音 + 英文错拼 + 漏句) | **2.35%** |
|
|
575
|
+
| 其中「AI 整句漏掉」占 | **47%** |
|
|
576
|
+
| 中文标点密度 | 103 万字里 **14 处** |
|
|
577
|
+
| 本工具净修正 | **0 字** |
|
|
578
|
+
|
|
579
|
+
三个必须承认的结论:
|
|
580
|
+
|
|
581
|
+
1. **预先枚举变体不可扩展**。B站把 `Git` 写成「盖特」、`Docker` 写成「多克/刀客」——
|
|
582
|
+
同一术语的音译写法是开放集合,词表永远收不全。13 个视频里一处都没触发,正是因为变体没收录。
|
|
583
|
+
2. **词表方向错了**。真实错误主体是**中文技术术语的同音错**:`基类→鸡肋`(单个视频里 50 次)、
|
|
584
|
+
`友元→有缘`、`遍历→便利`、`键值对→剑指队`。此前收的却是英文术语的中文音译。
|
|
585
|
+
3. **最大的问题不是错字**。中文同音错里 170/290 是单字(`它/他`、`作/做`、`再/在`),
|
|
586
|
+
歧义大到人类校对标准都不统一;再加上整句漏失和零标点,这些**规则方案处理不了**。
|
|
587
|
+
|
|
588
|
+
对照实验:同一段 131 字字幕,规则订正器 0 处改动(CER 3.8% 原样),
|
|
589
|
+
语言模型修正后 CER 0%。
|
|
590
|
+
|
|
591
|
+
所以本项目的定位是**高置信、可解释、可离线的那一小块**(术语与专名),
|
|
592
|
+
而不是「字幕纠错」。要覆盖标点、同音词、漏句,需要语言模型。
|
|
593
|
+
|
|
594
|
+
两份测量报告在 [`docs/`](./docs):[召回基准报告](./docs/召回基准报告.md)(怎么测、四个反证实验为什么被否掉)、
|
|
595
|
+
[真实字幕错误分析](./docs/真实字幕错误分析.md)(真实错误长什么样、为什么规则方案够不着)。
|
|
596
|
+
|
|
597
|
+
- **交叉验证基于 token 级对齐**。它衡量「多引擎是否一致」,而非「是否正确」——两个引擎可能同时错成同一个词。它回答的是「这段可不可信」,不是「这段绝对正确」。
|
|
598
|
+
- **质量评分是启发式的**,用于同一段内容的横向对比和快速筛查,不是绝对标准。
|
|
599
|
+
- **fetch 依赖外部服务**。字幕轨来自 B站 API(可能因风控或视频设置拿不到);无字幕时的必剪 ASR 需要 `yt-dlp` 下载音频并把音轨上传至 B站服务器。二者任一失败都会给出明确报错,而非静默返回空文本。
|