dsh-bili-watch 0.2.29 → 0.3.0
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.
- package/README.md +54 -23
- package/THIRD_PARTY_NOTICES.md +81 -0
- package/client.js +136 -4
- package/index.js +791 -97
- package/package.json +88 -77
- package/scripts/fetch-deps.ps1 +50 -8
- package/tools/look.mjs +1455 -262
- package/deps/.gitkeep +0 -1
- package/tools/verify.mjs +0 -109
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
> [!IMPORTANT]
|
|
8
8
|
> 面板要重启 DSH 后才出现,插件只在启动时加载。
|
|
9
|
-
> 抽帧类命令(`frames` / `ocr` / `sheet` / `substrip` / `text` 的兜底档)需要 ffmpeg,没有会直接报错,不会静默降级。
|
|
9
|
+
> 抽帧类命令(`peek` 的 5 帧 / `frames` / `ocr` / `sheet` / `substrip` / `text` 的兜底档)需要 ffmpeg,没有会直接报错,不会静默降级。
|
|
10
10
|
> 登录只影响 AI 字幕;其余命令匿名可用。
|
|
11
11
|
|
|
12
12
|
## 安装
|
|
@@ -25,6 +25,8 @@ dsh plugin --profile web add dsh-bili-watch
|
|
|
25
25
|
|
|
26
26
|
插件依赖 DSH 自带的 `@deepseek-ai/dsh-tools`(peer,optional)。profile 里缺这个包时不会报错:`bili_watch` 工具不注册,`/bili` 命令与插件面板照常可用,日志里会打一行提示。
|
|
27
27
|
|
|
28
|
+
0.2.30 起要求 DSH ≥ 0.2.0-rc.2(`engines` 不硬卡,代码层兼容 0.2.0)。
|
|
29
|
+
|
|
28
30
|
插件市场已收录(2026-10-21):在市场里搜 `dsh-bili-watch` 安装。新装插件默认不自动启用,装完到「插件」页把它的开关打开,再重启 DSH;市场里暂时搜不到时,按上面的命令装。
|
|
29
31
|
|
|
30
32
|
### 只当命令行工具
|
|
@@ -43,18 +45,45 @@ dsh plugin --profile web add dsh-bili-watch@latest
|
|
|
43
45
|
dsh plugin --profile web remove dsh-bili-watch
|
|
44
46
|
```
|
|
45
47
|
|
|
46
|
-
升级后重启 DSH。`deps/`
|
|
48
|
+
升级后重启 DSH。`deps/` 在包目录内,卸载或重装会连模型、环境一起删掉(**登录凭证不在 `deps/`**,它已经搬到 `$DSH_HOME/bili-watch/`);把 `BILI_WATCH_SEEN` 指到包外可以保住「已看过」记录。**注意 `BILI_WATCH_PROJ` 只影响运行时查找顺序,不影响 `fetch-deps` 的安装位置**——`fetch-deps` 永远装进包内 `deps/`,而且查找时 `deps/` 优先,所以「指到包外」这一步要真的生效,得先让包内 `deps/.venv-*` 不存在。**抽帧缓存根在包目录外**(默认 `~/.dsh/bili-watch/cache`,即 `$DSH_HOME/bili-watch/cache`;自定义过 `BILI_WATCH_CACHE` 就指到你自己设的位置,插件会在那个目录下再建一层 `bili-watch-cache/` 当缓存根),`dsh plugin remove` 不会动它,卸载后需手动删这个缓存目录。**只删 `cache/` 这一层:登录凭证是它的兄弟文件 `$DSH_HOME/bili-watch/credential.json`,删了要重新扫码。** 日常清理在面板里逐项删即可。
|
|
49
|
+
|
|
50
|
+
## 模块架构
|
|
51
|
+
|
|
52
|
+
```mermaid
|
|
53
|
+
flowchart TD
|
|
54
|
+
subgraph L1["① index.js(插件入口,跑在 DSH 进程内)"]
|
|
55
|
+
DT["defineTool<br/>注册 bili_watch 工具"]
|
|
56
|
+
CR["commands.register<br/>注册 /bili 斜杠命令"]
|
|
57
|
+
WS["webServer 路由<br/>插件面板(状态/扫码登录/删依赖)"]
|
|
58
|
+
end
|
|
59
|
+
subgraph L2["② tools/look.mjs(独立 node 子进程)"]
|
|
60
|
+
LOOK["node tools/look.mjs <cmd>"]
|
|
61
|
+
end
|
|
62
|
+
subgraph L3["③ 外部可执行(可选依赖)"]
|
|
63
|
+
PY["python deps/_bili-asr.py<br/>本地 ASR 转写(可选)"]
|
|
64
|
+
FF["ffmpeg<br/>抽帧 / 取音轨(可选)"]
|
|
65
|
+
YT["yt-dlp<br/>通用 URL 下载(可选)"]
|
|
66
|
+
end
|
|
67
|
+
DT -->|spawn| LOOK
|
|
68
|
+
CR -->|spawn| LOOK
|
|
69
|
+
WS -.->|面板扫码直写凭证,内容命令仍 spawn| LOOK
|
|
70
|
+
LOOK -->|spawn| PY
|
|
71
|
+
LOOK -->|spawn| FF
|
|
72
|
+
LOOK -->|spawn| YT
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
所有内容命令(search/hot/peek/view/sub/asr/frames/ocr/...)最终都走 `tools/look.mjs` 这一份实现;`index.js` 只负责把 DSH 的工具/命令/面板入口接到同一个 spawn 上。`deps/_bili-asr.py`、`ffmpeg`、`yt-dlp` 都是可选依赖,没装时对应档位自动降级或报错(见下「依赖」)。
|
|
47
76
|
|
|
48
77
|
## 插件面板
|
|
49
78
|
|
|
50
79
|
装进 DSH 后,打开插件列表,点 `dsh-bili-watch` 卡片进详情页,里面有一块面板(不用先进设置),常规操作无需去对话里敲命令:
|
|
51
80
|
|
|
52
81
|
- **依赖状态**:ffmpeg、yt-dlp、ASR 环境、OCR 环境、转写模型、登录凭证逐项列出是否就绪、实际路径、占用体积。缺的点击行末「一键下载」,后台跑 `scripts/fetch-deps`(体积是估算值,实际以跑完为准)。
|
|
53
|
-
- **扫码登录**:凭证未登录时点「去扫码登录」(已登录是「重新扫码」),面板里弹出二维码,手机 B 站扫完自动刷新。凭证保存在本机
|
|
54
|
-
-
|
|
82
|
+
- **扫码登录**:凭证未登录时点「去扫码登录」(已登录是「重新扫码」),面板里弹出二维码,手机 B 站扫完自动刷新。凭证保存在本机 `$DSH_HOME/bili-watch/credential.json`。
|
|
83
|
+
- **删除**:面板按实际路径判断——yt-dlp、转写模型、登录凭证可直接删;ffmpeg / ASR 环境 / OCR 环境不提供删除按钮(环境删了要重跑 `fetch-deps` 装回来,代价大于省下的空间),旁边标出它实际在哪。
|
|
55
84
|
- **批量**:标题行的「批量下载 / 批量清理」打开勾选清单,默认勾上所有可操作的项,删除前会再确认一次。
|
|
56
85
|
- **抽帧缓存**:显示当前占用与文件数,「立即清除」一键清空;不点则会按规则自动清理——保留天数(默认 7 天)与总量上限(默认 2048 MB),超过则从最旧的删起。规则存在 `deps/cache-config.json`,插件启动和打开面板时各检查一次。
|
|
57
|
-
- **缓存位置**:默认在 DSH
|
|
86
|
+
- **缓存位置**:默认在 DSH 家目录下再往下一层的 `cache/`(`$DSH_HOME/bili-watch/cache`;`$DSH_HOME` 就是 DSH 的家目录,Windows 下通常是用户目录里的 `.dsh`)。登录凭证是它的**兄弟文件** `$DSH_HOME/bili-watch/credential.json`,清缓存时别把上一层一起删。要换位置就设环境变量 `BILI_WATCH_CACHE`,或在 `deps/deps-config.json` 里写 `"cache": "<你的缓存目录>"`(指到一个还不是缓存根的目录时,插件会在里面建 `bili-watch-cache/`,不会直接拿你的目录当缓存根);改完重启 DSH 生效,面板「抽帧缓存 → 位置」会显示当前实际路径。
|
|
58
87
|
- **最近一次调用**:显示命令、时间、成败,用来判断上一次看视频是否卡住。
|
|
59
88
|
|
|
60
89
|
装完第一次重启 DSH,面板才会出现。
|
|
@@ -72,9 +101,9 @@ dsh plugin --profile web remove dsh-bili-watch
|
|
|
72
101
|
|
|
73
102
|
环境:Node.js ≥ 22。装在 DSH 里时 DSH 自带 node,命令行跑也一样。
|
|
74
103
|
|
|
75
|
-
`ffmpeg` 查找顺序:`--ffmpeg` 参数 → `BILI_WATCH_FFMPEG` → 插件 `deps
|
|
104
|
+
`ffmpeg` 查找顺序:`--ffmpeg` 参数 → `BILI_WATCH_FFMPEG` → `deps/deps-config.json` 里的 `ffmpeg` → 插件 `deps/ffmpeg.exe` → PATH。
|
|
76
105
|
|
|
77
|
-
`deps/` 用来放运行期文件:Python venv、模型、yt-dlp
|
|
106
|
+
`deps/` 用来放运行期文件:Python venv、模型、yt-dlp,默认不进 git(见 `.gitignore`)。登录凭证**不在** `deps/`,见上文的 `$DSH_HOME/bili-watch/credential.json`。ASR 和 OCR 的 Python 环境是可选的,不装也能用,只是少这两档。
|
|
78
107
|
|
|
79
108
|
`deps/_bili-asr.py` 默认从 `hf-mirror.com` 下模型,国内快。海外网络可以先设 `HF_ENDPOINT=https://huggingface.co` 再跑 `fetch-deps`。
|
|
80
109
|
|
|
@@ -112,10 +141,10 @@ macOS/Linux 的 ffmpeg 用系统包管理器装,脚本只给提示。其余目
|
|
|
112
141
|
|
|
113
142
|
只有拿 AI 字幕需要登录,二选一:
|
|
114
143
|
|
|
115
|
-
1. `login`:插件出二维码,手机 B
|
|
144
|
+
1. `login`:插件出二维码,手机 B 站扫码。凭证存在 `$DSH_HOME/bili-watch/credential.json`,不出本机。
|
|
116
145
|
2. `--cred <文件路径>`:读一份现成的 `{sessdata, bili_jct, buvid3, buvid4}` JSON。
|
|
117
146
|
|
|
118
|
-
凭证顺序:给了 `--cred` 就只用它,读不到就按匿名跑;没给 `--cred` 才用
|
|
147
|
+
凭证顺序:给了 `--cred` 就只用它,读不到就按匿名跑;没给 `--cred` 才用 `$DSH_HOME/bili-watch/credential.json`;两者都没有就匿名。**例外:** 本机若同时装有 bili-duty,且它的 `$DSH_HOME/bili-duty/bili_credential.json` 比 watch 自己那份更新,watch 会优先用那一份(同一台机器复用登录态)。因此「在面板里删掉凭证」不等于「立刻变匿名」——bili-duty 那份还在时会继续用它。
|
|
119
148
|
|
|
120
149
|
这份文件等同账号登录态。请勿分享、提交、上传。
|
|
121
150
|
|
|
@@ -123,29 +152,30 @@ macOS/Linux 的 ffmpeg 用系统包管理器装,脚本只给提示。其余目
|
|
|
123
152
|
|
|
124
153
|
| 命令 | 做什么 |
|
|
125
154
|
|---|---|
|
|
126
|
-
| `view <BV或链接或URL>` | 元数据 + 简介 +
|
|
127
|
-
| `peek <BV或链接>` | 元数据 + 热评 + 5
|
|
128
|
-
| `sub <BV或链接> [--format srt
|
|
155
|
+
| `view <BV或链接或URL>` | 元数据 + 简介 + 热评(不下载、不依赖 ffmpeg,永远能跑) |
|
|
156
|
+
| `peek <BV或链接>` | 元数据 + 热评 + 5 张均匀帧(需下载 360p 约每分钟 1 MB + 抽帧约 2 秒;装了 OCR 环境时会顺带识别这 5 帧;仅 B 站) |
|
|
157
|
+
| `sub <BV或链接> [--format srt\|json\|md\|txt] [--out 文件(需配合 --format)]` | AI 字幕 + 两判据(字/秒、关键词重合;时长只用作语速的分母);`--format` 输出 SRT、JSON(带官方时间戳)、Markdown(每条带跳回原片的链接)或纯文本 txt(不带时间戳) |
|
|
129
158
|
| `comments <BV> [N]` | 评论区,默认按热度排序 |
|
|
130
159
|
| `asr <BV或URL>` | 本地转写音频,并给出「可信 / 疑似幻觉」判定(见下节) |
|
|
131
160
|
| `frames <BV或URL> [--step 秒] [--ocr]` | 抽帧;`--ocr` 顺带识别画面文字 |
|
|
132
161
|
| `ocr <BV>` | 抽帧并 OCR |
|
|
133
|
-
| `text <BV或URL> [--format json
|
|
162
|
+
| `text <BV或URL> [--format json\|md]` | 四级链:字幕 → 转写 → 抽帧 OCR;`--format json` 带上时间轴口径(字幕 exact,转写与 OCR 是估算);`md` 有字幕时出时间轴,没有就出标题+全文;SRT 只有 `sub` 支持 |
|
|
134
163
|
| `sheet <BV> [--grid 3x3]` | 均匀挑帧拼成一张图(`NxM` 如 `3x3`、`4x4`,默认 9 帧) |
|
|
135
164
|
| `substrip <BV> [--step 秒]` | 底部字幕竖拼成一张图 |
|
|
136
165
|
| `danmaku <BV>` | 弹幕热区(按时间桶统计峰值) |
|
|
137
|
-
| `
|
|
166
|
+
| `local <文件路径> [--ocr] [--frames]` | 本地视频文件走同一条四级链(字幕层不适用,直接从转写起)。**仅 CLI 可用**,`bili_watch` 工具与 `/bili` 不提供 |
|
|
167
|
+
| `search <关键词> [--days 3] [--limit 10] [--order pubdate\|click]` | 搜视频。`--days` 生效时最多抓 3 页再筛,结果仍按 `--limit` 截断 |
|
|
138
168
|
| `hot [--limit 10]` | 综合热门榜 |
|
|
139
|
-
| `login [--check] [--poll]` |
|
|
140
|
-
| `clean` | 清空缓存根(默认 `$DSH_HOME/bili-watch
|
|
169
|
+
| `login [--check] [--poll] [--key KEY]` | 扫码登录。`--check` = 检查本地凭证是否还有效(不会重新登录);`--key KEY` 需配合 `--poll` 才生效(单独 `login --key X` 会被静默忽略) |
|
|
170
|
+
| `clean` | 清空缓存根(默认 `$DSH_HOME/bili-watch/cache`;只清本插件自己写的 `BV*` 与 `frames/`、`asr/`、`_logs/` 等目录,同一层里别人的文件不动,凭证在上一层不会被碰到)。有任务在跑(存在 `*.lock` 且持有进程还活着)时会拒绝清理并返回非零退出码;进程已被强杀的残留锁不算占用 |
|
|
141
171
|
|
|
142
|
-
|
|
172
|
+
工具侧的等价入口:13 个内容命令在 `bili_watch` 工具、`/bili` 命令、CLI 三处等价(工具参数是 `command` + `bv`,另有 `format` / `out` / `limit` 等字段);`login` 与 `clean` 只有 `/bili` 和 CLI;`local` 只有 CLI。
|
|
143
173
|
|
|
144
|
-
表里只列常用参数。完整参数(`--out
|
|
174
|
+
表里只列常用参数。完整参数(`--out`(**必须配合 `--format`,只给 `--out` 会报错退出**)、`--fresh`、`--scene`、`--ffmpeg`、`--cred`、`--fast`、`--mark-seen`、`--p`、`--asr-max`(转写时长闸门,默认 30 分钟,单位毫秒)等)跑 `bili-watch help` 看。
|
|
145
175
|
|
|
146
176
|
## 单条链接软提示
|
|
147
177
|
|
|
148
|
-
用户的消息里只有一条 B 站视频链接(BV 号或 `b23.tv` 短链)时,插件往消息流里补一句提示,告诉 agent
|
|
178
|
+
用户的消息里只有一条 B 站视频链接(BV 号或 `b23.tv` 短链)时,插件往消息流里补一句提示,告诉 agent 有这条链接可读、用 `peek` 先看一眼(需下载 360p 约每分钟 1 MB + 抽帧约 2 秒)。它**不会**替用户调用工具,也不会自动跑起来;作用只是让「贴链接不说话」这种场景不容易被漏掉。
|
|
149
179
|
|
|
150
180
|
- `b23.tv` 短链会先自动展开:问公共 DNS(`223.5.5.5` → `119.29.29.29` → `1.1.1.1`)拿真实 IP,再直连读 302 里的 BV 号,绕开系统 DNS;展开失败才提示用户换成浏览器地址栏里的完整链接。
|
|
151
181
|
- 为什么要兜底:部分内网 DNS 会把 `b23.tv` 答成 `31.13.x.x`(Facebook 段),这种环境下只有公共 DNS 这条路能展开(2026-10-23 实测)。
|
|
@@ -190,10 +220,10 @@ AI 字幕(需登录)
|
|
|
190
220
|
`text` 会做一次便宜的判断,命中任一条就打一行提示:
|
|
191
221
|
|
|
192
222
|
- 字幕语速不在 2–8.5 字/秒(真人口播的常见区间)
|
|
193
|
-
-
|
|
223
|
+
- 关键词命中率低(字幕和标题/UP 名对不上):不单独触发,要和语速异常同时出现才算补充理由
|
|
194
224
|
- 字幕里自称「实验 / 钓鱼 / 测试」
|
|
195
225
|
|
|
196
|
-
想进一步确认就加 `--verify`:它会抽 5 帧做 OCR
|
|
226
|
+
想进一步确认就加 `--verify`:它会抽 5 帧做 OCR,拿画面文字跟字幕比一遍,报「首帧写着某实验说明」或「画面与字幕几乎不重合」。**注意 `--verify` 只在字幕已被上面三条信号判为可疑时才生效**;字幕正常时它不抽帧、不 OCR,也不会额外提示(行为与不加 `--verify` 完全一样)。
|
|
197
227
|
|
|
198
228
|
```
|
|
199
229
|
node tools/look.mjs text BV1xxxxxxxxx --verify
|
|
@@ -209,7 +239,7 @@ node tools/look.mjs text BV1xxxxxxxxx --verify
|
|
|
209
239
|
| `BILI_WATCH_FFMPEG` | ffmpeg 可执行文件路径 |
|
|
210
240
|
| `BILI_WATCH_YTDLP` | yt-dlp 可执行文件路径 |
|
|
211
241
|
| `BILI_WATCH_SEEN` | `seen.md` 去重文件路径(默认 `deps/seen.md`)。文件不存在时 `--mark-seen` 记不了,先建一个空文件即可 |
|
|
212
|
-
| `BILI_WATCH_CACHE` |
|
|
242
|
+
| `BILI_WATCH_CACHE` | 抽帧缓存目录(默认 `$DSH_HOME/bili-watch/cache`)。优先级:这个变量 > `deps/deps-config.json` 的 `cache` > 默认路径;指到外部目录时插件会在里面建 `bili-watch-cache/` 当缓存根,不直接占用你的目录;目录不可写时面板侧回退到默认路径并写进警告(命令行直接跑 `look.mjs` 没有这层回退,会直接报错) |
|
|
213
243
|
| `BILI_WATCH_MIRROR` | `cn` 时 `scripts/fetch-deps.*` 走镜像下载(gh-proxy / 清华 PyPI / hf-mirror) |
|
|
214
244
|
| `HF_ENDPOINT` | 转写模型的 HuggingFace 端,默认 `https://hf-mirror.com`;海外可设官方站 |
|
|
215
245
|
| `BILI_ASR_MODEL` | 本地转写用的 faster-whisper 模型名,默认 `small` |
|
|
@@ -227,6 +257,7 @@ node tools/look.mjs text BV1xxxxxxxxx --verify
|
|
|
227
257
|
- **凭证**:等同账号登录态,泄露即被盗。只存本机,不进日志。
|
|
228
258
|
- **频率**:脚本没有硬限速(只在字幕重试时随机等几百毫秒);批量跑请自己加 sleep,别连着扫。
|
|
229
259
|
- **任意 http(s) 地址都会被请求**:通用 URL 走 yt-dlp 时不做内网地址过滤,别拿它请求内网服务。
|
|
260
|
+
- **撞名覆盖**:本插件注册 `bili_watch` 工具与 `/bili` 命令;若与其他插件撞名,后注册者静默覆盖,不报错。
|
|
230
261
|
|
|
231
262
|
## 致谢
|
|
232
263
|
|
|
@@ -242,7 +273,7 @@ MIT
|
|
|
242
273
|
|
|
243
274
|
## 长文本保护(2026-10-26)
|
|
244
275
|
|
|
245
|
-
`text` / `sub` / `asr` 的正文超过 **800 字**时,终端只给开头 400 字摘要 + 一行落盘路径,全文写进缓存目录下按视频分的文件夹:`<缓存目录>/<BV
|
|
276
|
+
`text` / `sub` / `asr` 的正文超过 **800 字**时,终端只给开头 400 字摘要 + 一行落盘路径,全文写进缓存目录下按视频分的文件夹:`<缓存目录>/<BV>[_<标题>]/<命令>.txt`。标题段只在先跑过 `view` / `peek` 时才有——直接 `text BVxxx` 建的是纯 `<BV>` 目录,之后再跑 `view` / `peek` 会把它改名成 `<BV>_<标题>`。
|
|
246
277
|
|
|
247
278
|
- `--head N`:自定义摘要长度
|
|
248
279
|
- `--full`:打全文
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# 第三方组件许可声明(THIRD_PARTY_NOTICES)
|
|
2
|
+
|
|
3
|
+
本插件随 npm 包分发的内容,以及由 `scripts/fetch-deps.ps1` 在本机按需安装、**不随本包分发**的第三方组件,其版本与许可如下。
|
|
4
|
+
|
|
5
|
+
版本号均在本机实测对过:随包依赖来自 `node_modules/qrcode/package.json`;本机 venv 依赖来自
|
|
6
|
+
`<本机工作区>\项目\刷B站助手\.venv-asr\Lib\site-packages\*.dist-info\METADATA` 与
|
|
7
|
+
`.venv-ocr\Lib\site-packages\*.dist-info\METADATA` 的 `Name`/`Version`/`License` 字段;
|
|
8
|
+
二进制来自 `ffmpeg -version` 与 `yt-dlp --version`。
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## 一、随 npm 包分发的组件
|
|
13
|
+
|
|
14
|
+
| 组件 | 版本 | 许可证 |
|
|
15
|
+
| --- | --- | --- |
|
|
16
|
+
| qrcode | 1.5.4 | MIT |
|
|
17
|
+
|
|
18
|
+
- **qrcode** 1.5.4 — MIT License。随本包 `dependencies` 一起分发。
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## 二、由 fetch-deps 安装、不随本包分发的组件
|
|
23
|
+
|
|
24
|
+
以下组件由用户运行 `scripts/fetch-deps.ps1` 时按需下载/安装到 `deps\` 下,**不**打包进 npm 发布物。
|
|
25
|
+
其中三个 pip 包在脚本里锁死版本(本机实测能跑):`faster-whisper==1.2.1`、`av==15.1.0`、
|
|
26
|
+
`rapidocr-onnxruntime==1.2.3`;其余 pip 包为其传递依赖,版本以本机实测为准。
|
|
27
|
+
ffmpeg 与 yt-dlp 不锁版本(跟上游走),装完的实际版本记录在 `deps\last-run.json`。
|
|
28
|
+
|
|
29
|
+
### ASR 虚拟环境(deps\.venv-asr)
|
|
30
|
+
|
|
31
|
+
| 组件 | 版本 | 许可证 |
|
|
32
|
+
| --- | --- | --- |
|
|
33
|
+
| faster-whisper | 1.2.1 | MIT |
|
|
34
|
+
| ctranslate2 | 4.8.3 | MIT |
|
|
35
|
+
| av | 15.1.0 | BSD-3-Clause |
|
|
36
|
+
| onnxruntime | 1.31.0 | MIT |
|
|
37
|
+
| numpy | 2.5.4 | BSD-3-Clause(含 0BSD / MIT / Zlib / CC0-1.0 等宽松组件) |
|
|
38
|
+
|
|
39
|
+
- **faster-whisper** 1.2.1 — MIT License。
|
|
40
|
+
- **ctranslate2** 4.8.3 — MIT License(faster-whisper 的推理后端)。
|
|
41
|
+
- **av** 15.1.0(PyAV)— BSD-3-Clause。
|
|
42
|
+
- **onnxruntime** 1.31.0 — MIT License。
|
|
43
|
+
- **numpy** 2.5.4 — 以 BSD-3-Clause 为主,另含 0BSD / MIT / Zlib / CC0-1.0 等宽松许可组件。
|
|
44
|
+
|
|
45
|
+
### OCR 虚拟环境(deps\.venv-ocr)
|
|
46
|
+
|
|
47
|
+
| 组件 | 版本 | 许可证 |
|
|
48
|
+
| --- | --- | --- |
|
|
49
|
+
| rapidocr-onnxruntime | 1.2.3 | Apache-2.0 |
|
|
50
|
+
| opencv-python | 5.0.0.93 | Apache-2.0 |
|
|
51
|
+
| pillow | 13.0.0 | HPND / Pillow License(MIT-CMU) |
|
|
52
|
+
| numpy | 2.5.4 | BSD-3-Clause(含其他宽松组件) |
|
|
53
|
+
| shapely | 2.2.0 | BSD-3-Clause |
|
|
54
|
+
| pyclipper | 1.4.0 | MIT |
|
|
55
|
+
| onnxruntime | 1.31.0 | MIT |
|
|
56
|
+
|
|
57
|
+
- **rapidocr-onnxruntime** 1.2.3 — Apache License 2.0。
|
|
58
|
+
(注:本插件用的是 rapidocr-onnxruntime,**不是** paddleocr;模型与运行时均为 onnxruntime。)
|
|
59
|
+
- **opencv-python** 5.0.0.93 — Apache License 2.0。
|
|
60
|
+
- **pillow** 13.0.0 — HPND(Pillow License,元数据记为 MIT-CMU)。
|
|
61
|
+
- **shapely** 2.2.0 — BSD-3-Clause。
|
|
62
|
+
- **pyclipper** 1.4.0 — MIT License。
|
|
63
|
+
|
|
64
|
+
### 二进制(deps\ffmpeg.exe、deps\ffprobe.exe、deps\yt-dlp.exe)
|
|
65
|
+
|
|
66
|
+
| 组件 | 版本 | 许可证 |
|
|
67
|
+
| --- | --- | --- |
|
|
68
|
+
| ffmpeg(含 ffprobe) | 8.0(BtbN Windows GPL 构建,随上游 latest 不锁版) | GPL v3 |
|
|
69
|
+
| yt-dlp | 随上游 latest 不锁版 | TODO:许可证待确认 |
|
|
70
|
+
|
|
71
|
+
- **ffmpeg / ffprobe** 8.0 — 取自 BtbN/FFmpeg-Builds 的 `win64-gpl` 静态构建,
|
|
72
|
+
构建选项含 `--enable-gpl --enable-version3 --enable-libx264 --enable-libx265`,
|
|
73
|
+
因此该二进制按 **GPL v3** 分发。随本插件使用时须遵守 GPL v3 条款(本插件本体为 MIT,
|
|
74
|
+
与 GPL 二进制的组合按 GPL 条款理解;不随 npm 包分发,仅在用户本机 `deps\` 下存在)。
|
|
75
|
+
- **yt-dlp** — 二进制来自 yt-dlp 官方 release,版本不锁、跟上游走。
|
|
76
|
+
> TODO:本机未能取到 yt-dlp 随包 LICENSE 文本,其许可证(据传为 Unlicense / Public Domain)
|
|
77
|
+
> **待确认**,此处不臆造。下次运行 fetch-deps 后请核对 `yt-dlp --version` 对应的上游 LICENSE。
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
本文件由 dsh-bili-watch 维护;如更新依赖版本,请同步刷新上表与 `scripts/fetch-deps.ps1` 中的锁版号。
|
package/client.js
CHANGED
|
@@ -39,7 +39,7 @@ window.__ModuleLoader__.load({
|
|
|
39
39
|
[data-bl-dot="ok"]{background:#3c9a5f;}
|
|
40
40
|
[data-bl-dot="bad"]{background:#d9534f;}
|
|
41
41
|
[data-bl-name]{min-width:150px;flex:none;}
|
|
42
|
-
[data-bl-detail]{flex:1;min-width:
|
|
42
|
+
[data-bl-detail]{flex:1;min-width:140px;color:var(--dsw-alias-label-tertiary,#8a9099);font-size:12px;word-break:break-all;}
|
|
43
43
|
[data-bl-size]{flex:none;color:var(--dsw-alias-label-tertiary,#8a9099);font-size:12px;min-width:64px;text-align:right;}
|
|
44
44
|
[data-bl-overlay-icon]{font-size:14px;line-height:1;color:#d97706}
|
|
45
45
|
[data-bl-overlay-text]{flex:1;min-width:0;display:flex;flex-direction:column;gap:1px;overflow:hidden}[data-bl-overlay-line]{display:flex;align-items:baseline;gap:8px;overflow:hidden}[data-bl-overlay-hint]{font-size:11.5px;color:#a16207;opacity:.92;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
@@ -50,6 +50,9 @@ window.__ModuleLoader__.load({
|
|
|
50
50
|
[data-bl-btn:disabled]{opacity:.45;cursor:default;}
|
|
51
51
|
[data-bl-input]{width:64px;font-size:12px;padding:2px 6px;border-radius:6px;border:1px solid var(--dsw-alias-border-l2,rgba(255,255,255,.16));background:transparent;color:inherit;}
|
|
52
52
|
[data-bl-note]{font-size:11px;line-height:16px;color:var(--dsw-alias-label-tertiary,#8a9099);}
|
|
53
|
+
/* 2026-10-28:凭证行的「文件:<长路径>」原来挤在同一 flex 行里,把更新时间那一列压到几十像素、
|
|
54
|
+
中文被逐字换行成竖排。让 note 在 flex 行里独占一行(flex-basis:100%),并允许长路径整词断开。 */
|
|
55
|
+
[data-bl-row] [data-bl-note]{flex-basis:100%;min-width:0;word-break:break-all;}
|
|
53
56
|
[data-bl-bad]{color:#d9534f;}
|
|
54
57
|
[data-bl-good]{color:#3c9a5f;}
|
|
55
58
|
[data-bl-qrbox]{display:flex;gap:16px;align-items:center;flex-wrap:wrap;}
|
|
@@ -96,6 +99,19 @@ window.__ModuleLoader__.load({
|
|
|
96
99
|
)
|
|
97
100
|
}
|
|
98
101
|
|
|
102
|
+
// 参数分层(2026-10-28 阶段B):一项 = 输入框 + 默认值提示 + 范围 + 一句话后果 + 恢复默认。
|
|
103
|
+
// 只写 h(),不引新依赖;保存/恢复都由外面传进来的回调走 /config。
|
|
104
|
+
function TuneRow({ label, value, unit, onChange, onSave, onRestore, def, range, hint }) {
|
|
105
|
+
return h('div', { 'data-bl-row': '' },
|
|
106
|
+
h('span', { 'data-bl-name': '' }, label),
|
|
107
|
+
h('span', { 'data-bl-detail': '' },
|
|
108
|
+
h('input', { 'data-bl-input': '', value, onChange: (e) => onChange(e.target.value), inputMode: 'numeric' }),
|
|
109
|
+
unit ? h('span', {}, ' ' + unit) : null,
|
|
110
|
+
h('span', { 'data-bl-note': '' }, `默认 ${def},范围 ${range}。${hint}`)),
|
|
111
|
+
h('button', { 'data-bl-btn': '', onClick: onSave }, '保存'),
|
|
112
|
+
h('button', { 'data-bl-btn': '', onClick: onRestore }, '恢复默认'))
|
|
113
|
+
}
|
|
114
|
+
|
|
99
115
|
function Panel() {
|
|
100
116
|
const [st, setSt] = React.useState(null)
|
|
101
117
|
const [err, setErr] = React.useState('')
|
|
@@ -104,6 +120,14 @@ window.__ModuleLoader__.load({
|
|
|
104
120
|
const [busy, setBusy] = React.useState('')
|
|
105
121
|
const [days, setDays] = React.useState('7')
|
|
106
122
|
const [maxMb, setMaxMb] = React.useState('2048')
|
|
123
|
+
// 参数分层:转写闸门在面板按分钟编辑,deps-config 里存的是秒(asrMaxSec)
|
|
124
|
+
const [asrMin, setAsrMin] = React.useState('30')
|
|
125
|
+
const [frames, setFrames] = React.useState('500')
|
|
126
|
+
const [asrTmo, setAsrTmo] = React.useState('900')
|
|
127
|
+
const [ffTmo, setFfTmo] = React.useState('300')
|
|
128
|
+
const [cmdTmo, setCmdTmo] = React.useState('180')
|
|
129
|
+
const [advOpen, setAdvOpen] = React.useState(false)
|
|
130
|
+
const [tuneMeta, setTuneMeta] = React.useState(null)
|
|
107
131
|
const [qr, setQr] = React.useState(null)
|
|
108
132
|
const [qrState, setQrState] = React.useState('')
|
|
109
133
|
const [dlg, setDlg] = React.useState(null) // 'download' | 'clean'
|
|
@@ -117,6 +141,17 @@ window.__ModuleLoader__.load({
|
|
|
117
141
|
setErr('')
|
|
118
142
|
setDays(String(j.data.cache?.days ?? 7))
|
|
119
143
|
setMaxMb(String(j.data.cache?.maxMb ?? 2048))
|
|
144
|
+
// 参数分层:初值取 /status 的 tuning / tuningDefaults / tuningRanges
|
|
145
|
+
const tv = j.data.tuning || {}
|
|
146
|
+
const td = j.data.tuningDefaults || {}
|
|
147
|
+
const tr = j.data.tuningRanges || {}
|
|
148
|
+
setTuneMeta({ defaults: td, ranges: tr })
|
|
149
|
+
const sec = Number(tv.asrMaxSec ?? td.asrMaxSec ?? 1800)
|
|
150
|
+
setAsrMin(String(Math.round(sec / 60)))
|
|
151
|
+
setFrames(String(tv.maxFrames ?? td.maxFrames ?? 500))
|
|
152
|
+
setAsrTmo(String(tv.asrTimeoutSec ?? td.asrTimeoutSec ?? 900))
|
|
153
|
+
setFfTmo(String(tv.ffmpegTimeoutSec ?? td.ffmpegTimeoutSec ?? 300))
|
|
154
|
+
setCmdTmo(String(tv.cmdTimeoutSec ?? td.cmdTimeoutSec ?? 180))
|
|
120
155
|
} else {
|
|
121
156
|
setErr(j.why || '状态读取失败')
|
|
122
157
|
}
|
|
@@ -215,8 +250,37 @@ window.__ModuleLoader__.load({
|
|
|
215
250
|
|
|
216
251
|
const saveRule = async () => {
|
|
217
252
|
try {
|
|
218
|
-
await jpost('/
|
|
219
|
-
|
|
253
|
+
const j = await jpost('/config', { days: Number(days), maxMb: Number(maxMb) })
|
|
254
|
+
setMsg(j?.ok === false ? ('保存失败:' + (j.why || '未知错误')) : '自动清理规则已保存。')
|
|
255
|
+
} catch (e) {
|
|
256
|
+
// A2(2026-10-27 批次8):原来 catch{} 静默吞,保存失败用户完全无感——现在显式提示
|
|
257
|
+
setMsg('保存失败:' + String(e?.message || e))
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// 参数分层:保存一项(patch 里只带一个键);恢复默认 = POST 该键为 null
|
|
262
|
+
const saveTune = async (patch, okText) => {
|
|
263
|
+
try {
|
|
264
|
+
const j = await jpost('/config', patch)
|
|
265
|
+
if (j?.ok === false) {
|
|
266
|
+
setMsg('保存失败:' + (j.why || '未知错误'))
|
|
267
|
+
return
|
|
268
|
+
}
|
|
269
|
+
setMsg(okText)
|
|
270
|
+
await load()
|
|
271
|
+
} catch (e) {
|
|
272
|
+
setMsg('保存失败:' + String(e?.message || e))
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
const saveAsrMax = () => {
|
|
276
|
+
const m = Number(asrMin)
|
|
277
|
+
const sec = Math.round(m * 60)
|
|
278
|
+
// 服务端 asrMaxSec 范围 300–3600 秒;超界会被静默回落默认值,这里先拦一下免得用户以为存上了
|
|
279
|
+
if (!Number.isFinite(m) || sec < 300 || sec > 3600) {
|
|
280
|
+
setMsg('转写闸门需在 5–60 分钟之间(服务端上限 3600 秒),未保存。')
|
|
281
|
+
return
|
|
282
|
+
}
|
|
283
|
+
saveTune({ asrMaxSec: sec }, `转写闸门已保存(${m} 分钟)。`)
|
|
220
284
|
}
|
|
221
285
|
|
|
222
286
|
const rows = st?.deps || []
|
|
@@ -381,7 +445,7 @@ window.__ModuleLoader__.load({
|
|
|
381
445
|
h('button', { 'data-bl-btn': '', onClick: saveCache }, '立即清除')),
|
|
382
446
|
h('div', { 'data-bl-row': '' },
|
|
383
447
|
h('span', { 'data-bl-name': '' }, '位置'),
|
|
384
|
-
h('span', { 'data-bl-detail': '' }, cache.dir || '(
|
|
448
|
+
h('span', { 'data-bl-detail': '' }, cache.dir || '(未设置)'),
|
|
385
449
|
h('button', {
|
|
386
450
|
'data-bl-btn': '',
|
|
387
451
|
onClick: async () => {
|
|
@@ -410,6 +474,74 @@ window.__ModuleLoader__.load({
|
|
|
410
474
|
h('button', { 'data-bl-btn': '', onClick: saveRule }, '保存规则')),
|
|
411
475
|
h('div', { 'data-bl-note': '' },
|
|
412
476
|
'不点也会按上面的规则自动清(每次打开面板与插件启动时检查一次)。')),
|
|
477
|
+
// 参数分层(阶段B)常规组:不折叠。转写闸门按分钟显示,存的是秒。
|
|
478
|
+
h('div', { 'data-bl-sec': '' },
|
|
479
|
+
h('span', { 'data-bl-sec-title': '' }, '转写与抽帧'),
|
|
480
|
+
h(TuneRow, {
|
|
481
|
+
label: '转写闸门',
|
|
482
|
+
value: asrMin,
|
|
483
|
+
unit: '分钟',
|
|
484
|
+
onChange: setAsrMin,
|
|
485
|
+
onSave: saveAsrMax,
|
|
486
|
+
onRestore: () => saveTune({ asrMaxSec: null }, '转写闸门已恢复默认(30 分钟)。'),
|
|
487
|
+
def: '30 分钟',
|
|
488
|
+
range: `${Math.round((tuneMeta?.ranges?.asrMaxSec?.[0] ?? 300) / 60)}–${Math.round((tuneMeta?.ranges?.asrMaxSec?.[1] ?? 3600) / 60)} 分钟`,
|
|
489
|
+
hint: '低于视频长度时,超长视频只出画面描述、不出字幕。',
|
|
490
|
+
}),
|
|
491
|
+
h(TuneRow, {
|
|
492
|
+
label: '抽帧张数上限',
|
|
493
|
+
value: frames,
|
|
494
|
+
unit: '张',
|
|
495
|
+
onChange: setFrames,
|
|
496
|
+
onSave: () => saveTune({ maxFrames: Number(frames) }, '抽帧张数上限已保存。'),
|
|
497
|
+
onRestore: () => saveTune({ maxFrames: null }, '抽帧张数上限已恢复默认(500 张)。'),
|
|
498
|
+
def: '500 张',
|
|
499
|
+
range: `${tuneMeta?.ranges?.maxFrames?.[0] ?? 100}–${tuneMeta?.ranges?.maxFrames?.[1] ?? 1000} 张`,
|
|
500
|
+
hint: '抽得越多越费时间和磁盘,但关键画面更不容易漏。',
|
|
501
|
+
}),
|
|
502
|
+
h('div', { 'data-bl-row': '' },
|
|
503
|
+
h('button', { 'data-bl-btn': '', onClick: () => setAdvOpen((v) => !v) },
|
|
504
|
+
(advOpen ? '▾ ' : '▸ ') + '高级设置(不建议修改)'))),
|
|
505
|
+
// 高级设置:折叠,默认收起。改错会让长视频静默降级,所以放在折叠区里。
|
|
506
|
+
advOpen
|
|
507
|
+
? h('div', { 'data-bl-sec': '' },
|
|
508
|
+
h('span', { 'data-bl-sec-title': '' }, '高级设置(不建议修改)'),
|
|
509
|
+
h(TuneRow, {
|
|
510
|
+
label: 'ASR 超时',
|
|
511
|
+
value: asrTmo,
|
|
512
|
+
unit: '秒',
|
|
513
|
+
onChange: setAsrTmo,
|
|
514
|
+
onSave: () => saveTune({ asrTimeoutSec: Number(asrTmo) }, 'ASR 超时已保存。'),
|
|
515
|
+
onRestore: () => saveTune({ asrTimeoutSec: null }, 'ASR 超时已恢复默认(900 秒)。'),
|
|
516
|
+
def: '900 秒',
|
|
517
|
+
range: `${tuneMeta?.ranges?.asrTimeoutSec?.[0] ?? 300}–${tuneMeta?.ranges?.asrTimeoutSec?.[1] ?? 3600} 秒`,
|
|
518
|
+
hint: '太短会把长音频判成转写失败。',
|
|
519
|
+
}),
|
|
520
|
+
h(TuneRow, {
|
|
521
|
+
label: 'ffmpeg 超时',
|
|
522
|
+
value: ffTmo,
|
|
523
|
+
unit: '秒',
|
|
524
|
+
onChange: setFfTmo,
|
|
525
|
+
onSave: () => saveTune({ ffmpegTimeoutSec: Number(ffTmo) }, 'ffmpeg 超时已保存。'),
|
|
526
|
+
onRestore: () => saveTune({ ffmpegTimeoutSec: null }, 'ffmpeg 超时已恢复默认(300 秒)。'),
|
|
527
|
+
def: '300 秒',
|
|
528
|
+
range: `${tuneMeta?.ranges?.ffmpegTimeoutSec?.[0] ?? 60}–${tuneMeta?.ranges?.ffmpegTimeoutSec?.[1] ?? 1800} 秒`,
|
|
529
|
+
hint: '太短会让大视频截帧失败。',
|
|
530
|
+
}),
|
|
531
|
+
h(TuneRow, {
|
|
532
|
+
label: '命令超时',
|
|
533
|
+
value: cmdTmo,
|
|
534
|
+
unit: '秒',
|
|
535
|
+
onChange: setCmdTmo,
|
|
536
|
+
onSave: () => saveTune({ cmdTimeoutSec: Number(cmdTmo) }, '命令超时已保存。'),
|
|
537
|
+
onRestore: () => saveTune({ cmdTimeoutSec: null }, '命令超时已恢复默认(180 秒)。'),
|
|
538
|
+
def: '180 秒',
|
|
539
|
+
range: `${tuneMeta?.ranges?.cmdTimeoutSec?.[0] ?? 60}–${tuneMeta?.ranges?.cmdTimeoutSec?.[1] ?? 900} 秒`,
|
|
540
|
+
hint: '太短会让长视频的 text 命令超时。',
|
|
541
|
+
}),
|
|
542
|
+
h('div', { 'data-bl-note': '' },
|
|
543
|
+
'这几项会写进 deps/deps-config.json,改完立即生效,不用重启。「恢复默认」= 把这个键从配置里删掉。'))
|
|
544
|
+
: null,
|
|
413
545
|
h('div', { 'data-bl-sec': '' },
|
|
414
546
|
h('span', { 'data-bl-sec-title': '' }, '最近一次调用'),
|
|
415
547
|
h('div', { 'data-bl-row': '' },
|