@pls-compile-owo/fisheye 0.3.0 → 0.7.1
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/CHANGELOG.md +69 -0
- package/README.md +62 -1
- package/fisheye.mjs +88 -39
- package/lib/audio.mjs +646 -0
- package/lib/batch.mjs +191 -0
- package/lib/bili.mjs +86 -30
- package/lib/budget.mjs +117 -0
- package/lib/env.mjs +1 -0
- package/lib/frame.mjs +1 -1
- package/lib/i18n.mjs +147 -0
- package/lib/ocr.mjs +386 -0
- package/lib/progress.mjs +140 -0
- package/lib/resume.mjs +378 -0
- package/lib/retry.mjs +209 -0
- package/lib/stages.mjs +317 -32
- package/lib/util.mjs +9 -8
- package/lib/ytdlp.mjs +393 -0
- package/mcp-video-server.js +261 -0
- package/package.json +3 -2
- package/test/audio.test.mjs +23 -0
- package/test/batch.test.mjs +212 -0
- package/test/bili.test.mjs +70 -0
- package/test/budget.test.mjs +63 -0
- package/test/env.test.mjs +35 -0
- package/test/frame.test.mjs +35 -0
- package/test/i18n.test.mjs +45 -0
- package/test/ocr-sample.png +0 -0
- package/test/ocr.test.mjs +89 -0
- package/test/progress.test.mjs +139 -0
- package/test/resume.test.mjs +232 -0
- package/test/retry.test.mjs +69 -0
- package/test/ytdlp.test.mjs +179 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,75 @@
|
|
|
2
2
|
|
|
3
3
|
本项目遵循 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/) 规范与 [语义化版本](https://semver.org/lang/zh-CN/)。
|
|
4
4
|
|
|
5
|
+
## [0.7.1] - 2026-11-14
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- `auto --resume`:断点续跑完整版——manifest 与计划一致且拼图齐全时**跳过抽帧**,直接复用已有拼图继续后续阶段(`--force` 强制重跑)。
|
|
10
|
+
|
|
11
|
+
## [0.7.0] - 2026-11-14
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- `batch <清单文件|条目...> [--concurrency N]` 子命令:一次处理多个 BV/本地文件/URL(`lib/batch.mjs`)。
|
|
16
|
+
- 字幕翻译:外文视频的官方字幕会同时读取 B站 翻译轨(`translated` 字段,落盘 `<BV>-official-subs-translated.txt`)。
|
|
17
|
+
- 断点续跑基础(`lib/resume.mjs`):manifest 记录计划/阶段,重跑时提示可复用产物(`--force` 强重跑)。
|
|
18
|
+
- 进度辅助(`lib/progress.mjs`):分段抽帧进度、阶段计时。
|
|
19
|
+
- MCP server 入包(`mcp-video-server.js`,可选组件,需自装 @modelcontextprotocol/sdk / zod)+ 中英 README + CONFIG 文档。
|
|
20
|
+
|
|
21
|
+
## [0.6.0] - 2026-11-14
|
|
22
|
+
|
|
23
|
+
### Added
|
|
24
|
+
|
|
25
|
+
- OCR 画面文字识别:`ocr <图片路径>` 子命令(tesseract,可选);selftest 显示 OCR 可用性(`lib/ocr.mjs`)。
|
|
26
|
+
- 任意平台视频:`url <视频链接>` 子命令(yt-dlp,可选,下载后走本地视频流程)(`lib/ytdlp.mjs`)。
|
|
27
|
+
- `clean [--target cache|out|all] [--yes]` 子命令:清理缓存/产物(dry-run 默认,--yes 才删)。
|
|
28
|
+
- `auto --audio`:无官方字幕时自动用 whisper 转写台词,跳过烧录字幕带。
|
|
29
|
+
|
|
30
|
+
### Changed
|
|
31
|
+
|
|
32
|
+
- selftest 增加 OCR / yt-dlp 检查项。
|
|
33
|
+
|
|
34
|
+
## [0.5.0] - 2026-11-14
|
|
35
|
+
|
|
36
|
+
### Added
|
|
37
|
+
|
|
38
|
+
- `transcribe <BV> [--lang zh]` 子命令:用 whisper.cpp 转写音频,文本落 `out/<BV>-audio-transcript.txt`。
|
|
39
|
+
- `lib/audio.mjs` 自动探测 whisper.cpp 官方 Windows 包解压结构(`whisper/Release/`)。
|
|
40
|
+
- 单元测试覆盖扩展到全部模块:retry / budget / i18n / frame / env / audio / bili(共 46 个用例)。
|
|
41
|
+
- 外部自检脚本增加「模块导入冒烟」(10 个 lib 模块全部可加载)。
|
|
42
|
+
|
|
43
|
+
## [0.4.2] - 2026-11-14
|
|
44
|
+
|
|
45
|
+
### Fixed
|
|
46
|
+
|
|
47
|
+
- `lib/env.mjs` 缺少 `execFileSync` 导入:`onPath()` 失败导致 PATH 上的 ffmpeg/python 永远无法被识别(影响 `resolveTool` 的 PATH 兜底)。
|
|
48
|
+
- `selftest`:PATH 上的 python 无法执行(如 Windows 商店占位符)时降级为跳过而不是失败。
|
|
49
|
+
|
|
50
|
+
## [0.4.1] - 2026-11-14
|
|
51
|
+
|
|
52
|
+
### Added
|
|
53
|
+
|
|
54
|
+
- 音频转写能力探测(`lib/audio.mjs`):检测 whisper.cpp,`selftest` 显示音频转写是否可用;不可用时优雅提示安装方式。
|
|
55
|
+
- README 环境变量表补充 `FISHEYE_WHISPER` / `FISHEYE_WHISPER_MODEL`。
|
|
56
|
+
|
|
57
|
+
### Fixed
|
|
58
|
+
|
|
59
|
+
- 首次运行(`out/`、`cache/` 目录不存在)时字幕缓存写入与视频下载不再报 ENOENT。
|
|
60
|
+
|
|
61
|
+
## [0.4.0] - 2026-11-14
|
|
62
|
+
|
|
63
|
+
### Added
|
|
64
|
+
|
|
65
|
+
- B站 API 请求自动重试:网络错误 / 超时 / 5xx / 429 自动重试,指数退避 + 抖动;4xx 不重试(`lib/retry.mjs`)。
|
|
66
|
+
- token 预算估算可配置:`FISHEYE_TOKENS_PER_GRID` / `FISHEYE_TOKENS_PER_SUBS` / `FISHEYE_TOKENS_PER_FINE` 覆盖单张成本,`FISHEYE_TOKEN_MODEL`(gpt / qwen / gemini / 数字)按模型 tokenizer 修正(`lib/budget.mjs`)。
|
|
67
|
+
- CLI 中英双语:`FISHEYE_LANG=en` 输出英文,默认中文(`lib/i18n.mjs`)。
|
|
68
|
+
|
|
69
|
+
### Changed
|
|
70
|
+
|
|
71
|
+
- `lib/bili.mjs` 的请求统一走 `fetchWithRetry`(函数签名与返回值不变)。
|
|
72
|
+
- `lib/stages.mjs` 的预算估算改用 `lib/budget.mjs`(默认值与旧 COST 一致)。
|
|
73
|
+
|
|
5
74
|
## [0.3.0] - 2026-11-14
|
|
6
75
|
|
|
7
76
|
### Changed
|
package/README.md
CHANGED
|
@@ -68,6 +68,18 @@ npm link # 可选:装成全局命令
|
|
|
68
68
|
|
|
69
69
|
> ⚠️ 这个文件已被 `.gitignore` 排除,**永远不要提交**。
|
|
70
70
|
|
|
71
|
+
### 环境变量
|
|
72
|
+
|
|
73
|
+
| 变量 | 说明 |
|
|
74
|
+
|---|---|
|
|
75
|
+
| `FISHEYE_LANG` | CLI 输出语言:`zh`(默认)/ `en` |
|
|
76
|
+
| `FISHEYE_TOKENS_PER_GRID` / `FISHEYE_TOKENS_PER_SUBS` / `FISHEYE_TOKENS_PER_FINE` | 单张拼图 / 字幕带 / 精读帧的 token 估算覆盖(默认 1200 / 1000 / 1300) |
|
|
77
|
+
| `FISHEYE_TOKEN_MODEL` | 按模型 tokenizer 修正估算:`gpt`(1.0)/ `qwen`(1.35)/ `gemini`(0.8)/ 数字 |
|
|
78
|
+
| `FISHEYE_WHISPER` | whisper.cpp 可执行文件路径(音频转写,可选;不装则跳过) |
|
|
79
|
+
| `FISHEYE_WHISPER_MODEL` | whisper 模型文件(ggml-*.bin,可选) |
|
|
80
|
+
| `FISHEYE_HOME` | 数据目录(cache/out 位置,默认项目目录) |
|
|
81
|
+
| `FFMPEG` / `PYTHON` / `BILI_CRED` | 工具与登录态路径覆盖 |
|
|
82
|
+
|
|
71
83
|
### 依赖路径(一般不用管)
|
|
72
84
|
|
|
73
85
|
fisheye 按这个顺序找 ffmpeg / python / 凭证:
|
|
@@ -147,6 +159,15 @@ node fisheye.mjs grab BV1ziWP6qENy 82.5,145.0
|
|
|
147
159
|
00:27 弹幕热区 —— 弹幕热区 19 条
|
|
148
160
|
```
|
|
149
161
|
|
|
162
|
+
## 断点续跑
|
|
163
|
+
|
|
164
|
+
长视频处理中断后,重新跑同样的命令会提示可复用产物:
|
|
165
|
+
|
|
166
|
+
```bash
|
|
167
|
+
node fisheye.mjs auto BV1xxx --budget 20000 --resume # 跳过已抽帧阶段,复用拼图
|
|
168
|
+
node fisheye.mjs auto BV1xxx --budget 20000 --force # 强制全部重跑
|
|
169
|
+
```
|
|
170
|
+
|
|
150
171
|
## 命令
|
|
151
172
|
|
|
152
173
|
| 命令 | 作用 |
|
|
@@ -193,6 +214,46 @@ node fisheye.mjs local <视频文件> [--grid 3x3] [--from 30] [--to 90]
|
|
|
193
214
|
不依赖 B站 登录与字幕:ffmpeg 解析时长 → 按 grid 等间隔抽帧拼图 → 输出 `SHEET: <拼图路径>`。
|
|
194
215
|
把拼图路径交给模型 `read_image` 即可"看"本地视频。适合 mp4/mkv/avi/mov/webm。
|
|
195
216
|
|
|
217
|
+
## 音频转写
|
|
218
|
+
|
|
219
|
+
装了 whisper.cpp 后,可以给无字幕视频做语音转写:
|
|
220
|
+
|
|
221
|
+
```bash
|
|
222
|
+
node fisheye.mjs transcribe BV1xxx [--lang zh]
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
自动探测顺序:`FISHEYE_WHISPER` 环境变量 → PATH → 常见安装路径(含 `~/.dsh/surfcore/whisper/Release/`)。
|
|
226
|
+
模型放 `~/.dsh/surfcore/whisper/`(`ggml-*.bin`)或用 `FISHEYE_WHISPER_MODEL` 指定。
|
|
227
|
+
转写文本落 `out/<BV>-audio-transcript.txt`。
|
|
228
|
+
|
|
229
|
+
## OCR(图片文字识别)
|
|
230
|
+
|
|
231
|
+
装了 tesseract 后可以识别图片/画面里的文字:
|
|
232
|
+
|
|
233
|
+
```bash
|
|
234
|
+
node fisheye.mjs ocr screenshot.png [--lang chi_sim+eng]
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
自动探测 `FISHEYE_TESSERACT` → PATH → 常见安装路径;语言包缺 `chi_sim` 时自动降级到 `eng`。
|
|
238
|
+
识别结果落 `out/<图片名>.ocr.txt`。
|
|
239
|
+
|
|
240
|
+
## 任意平台视频(yt-dlp)
|
|
241
|
+
|
|
242
|
+
装了 yt-dlp 后可以直接给任意平台(YouTube/抖音/Twitter…)的链接:
|
|
243
|
+
|
|
244
|
+
```bash
|
|
245
|
+
node fisheye.mjs url "https://www.youtube.com/watch?v=xxx"
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
下载(yt-dlp)→ 自动走本地视频抽帧流程。探测顺序:`FISHEYE_YTDLP` → PATH → 常见安装路径。
|
|
249
|
+
|
|
250
|
+
## 清理
|
|
251
|
+
|
|
252
|
+
```bash
|
|
253
|
+
node fisheye.mjs clean # 预览将清理的缓存/产物(dry-run)
|
|
254
|
+
node fisheye.mjs clean --target out --yes # 实际删除 out/
|
|
255
|
+
```
|
|
256
|
+
|
|
196
257
|
## 它怎么工作
|
|
197
258
|
|
|
198
259
|
```
|
|
@@ -240,7 +301,7 @@ B 站的弹幕密度本身就是内容信号。实测一条 40 分钟视频:
|
|
|
240
301
|
## 开发
|
|
241
302
|
|
|
242
303
|
```bash
|
|
243
|
-
node --test test/ #
|
|
304
|
+
node --test test/ # 单元测试(全部模块:46 个用例)
|
|
244
305
|
node test-danmaku-filter.js # 弹幕过滤规则测试(51 条用例)
|
|
245
306
|
node fisheye.mjs selftest --deep # 含抽帧/字幕带实测的自检
|
|
246
307
|
```
|
package/fisheye.mjs
CHANGED
|
@@ -15,40 +15,30 @@
|
|
|
15
15
|
// · 拼图每格顺序固定,manifest 按「从左到右、从上到下」记录时间,供 agent 对照
|
|
16
16
|
// · 拼图与精读用同一种 seek,保证同一时间戳取到同一帧
|
|
17
17
|
// · 所有子进程用 stdio:'ignore'(沙箱禁管道捕获),数据走临时文件
|
|
18
|
+
import fs from 'node:fs'
|
|
18
19
|
import path from 'node:path'
|
|
19
20
|
import { PKG_DIR, WS, CFG_FILE, FFMPEG, CRED } from './lib/env.mjs'
|
|
20
21
|
import { parseTime, num, pageNum } from './lib/util.mjs'
|
|
21
|
-
import {
|
|
22
|
+
import { t } from './lib/i18n.mjs'
|
|
23
|
+
import { stageAuto, stageGrid, stageSubs, stageSelftest, stageRefine, stageGrab, stageLocal, stageTranscribe, stageClean, stageOcr, stageUrl, stageBatch } from './lib/stages.mjs'
|
|
22
24
|
|
|
23
25
|
const IS_SELFTEST = process.argv[2] === 'selftest'
|
|
24
26
|
|
|
25
27
|
// local 处理本地文件,不需要 B站 登录态(但仍然需要 ffmpeg)
|
|
26
28
|
const IS_LOCAL = process.argv[2] === 'local'
|
|
29
|
+
// clean / ocr 只动本地文件,不需要 ffmpeg 与 B站 凭证
|
|
30
|
+
const IS_CLEAN = process.argv[2] === 'clean'
|
|
31
|
+
const IS_OCR = process.argv[2] === 'ocr'
|
|
32
|
+
// url 需要 yt-dlp + ffmpeg,但不需要 B站 凭证
|
|
33
|
+
const IS_URL = process.argv[2] === 'url'
|
|
27
34
|
|
|
28
|
-
if (!IS_SELFTEST) {
|
|
35
|
+
if (!IS_SELFTEST && !IS_CLEAN && !IS_OCR) {
|
|
29
36
|
if (!FFMPEG) {
|
|
30
|
-
console.error(
|
|
31
|
-
|
|
32
|
-
安装方式(任选其一):
|
|
33
|
-
1. 装到 PATH:https://ffmpeg.org/download.html (Windows 用 gyan.dev 的 build)
|
|
34
|
-
2. 放项目里:把 ffmpeg 可执行文件放到 ${path.join(PKG_DIR, 'bin')}/
|
|
35
|
-
3. 设环境变量:FFMPEG=<ffmpeg 完整路径>
|
|
36
|
-
4. 写配置:${CFG_FILE} 里加 {"ffmpeg": "<完整路径>"}`)
|
|
37
|
+
console.error(t('err.ffmpeg.missing', { binDir: path.join(PKG_DIR, 'bin'), cfgFile: CFG_FILE }))
|
|
37
38
|
process.exit(2)
|
|
38
39
|
}
|
|
39
|
-
if (!IS_LOCAL && !CRED) {
|
|
40
|
-
console.error(
|
|
41
|
-
|
|
42
|
-
创建 ${path.join(WS, 'credentials.json')},内容:
|
|
43
|
-
{
|
|
44
|
-
"sessdata": "<B站 SESSDATA>",
|
|
45
|
-
"bili_jct": "<bili_jct>",
|
|
46
|
-
"buvid3": "<buvid3>"
|
|
47
|
-
}
|
|
48
|
-
获取方法:浏览器登录 B站 → F12 → Application → Cookies → bilibili.com,
|
|
49
|
-
复制这三个值。或用环境变量 BILI_CRED=<文件路径> 指向已有文件。
|
|
50
|
-
|
|
51
|
-
(也可以只跑 selftest 先看环境是否就绪:node fisheye.mjs selftest)`)
|
|
40
|
+
if (!IS_LOCAL && !IS_CLEAN && !IS_OCR && !IS_URL && !CRED) {
|
|
41
|
+
console.error(t('err.cred.missing', { credPath: path.join(WS, 'credentials.json') }))
|
|
52
42
|
process.exit(2)
|
|
53
43
|
}
|
|
54
44
|
}
|
|
@@ -71,7 +61,7 @@ const opt = (name, dflt) => {
|
|
|
71
61
|
if (stage === 'local') {
|
|
72
62
|
const fileArg = argv[1]
|
|
73
63
|
if (!fileArg || fileArg.startsWith('--')) {
|
|
74
|
-
console.error('
|
|
64
|
+
console.error(t('err.local.usage'))
|
|
75
65
|
process.exit(2)
|
|
76
66
|
}
|
|
77
67
|
try {
|
|
@@ -81,25 +71,77 @@ if (stage === 'local') {
|
|
|
81
71
|
to: parseTime(opt('to', null), 'to')
|
|
82
72
|
})
|
|
83
73
|
} catch (error) {
|
|
84
|
-
console.error(
|
|
74
|
+
console.error(t('err.local.failed', { message: error?.message ?? error }))
|
|
85
75
|
process.exit(1)
|
|
86
76
|
}
|
|
87
77
|
process.exit(0)
|
|
88
78
|
}
|
|
89
|
-
|
|
79
|
+
|
|
80
|
+
if (stage === 'ocr') {
|
|
81
|
+
const img = argv[1]
|
|
82
|
+
if (!img || img.startsWith('--')) {
|
|
83
|
+
console.error(t('err.ocr.usage'))
|
|
84
|
+
process.exit(2)
|
|
85
|
+
}
|
|
86
|
+
try {
|
|
87
|
+
await stageOcr(img, { lang: opt('lang', null) })
|
|
88
|
+
} catch (error) {
|
|
89
|
+
console.error(t('err.local.failed', { message: error?.message ?? error }))
|
|
90
|
+
process.exit(1)
|
|
91
|
+
}
|
|
92
|
+
process.exit(0)
|
|
93
|
+
}
|
|
94
|
+
if (stage === 'url') {
|
|
95
|
+
const link = argv[1]
|
|
96
|
+
if (!link || link.startsWith('--')) {
|
|
97
|
+
console.error(t('err.url.usage'))
|
|
98
|
+
process.exit(2)
|
|
99
|
+
}
|
|
100
|
+
try {
|
|
101
|
+
await stageUrl(link, {
|
|
102
|
+
grid: opt('grid', '3x3'),
|
|
103
|
+
from: parseTime(opt('from', null), 'from'),
|
|
104
|
+
to: parseTime(opt('to', null), 'to')
|
|
105
|
+
})
|
|
106
|
+
} catch (error) {
|
|
107
|
+
console.error(t('err.local.failed', { message: error?.message ?? error }))
|
|
108
|
+
process.exit(1)
|
|
109
|
+
}
|
|
110
|
+
process.exit(0)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
if (stage === 'batch') {
|
|
115
|
+
const arg = argv[1]
|
|
116
|
+
const listText = (arg && !arg.startsWith('--') && fs.existsSync(arg))
|
|
117
|
+
? fs.readFileSync(arg, 'utf8')
|
|
118
|
+
: argv.slice(1).filter((a) => !a.startsWith('--')).join('\n')
|
|
119
|
+
if (!listText.trim()) {
|
|
120
|
+
console.error(t('err.batch.usage'))
|
|
121
|
+
process.exit(2)
|
|
122
|
+
}
|
|
123
|
+
try {
|
|
124
|
+
await stageBatch(listText, {
|
|
125
|
+
concurrency: num(opt('concurrency', 1), 'concurrency', { min: 1, max: 8 }),
|
|
126
|
+
budget: opt('budget', null) ? num(opt('budget'), 'budget', { min: 2000, max: 200000 }) : 20000,
|
|
127
|
+
grid: opt('grid', '3x3'),
|
|
128
|
+
audio: argv.includes('--audio'),
|
|
129
|
+
lang: opt('lang', null),
|
|
130
|
+
from: parseTime(opt('from', null), 'from'),
|
|
131
|
+
to: parseTime(opt('to', null), 'to')
|
|
132
|
+
})
|
|
133
|
+
} catch (error) {
|
|
134
|
+
console.error(t('err.local.failed', { message: error?.message ?? error }))
|
|
135
|
+
process.exit(1)
|
|
136
|
+
}
|
|
137
|
+
process.exit(0)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (!stage || (stage !== 'selftest' && stage !== 'clean' && stage !== 'ocr' && stage !== 'url' && stage !== 'batch' && !bvid)) {
|
|
90
141
|
if (rawBvid && stage !== 'selftest') {
|
|
91
|
-
console.error(
|
|
142
|
+
console.error(`${t('err.bvFormat', { raw: rawBvid })}\n`)
|
|
92
143
|
}
|
|
93
|
-
console.log('
|
|
94
|
-
console.log(' node fisheye.mjs auto <BV> [--budget 20000] [--density 6] ← 推荐:自动规划并铺网')
|
|
95
|
-
console.log(' node fisheye.mjs refine <BV> --template ← 生成精读决策模板')
|
|
96
|
-
console.log(' node fisheye.mjs refine <BV> --decisions <file.json> ← 按模型判断精读')
|
|
97
|
-
console.log(' node fisheye.mjs refine <BV> ← 几何兜底(零 token)')
|
|
98
|
-
console.log(' node fisheye.mjs grid <BV> [--chunk 60] [--grid 3x3]')
|
|
99
|
-
console.log(' node fisheye.mjs subs <BV> [--count 12] [--band auto|0.22]')
|
|
100
|
-
console.log(' node fisheye.mjs grab <BV> <t1,t2,t3> ← 手动指定时间点')
|
|
101
|
-
console.log(' node fisheye.mjs local <视频文件> [--grid 3x3] [--from t] [--to t] ← 看本地视频(mp4/mkv/...)')
|
|
102
|
-
console.log(' node fisheye.mjs selftest [--deep] ← 自检(依赖/凭证/API/缓存/磁盘)')
|
|
144
|
+
console.log(t('usage'))
|
|
103
145
|
process.exit(2)
|
|
104
146
|
}
|
|
105
147
|
// 顶层错误兜底:不要把堆栈直接甩给用户
|
|
@@ -119,7 +161,9 @@ try {
|
|
|
119
161
|
await stageAuto(bvid,
|
|
120
162
|
num(opt('budget', 20000), 'budget', { min: 2000, max: 200000 }),
|
|
121
163
|
{ density: d === null ? null : num(d, 'density', { min: 0.5, max: 60 }), page, from, to,
|
|
122
|
-
useDanmakuFrames: !argv.includes('--no-danmaku-frames')
|
|
164
|
+
useDanmakuFrames: !argv.includes('--no-danmaku-frames'),
|
|
165
|
+
audio: argv.includes('--audio'), audioLang: opt('lang', null),
|
|
166
|
+
resume: argv.includes('--resume'), force: argv.includes('--force') })
|
|
123
167
|
} else if (stage === 'refine') {
|
|
124
168
|
await stageRefine(bvid, { template: argv.includes('--template'), decisions: opt('decisions', null) })
|
|
125
169
|
} else if (stage === 'grid') {
|
|
@@ -133,9 +177,14 @@ try {
|
|
|
133
177
|
opt('band', 'auto'),
|
|
134
178
|
{ page, range })
|
|
135
179
|
} else if (stage === 'grab') await stageGrab(bvid, argv[2] ?? '', { page })
|
|
136
|
-
else
|
|
180
|
+
else if (stage === 'clean') {
|
|
181
|
+
// 清理缓存/产物:本地操作,不需要 BV 号
|
|
182
|
+
await stageClean({ target: opt('target', 'all'), yes: argv.includes('--yes') })
|
|
183
|
+
}
|
|
184
|
+
else if (stage === 'transcribe') await stageTranscribe(bvid, { page, lang: opt('lang', null) })
|
|
185
|
+
else { console.error(t('err.unknownStage', { stage })); process.exit(2) }
|
|
137
186
|
} catch (e) {
|
|
138
|
-
console.error(
|
|
187
|
+
console.error(t('err.fatal', { message: e.message }))
|
|
139
188
|
if (process.env.FISHEYE_DEBUG) console.error(e.stack)
|
|
140
189
|
process.exit(1)
|
|
141
190
|
}
|