video-pipeline 1.2.7 → 1.2.8
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/.env.example +16 -17
- package/CHANGELOG.md +19 -0
- package/README.md +82 -66
- package/package.json +1 -1
- package/process_videos.js +63 -39
package/.env.example
CHANGED
|
@@ -54,12 +54,13 @@ WHISPER_SERVICE=http://localhost:9588 # 【自由】服务地址
|
|
|
54
54
|
WHISPER_TEMPERATURE=0.0 # 【自由】whisper 推理温度 (0.0~1.0, 越低越确定)
|
|
55
55
|
WHISPER_TEMPERATURE_INC=0.2 # 【自由】whisper 温度增量 (fallback 时升温步长)
|
|
56
56
|
WHISPER_RESPONSE_FORMAT=json # 【自由】whisper 返回格式 (json/text/srt/vtt)
|
|
57
|
+
WHISPER_SERVICE_MODEL=models/ggml-base.bin # 【自由】服务端模型文件路径(留空则使用服务器当前模型)
|
|
57
58
|
|
|
58
59
|
# 本地模式 - openai-whisper CLI (取消下方注释并注释上方即可切换)
|
|
59
60
|
# WHISPER_BACKEND=local
|
|
60
61
|
# WHISPER_MODEL=base
|
|
61
62
|
# WHISPER_DEVICE=cpu
|
|
62
|
-
# WHISPER_LANGUAGE=
|
|
63
|
+
# WHISPER_LANGUAGE=
|
|
63
64
|
|
|
64
65
|
# ── 转码参数 ────────────────────────────────────────────────────────────────
|
|
65
66
|
# 【自由】wav 格式参数通常不变;如需其他格式(mp3/flac)可一起修改
|
|
@@ -77,10 +78,10 @@ COL_CONTENT=content # 输出列:识别文本写入此列
|
|
|
77
78
|
COL_KEYWORDS=keywords # 输出列:AI 分析结果(关键词归纳)写入此列
|
|
78
79
|
#
|
|
79
80
|
# 【平台视频 ID 列】至少配置一个,列值会替换 URL 模板中的占位符
|
|
80
|
-
COL_TENCENTVID=extra.
|
|
81
|
-
COL_BILIBILIBVID=extra.
|
|
82
|
-
COL_YOUTUBEID=extra.
|
|
83
|
-
COL_YOUKUID=extra.
|
|
81
|
+
COL_TENCENTVID=extra.tencent
|
|
82
|
+
COL_BILIBILIBVID=extra.bilibili
|
|
83
|
+
COL_YOUTUBEID=extra.youtube
|
|
84
|
+
COL_YOUKUID=extra.youku
|
|
84
85
|
|
|
85
86
|
# ── 处理的 Sheet ────────────────────────────────────────────────────────────
|
|
86
87
|
# 【自由】逗号分隔的 sheet 名称列表,可增删、调序;不指定则处理所有 sheet
|
|
@@ -88,27 +89,27 @@ VIDEO_SHEETS=YouTube视频,普诺赛中文站
|
|
|
88
89
|
|
|
89
90
|
# ── 平台优先级 ──────────────────────────────────────────────────────────────
|
|
90
91
|
# 【调序】可选值来自以下固定集合(与上方平台列名后缀对应):
|
|
91
|
-
#
|
|
92
|
-
#
|
|
93
|
-
#
|
|
94
|
-
#
|
|
92
|
+
# bilibili = B 站 BV 号
|
|
93
|
+
# youtube = YouTube 视频 ID
|
|
94
|
+
# tencent = 腾讯视频 VID
|
|
95
|
+
# youku = 优酷视频 ID
|
|
95
96
|
#
|
|
96
97
|
# 你可以:
|
|
97
98
|
# 1. 调整顺序 → 排前面的平台优先下载
|
|
98
99
|
# 2. 增减条目 → 只保留你需要的平台
|
|
99
100
|
# ✗ 不能使用集合之外的值(如 "tiktokId"、"douyinId" 等未定义的 key)
|
|
100
|
-
PLATFORM_PRIORITY=
|
|
101
|
+
PLATFORM_PRIORITY=bilibili,youtube,tencent,youku
|
|
101
102
|
|
|
102
103
|
# ── 平台 URL 模板 ───────────────────────────────────────────────────────────
|
|
103
104
|
# 【关联】模板中的 {占位符} 必须与 PLATFORM_PRIORITY 中的 key 名一致
|
|
104
|
-
# 如 {
|
|
105
|
+
# 如 {youtube} 会被 COL_YOUTUBEID 列的值替换
|
|
105
106
|
# 【自由】URL 本身可随意修改(例如从 youtu.be 改为 www.youtube.com/watch?v=)
|
|
106
107
|
#
|
|
107
108
|
# 前缀规则: TENCENT=腾讯视频 BILIBILI=B站 YOUTUBE=YouTube YOUKU=优酷
|
|
108
|
-
TENCENT_URL_TPL=https://v.qq.com/x/page/{
|
|
109
|
-
BILIBILI_URL_TPL=https://www.bilibili.com/video/{
|
|
110
|
-
YOUTUBE_URL_TPL=https://youtu.be/{
|
|
111
|
-
YOUKU_URL_TPL=https://v.youku.com/v_show/id_{
|
|
109
|
+
TENCENT_URL_TPL=https://v.qq.com/x/page/{tencent}.html
|
|
110
|
+
BILIBILI_URL_TPL=https://www.bilibili.com/video/{bilibili}/
|
|
111
|
+
YOUTUBE_URL_TPL=https://youtu.be/{youtube}
|
|
112
|
+
YOUKU_URL_TPL=https://v.youku.com/v_show/id_{youku}.html
|
|
112
113
|
|
|
113
114
|
# ── 平台通用配置 ────────────────────────────────────────────────────────────
|
|
114
115
|
# 以下变量对每个平台分别配置,前缀 = 平台前缀(BILIBILI / YOUTUBE / TENCENT / YOUKU)
|
|
@@ -167,8 +168,6 @@ AI_ENABLED=true
|
|
|
167
168
|
AI_API_KEY=your-api-key-here
|
|
168
169
|
AI_BASE_URL=https://your-api-host/v1
|
|
169
170
|
AI_MODEL=your-model-name
|
|
170
|
-
# 【自由】请求超时(秒),默认 300
|
|
171
|
-
AI_TIMEOUT=300
|
|
172
171
|
# 【自由】AI 推理温度 (0.0~2.0, 越低越确定/保守, 越高越随机/创意)
|
|
173
172
|
AI_TEMPERATURE=0.3
|
|
174
173
|
# 【关联】提示词模板,{content} 占位符会被识别文本替换
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.2.8] - 2026-06-11
|
|
4
|
+
|
|
5
|
+
### Features
|
|
6
|
+
|
|
7
|
+
- add colored console output and task separators (PY) (`d983920`)
|
|
8
|
+
- add colored console output and task separators (JS) (`3119a74`)
|
|
9
|
+
|
|
10
|
+
### Bug Fixes
|
|
11
|
+
|
|
12
|
+
- add long-form platform key mappings + upgrade to bright ANSI colors (`f56cb1f`)
|
|
13
|
+
- remove AI_TIMEOUT env, unify platform keys (`142bc37`)
|
|
14
|
+
|
|
15
|
+
### Documentation
|
|
16
|
+
|
|
17
|
+
- update (`82637ea`)
|
|
18
|
+
- add missing WHISPER_SERVICE_MODEL to .env.example (`de1d4d8`)
|
|
19
|
+
- restructure README with progressive layout and updated platform keys (`298f049`)
|
|
20
|
+
|
|
21
|
+
|
|
3
22
|
## [1.2.7] - 2026-06-11
|
|
4
23
|
|
|
5
24
|
### Features
|
package/README.md
CHANGED
|
@@ -3,11 +3,14 @@
|
|
|
3
3
|
基于 `process_videos.js` (Node.js) 或 `process_videos.py` (Python),一键完成:yt-dlp 下载 → ffmpeg 转码 → whisper 识别 → AI 关键词归纳 → 写回 Excel。
|
|
4
4
|
|
|
5
5
|
**五种使用方式,覆盖不同场景:**
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
6
|
+
|
|
7
|
+
| 模式 | 输入 | 跳过步骤 | 适用场景 |
|
|
8
|
+
|------|------|---------|----------|
|
|
9
|
+
| **Excel 批量** | Excel 行(多视频) | — | 批量处理全流程 |
|
|
10
|
+
| **--url 直链** | 单个视频 URL | — | 临时下载单个视频 |
|
|
11
|
+
| **--input 本地** | 本地视频/音频文件 | 下载 | 处理已有文件 |
|
|
12
|
+
| **--content 纯文本** | 文件路径或内联文本 | 下载+转码+识别 | 已有文本直接分析 |
|
|
13
|
+
| **--content-column** | Excel 列的已有文本 | 下载+转码+识别 | 批量分析 Excel 中的文本 |
|
|
11
14
|
|
|
12
15
|
---
|
|
13
16
|
|
|
@@ -41,7 +44,7 @@ pip install pandas openpyxl requests python-dotenv questionary
|
|
|
41
44
|
### 必装工具
|
|
42
45
|
|
|
43
46
|
| 工具 | 版本要求 | 安装方式 | 用途 |
|
|
44
|
-
|
|
47
|
+
|------|-----------|----------|------|
|
|
45
48
|
| Python | 3.9+ | [python.org](https://www.python.org/) | 脚本运行 |
|
|
46
49
|
| yt-dlp | 最新 | `pip install yt-dlp` 或 [GitHub Release](https://github.com/yt-dlp/yt-dlp/releases) | 视频下载 |
|
|
47
50
|
| ffmpeg + ffprobe | 4.0+ | [ffmpeg.org](https://ffmpeg.org/download.html) 或 `winget install ffmpeg` | 音频转码 + 时长检测 |
|
|
@@ -53,7 +56,7 @@ pip install pandas openpyxl requests python-dotenv questionary
|
|
|
53
56
|
YouTube 要求 JS 运行时解开 n-sig 挑战,否则无法提取视频格式。
|
|
54
57
|
|
|
55
58
|
| 方式 | 安装命令 |
|
|
56
|
-
|
|
59
|
+
|------|----------|
|
|
57
60
|
| Node.js(推荐) | [nodejs.org](https://nodejs.org/) 下载 LTS 版,安装后 `node --version` 验证 |
|
|
58
61
|
| Deno | `winget install DenoLand.Deno` 或 [deno.com](https://deno.com/) |
|
|
59
62
|
|
|
@@ -62,9 +65,11 @@ YouTube 要求 JS 运行时解开 n-sig 挑战,否则无法提取视频格式
|
|
|
62
65
|
### Python 依赖
|
|
63
66
|
|
|
64
67
|
```bash
|
|
65
|
-
pip install pandas openpyxl requests python-dotenv
|
|
68
|
+
pip install pandas openpyxl requests python-dotenv questionary
|
|
66
69
|
```
|
|
67
70
|
|
|
71
|
+
> `questionary` 为可选依赖(交互式确认时使用),建议一并安装。
|
|
72
|
+
|
|
68
73
|
### 环境变量配置(.env)
|
|
69
74
|
|
|
70
75
|
**从 v2 开始,所有路径、字段映射、平台参数均通过 `.env` 文件配置。** 这意味着同一套脚本可以直接用于其他 Excel 文件,只需修改 `.env` 中的值即可。
|
|
@@ -83,10 +88,10 @@ cp .env.example .env
|
|
|
83
88
|
|------|------|------|
|
|
84
89
|
| 输入 | `EXCEL_FILE` | Excel 文件路径 |
|
|
85
90
|
| 列映射 | `COL_ID` / `COL_TITLE` / `COL_CONTENT` / `COL_KEYWORDS` | 唯一标识列 / 标题列 / 识别文本输出列 / AI 关键词输出列 |
|
|
86
|
-
| 列映射 | `
|
|
91
|
+
| 列映射 | `COL_TENCENT` / `COL_BILIBILI` / `COL_YOUTUBE` / `COL_YOUKU` | 各平台视频 ID 所在列 |
|
|
87
92
|
| Sheet | `VIDEO_SHEETS` | 逗号分隔需要处理的 sheet(留空则全部) |
|
|
88
93
|
| 平台 | `PLATFORM_PRIORITY` | 平台重试优先级 |
|
|
89
|
-
| 平台 | `{平台}_URL_TPL` | URL 模板(如 `YOUTUBE_URL_TPL=https://youtu.be/{
|
|
94
|
+
| 平台 | `{平台}_URL_TPL` | URL 模板(如 `YOUTUBE_URL_TPL=https://youtu.be/{youtube}`) |
|
|
90
95
|
| 平台 | `{平台}_COOKIES_FROM_BROWSER` | 从浏览器直读 cookie(推荐 Firefox,替代手动导出文件) |
|
|
91
96
|
| 平台 | `{平台}_COOKIE_FILE` | cookie 文件路径(备用方案,需定期更新) |
|
|
92
97
|
| 平台 | `{平台}_PROXY` | 代理地址(如 `http://127.0.0.1:7897`,Clash Verge) |
|
|
@@ -101,7 +106,7 @@ cp .env.example .env
|
|
|
101
106
|
| AI 分析 | `AI_ENABLED` | `true` 启用 / `false` 跳过(默认 true) |
|
|
102
107
|
| AI 分析 | `AI_API_KEY` / `AI_BASE_URL` / `AI_MODEL` | OpenAI 兼容 API 配置 |
|
|
103
108
|
| AI 分析 | `AI_PROMPT_TPL` | 提示词模板,必须包含 `{content}` 占位符 |
|
|
104
|
-
| AI 分析 | `
|
|
109
|
+
| AI 分析 | `AI_TEMPERATURE` | AI 推理温度 (0.0~2.0) |
|
|
105
110
|
|
|
106
111
|
### .env 配置项变更权限
|
|
107
112
|
|
|
@@ -110,11 +115,11 @@ cp .env.example .env
|
|
|
110
115
|
| 标记 | 含义 | 涵盖的配置项 | 示例 |
|
|
111
116
|
|------|------|-------------|------|
|
|
112
117
|
| **【自由】** | 值可随意改为任意合法内容 | 路径、开关、数字、字符串、URL、UA、格式参数等 | `EXCEL_FILE`, `YOUTUBE_PROXY`, `WHISPER_MODEL` |
|
|
113
|
-
| **【调序】** | 只能从固定集合中增减/排序,不能用集合外的值 | `PLATFORM_PRIORITY` | 只能包含 `
|
|
114
|
-
| **【关联】** | 值需与脚本内约定的 Key 名一致 | URL 模板中的 `{占位符}` | `{
|
|
118
|
+
| **【调序】** | 只能从固定集合中增减/排序,不能用集合外的值 | `PLATFORM_PRIORITY` | 只能包含 `bilibili` / `youtube` / `tencent` / `youku` |
|
|
119
|
+
| **【关联】** | 值需与脚本内约定的 Key 名一致 | URL 模板中的 `{占位符}` | `{youtube}` 必须跟 `COL_YOUTUBE` 的后缀一致 |
|
|
115
120
|
| **【固定】** | 除非 Excel 列名或脚本内部逻辑改变,否则不应修改 | 列名映射 | `COL_ID=extra.id`、`COL_TITLE=title` 等 |
|
|
116
121
|
|
|
117
|
-
> **最容易混淆的是【调序】**:`PLATFORM_PRIORITY` 可以调整顺序、增减条目,但只能用脚本已定义的 4 个 key,新增 `
|
|
122
|
+
> **最容易混淆的是【调序】**:`PLATFORM_PRIORITY` 可以调整顺序、增减条目,但只能用脚本已定义的 4 个 key,新增 `tiktok`、`douyin` 等无效 key 会导致脚本无法识别。
|
|
118
123
|
|
|
119
124
|
### Whisper 语音识别
|
|
120
125
|
|
|
@@ -139,9 +144,7 @@ WHISPER_LANGUAGE=zh # 空=多语言自动检测(默认),需要指
|
|
|
139
144
|
```
|
|
140
145
|
脚本会直接调用 `whisper` CLI,无需额外服务进程。
|
|
141
146
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
## 目录结构
|
|
147
|
+
### 目录结构
|
|
145
148
|
|
|
146
149
|
```
|
|
147
150
|
├── process_videos.js # Node.js 主流程脚本(推荐)
|
|
@@ -154,28 +157,32 @@ WHISPER_LANGUAGE=zh # 空=多语言自动检测(默认),需要指
|
|
|
154
157
|
├── cookies/ # 站点 cookie 文件
|
|
155
158
|
│ ├── bilibili.txt # B站 cookie(Netscape 格式)
|
|
156
159
|
│ └── youtube.txt # YouTube cookie 备用(Firefox 直读方案不需要)
|
|
157
|
-
├──
|
|
158
|
-
│ ├──
|
|
159
|
-
│
|
|
160
|
-
|
|
161
|
-
│ ├──
|
|
162
|
-
│
|
|
163
|
-
|
|
164
|
-
│
|
|
165
|
-
│
|
|
166
|
-
│
|
|
167
|
-
│
|
|
168
|
-
│
|
|
169
|
-
│
|
|
170
|
-
│
|
|
171
|
-
│
|
|
172
|
-
│
|
|
173
|
-
│
|
|
174
|
-
│
|
|
175
|
-
│
|
|
176
|
-
│ └──
|
|
177
|
-
│ ├──
|
|
178
|
-
│
|
|
160
|
+
├── output/ # 输出根目录(可通过环境变量覆盖)
|
|
161
|
+
│ ├── downloads/ # yt-dlp 下载输出(mp4)
|
|
162
|
+
│ │ ├── youtube/ # 按平台分目录
|
|
163
|
+
│ │ └── bilibili/
|
|
164
|
+
│ ├── transcoded/ # ffmpeg 转码输出(wav 16kHz mono)
|
|
165
|
+
│ │ ├── youtube/
|
|
166
|
+
│ │ └── bilibili/
|
|
167
|
+
│ └── reports/ # 执行报告(按 sheet/平台分目录)
|
|
168
|
+
│ ├── YouTube视频/
|
|
169
|
+
│ │ ├── report_YYYYMMDD_HHMMSS.json # JSON 报告(机器可读,用于重跑)
|
|
170
|
+
│ │ └── tasks/ # 人类可读文本摘要
|
|
171
|
+
│ │ ├── 2143.txt
|
|
172
|
+
│ │ └── ...
|
|
173
|
+
│ ├── 普诺赛中文站/
|
|
174
|
+
│ │ ├── report_YYYYMMDD_HHMMSS.json
|
|
175
|
+
│ │ └── tasks/
|
|
176
|
+
│ │ └── ...
|
|
177
|
+
│ ├── youtube/ # --url 模式按平台名分目录
|
|
178
|
+
│ │ ├── report_YYYYMMDD_HHMMSS.json
|
|
179
|
+
│ │ └── tasks/
|
|
180
|
+
│ ├── local/ # --input 模式默认目录
|
|
181
|
+
│ │ ├── report_YYYYMMDD_HHMMSS.json
|
|
182
|
+
│ │ └── tasks/
|
|
183
|
+
│ └── content/ # --content 模式固定目录
|
|
184
|
+
│ ├── report_YYYYMMDD_HHMMSS.json
|
|
185
|
+
│ └── tasks/
|
|
179
186
|
├── scripts/ # 辅助脚本
|
|
180
187
|
│ ├── release.js # 版本发布脚本
|
|
181
188
|
│ └── regenerate-changelog.js # CHANGELOG 重建脚本
|
|
@@ -215,9 +222,20 @@ yt-dlp 可直接从 Firefox 浏览器读取 cookie,无需手动导出:
|
|
|
215
222
|
|
|
216
223
|
### B站(bilibili)
|
|
217
224
|
|
|
218
|
-
|
|
225
|
+
**方案 A(推荐):直接从 Firefox 浏览器读 cookie**
|
|
226
|
+
|
|
227
|
+
1. 用 Firefox 浏览器登录 [bilibili.com](https://www.bilibili.com)
|
|
228
|
+
2. 在 `.env` 中设置 `BILIBILI_COOKIES_FROM_BROWSER=firefox`
|
|
229
|
+
3. 脚本自动通过 `--cookies-from-browser firefox` 读取
|
|
230
|
+
|
|
231
|
+
> Firefox cookie 直读方案同样适用于 B站,无需手动导出。
|
|
232
|
+
|
|
233
|
+
**方案 B(备用):从文件读取 cookie**
|
|
234
|
+
|
|
235
|
+
1. Chrome 安装扩展 [Get cookies.txt LOCALLY](https://chromewebstore.google.com/detail/get-cookiestxt-locally/cclelndahbckbenkjhflpdbgdldlbecc)
|
|
219
236
|
2. 访问 [bilibili.com](https://www.bilibili.com) 并登录
|
|
220
237
|
3. 点击扩展图标 → Export → 保存为 `cookies/bilibili.txt`
|
|
238
|
+
4. 在 `.env` 中注释掉 `BILIBILI_COOKIES_FROM_BROWSER`,启用 `BILIBILI_COOKIE_FILE=cookies/bilibili.txt`
|
|
221
239
|
|
|
222
240
|
---
|
|
223
241
|
|
|
@@ -419,7 +437,7 @@ node process_videos.js --content-column "content" --concurrency 2 --retry 2
|
|
|
419
437
|
## 参数说明
|
|
420
438
|
|
|
421
439
|
| 参数 | 类型 | 默认值 | 说明 |
|
|
422
|
-
|
|
440
|
+
|------|------|---------|------|
|
|
423
441
|
| `--sheet <name>` | str | 全部 | 指定 sheet 名称 |
|
|
424
442
|
| `--id <id>` | str | — | 指定 extra.id 或 title(单条测试) |
|
|
425
443
|
| `--offset <n>` | int | 0 | 跳过前 N 条任务(从 0 开始),适合调试大量数据 |
|
|
@@ -449,7 +467,7 @@ node process_videos.js --content-column "content" --concurrency 2 --retry 2
|
|
|
449
467
|
## 重试规则
|
|
450
468
|
|
|
451
469
|
| 可重试 | 不重试 |
|
|
452
|
-
|
|
470
|
+
|----------|----------|
|
|
453
471
|
| 网络超时、连接拒绝 | HTTP 404 / 403 / 401 |
|
|
454
472
|
| yt-dlp 下载中断 | 视频已删除 / 私有 |
|
|
455
473
|
| whisper 服务超时 | 无效 URL、文件不存在 |
|
|
@@ -462,7 +480,7 @@ node process_videos.js --content-column "content" --concurrency 2 --retry 2
|
|
|
462
480
|
脚本默认不会重复处理已有文件,但会在以下情况自动触发重做:
|
|
463
481
|
|
|
464
482
|
| 步骤 | 跳过条件 | 自动重做条件 |
|
|
465
|
-
|
|
483
|
+
|------|-----------|----------------|
|
|
466
484
|
| 下载 | 同名文件已存在(非 `--force`) | `--force` 或文件不存在 |
|
|
467
485
|
| 转码 | WAV 已存在 **且** MP4 时间戳 ≤ WAV 时间戳 | `--force` 或 **MP4 比 WAV 新**(重新下载过) |
|
|
468
486
|
| 识别 | —(每次必跑,覆盖写入 Excel) | — |
|
|
@@ -505,8 +523,8 @@ AI_MODEL=agnes-2.0-flash
|
|
|
505
523
|
# 提示词模板({content} 会被识别文本替换)
|
|
506
524
|
AI_PROMPT_TPL=帮我归纳总结一下Keywords,尽可能全一点,这是内容:{content}
|
|
507
525
|
|
|
508
|
-
#
|
|
509
|
-
AI_TIMEOUT
|
|
526
|
+
# 请求超时(秒,通过 --analyze-timeout 参数设置)
|
|
527
|
+
# 不再使用 AI_TIMEOUT 环境变量(已统一为 --analyze-timeout 参数)
|
|
510
528
|
```
|
|
511
529
|
|
|
512
530
|
### 工作原理
|
|
@@ -544,21 +562,19 @@ node process_videos.js --sheet "YouTube视频" --step analyze --concurrency 2
|
|
|
544
562
|
|--------|------|------|
|
|
545
563
|
| 1 | `{id}` | `2143` |
|
|
546
564
|
| 2 | `{id}_{title}` | `2143_产品介绍` |
|
|
547
|
-
| 3 | `{id}_{title}_{
|
|
565
|
+
| 3 | `{id}_{title}_{platform}` | `2143_产品介绍_bilibili` |
|
|
548
566
|
|
|
549
567
|
> 去重仅在同 sheet 内生效,不同 sheet 之间允许同名文件(存放在不同子目录)。
|
|
550
568
|
|
|
551
569
|
---
|
|
552
570
|
|
|
553
|
-
---
|
|
554
|
-
|
|
555
571
|
## 进度显示
|
|
556
572
|
|
|
557
573
|
执行时会同时展示**总体进度**和**单视频进度**:
|
|
558
574
|
|
|
559
575
|
```text
|
|
560
|
-
[1/91] [2143] 开始处理 (sheet=YouTube视频, platform=
|
|
561
|
-
[2143] 开始下载 (平台=
|
|
576
|
+
[1/91] [2143] 开始处理 (sheet=YouTube视频, platform=youtube, title=xxx)
|
|
577
|
+
[2143] 开始下载 (平台=youtube)
|
|
562
578
|
[2143] https://youtu.be/zzJmKPX8a3c
|
|
563
579
|
[2143] 解析页面...
|
|
564
580
|
[2143] 15.2% 2.50MiB/s ETA 00:17 ← 下载实时进度
|
|
@@ -579,7 +595,7 @@ node process_videos.js --sheet "YouTube视频" --step analyze --concurrency 2
|
|
|
579
595
|
```
|
|
580
596
|
|
|
581
597
|
| 层级 | 显示内容 |
|
|
582
|
-
|
|
598
|
+
|------|----------|
|
|
583
599
|
| 总体进度 | 完成/总任务数、百分比、✅成功 ❌失败 ⚠️部分 ⏭️无视频 四维计数 |
|
|
584
600
|
| 下载 | yt-dlp 实时百分比 + 速度 + ETA |
|
|
585
601
|
| 转码 | 先 ffprobe 取时长,再实时解析 `time=` 算百分比(如 `25.3% (38s/150s)`) |
|
|
@@ -595,7 +611,7 @@ node process_videos.js --sheet "YouTube视频" --step analyze --concurrency 2
|
|
|
595
611
|
五种输入来源在不同处理环节的输出路径汇总如下。所有路径均以 `output/` 为根(可通过 `DOWNLOADS_DIR` / `TRANSCODED_DIR` / `REPORTS_DIR` 环境变量覆盖)。
|
|
596
612
|
|
|
597
613
|
> `{sheet}` = Excel 工作表名(如 `YouTube视频`、`普诺赛中文站`)
|
|
598
|
-
> `{platform}` = 视频平台标识(如 `youtube`、`bilibili`、`
|
|
614
|
+
> `{platform}` = 视频平台标识(如 `youtube`、`bilibili`、`tencent`、`youku`)
|
|
599
615
|
> `{stem}` = 去重后的安全文件名(不含扩展名)
|
|
600
616
|
|
|
601
617
|
### ① Excel 批量模式(默认)
|
|
@@ -778,11 +794,11 @@ node process_videos.js --content-column "content" --concurrency 2 # 执行
|
|
|
778
794
|
脚本支持四个视频平台的下载,各有不同的反爬配置:
|
|
779
795
|
|
|
780
796
|
| 平台 | 字段 | 反爬措施 |
|
|
781
|
-
|
|
782
|
-
| B站 (bilibili) | `extra.
|
|
783
|
-
| YouTube | `extra.
|
|
784
|
-
| 腾讯视频 | `extra.
|
|
785
|
-
| 优酷 | `extra.
|
|
797
|
+
|------|-------|----------|
|
|
798
|
+
| B站 (bilibili) | `extra.bilibili` | Chrome UA + Referer 头 + 有效 cookie + 并发分片 |
|
|
799
|
+
| YouTube | `extra.youtube` | Chrome UA + Firefox cookie 直读 + 代理 + Node.js 解 n-sig |
|
|
800
|
+
| 腾讯视频 | `extra.tencent` | 无需特殊配置 |
|
|
801
|
+
| 优酷 | `extra.youku` | 无需特殊配置(部分视频需会员) |
|
|
786
802
|
|
|
787
803
|
> YouTube 反爬最强:需要 **代理** + **登录态 cookie** + **JS runtime 解 n-sig** 三者配合。
|
|
788
804
|
> 脚本会自动给 yt-dlp 及其 node/ejs 子进程注入 `HTTPS_PROXY` 环境变量,确保所有流量走代理。
|
|
@@ -795,7 +811,7 @@ node process_videos.js --content-column "content" --concurrency 2 # 执行
|
|
|
795
811
|
#### YouTube
|
|
796
812
|
|
|
797
813
|
| 格式类型 | URL 示例 | 视频 ID 提取正则 |
|
|
798
|
-
|
|
814
|
+
|----------|-----------|-------------------|
|
|
799
815
|
| 标准观看页 | `https://www.youtube.com/watch?v=VIDEO_ID` | `youtube\.com/watch\?v=([a-zA-Z0-9_-]{11})` |
|
|
800
816
|
| 短链接 | `https://youtu.be/VIDEO_ID` | `youtu\.be/([a-zA-Z0-9_-]{11})` |
|
|
801
817
|
| Shorts | `https://www.youtube.com/shorts/VIDEO_ID` | `youtube\.com/shorts/([a-zA-Z0-9_-]{11})` |
|
|
@@ -815,7 +831,7 @@ node process_videos.js --content-column "content" --concurrency 2 # 执行
|
|
|
815
831
|
#### B站(bilibili)
|
|
816
832
|
|
|
817
833
|
| 格式类型 | URL 示例 | 视频 ID 提取正则 |
|
|
818
|
-
|
|
834
|
+
|----------|-----------|-------------------|
|
|
819
835
|
| 标准页(BV 号) | `https://www.bilibili.com/video/BV1xx411c7mD` | `bilibili\.com\/video\/(BV[a-zA-Z0-9]{10})` |
|
|
820
836
|
| 标准页(av 号) | `https://www.bilibili.com/video/av170001` | `bilibili\.com\/video\/av(\d+)` |
|
|
821
837
|
| 短链接 | `https://b23.tv/BV1xx411c7mD` | `b23\.tv\/(BV[a-zA-Z0-9]{10})` |
|
|
@@ -836,7 +852,7 @@ node process_videos.js --content-column "content" --concurrency 2 # 执行
|
|
|
836
852
|
#### 腾讯视频
|
|
837
853
|
|
|
838
854
|
| 格式类型 | URL 示例 | 视频 ID 提取正则 |
|
|
839
|
-
|
|
855
|
+
|----------|-----------|-------------------|
|
|
840
856
|
| 标准页(x/page) | `https://v.qq.com/x/page/VIDEO_ID.html` | `v\.qq\.com\/x\/page\/([a-zA-Z0-9]+)\.html` |
|
|
841
857
|
| 标准页(x/cover) | `https://v.qq.com/x/cover/COVER/VIDEO_ID.html` | `v\.qq\.com\/x\/cover\/[^\/]+\/([a-zA-Z0-9]+)\.html` |
|
|
842
858
|
| 内嵌页 | `https://v.qq.com/txp/iframe/player.html?vid=VIDEO_ID` | `[?&]vid=([a-zA-Z0-9]+)` |
|
|
@@ -853,7 +869,7 @@ node process_videos.js --content-column "content" --concurrency 2 # 执行
|
|
|
853
869
|
#### 优酷(Youku)
|
|
854
870
|
|
|
855
871
|
| 格式类型 | URL 示例 | 视频 ID 提取正则 |
|
|
856
|
-
|
|
872
|
+
|----------|-----------|-------------------|
|
|
857
873
|
| 标准页(v_show) | `https://v.youku.com/v_show/id_VIDEO_ID.html` | `v\.youku\.com\/v_show\/id_([a-zA-Z0-9=]+)\.html` |
|
|
858
874
|
| 标准页(video) | `https://v.youku.com/video/VIDEO_ID` | `v\.youku\.com\/video\/([a-zA-Z0-9=]+)` |
|
|
859
875
|
| 标准页(www) | `https://www.youku.com/v_show/id_VIDEO_ID.html` | `www\.youku\.com\/v_show\/id_([a-zA-Z0-9=]+)\.html` |
|
|
@@ -866,18 +882,18 @@ node process_videos.js --content-column "content" --concurrency 2 # 执行
|
|
|
866
882
|
- **格式互转**:
|
|
867
883
|
- 优酷内嵌格式较复杂,建议直接使用标准页链接(`{YOUKU_URL_TPL}`)
|
|
868
884
|
|
|
869
|
-
> **脚本使用提示**:Excel 中只需填入视频 ID(如 `zzJmKPX8a3c`、`BV1pg411b7Ug`、`o0325y3hqh`、`XMzgxNzExNTY4MA==`),脚本自动替换 URL 模板中的 `{
|
|
885
|
+
> **脚本使用提示**:Excel 中只需填入视频 ID(如 `zzJmKPX8a3c`、`BV1pg411b7Ug`、`o0325y3hqh`、`XMzgxNzExNTY4MA==`),脚本自动替换 URL 模板中的 `{youtube}`、`{bilibili}` 等占位符生成下载链接。
|
|
870
886
|
|
|
871
887
|
### 常见下载错误
|
|
872
888
|
|
|
873
889
|
| 错误 | 平台 | 原因 | 解决方案 |
|
|
874
|
-
|
|
890
|
+
|------|------|------|----------|
|
|
875
891
|
| `Sign in to confirm you're not a bot` | YouTube | cookie 过期或无效 | 检查 Firefox 登录态,或重新导出 cookie 文件 |
|
|
876
892
|
| `cookies does no longer seem to be valid` | YouTube | cookie 文件超过 48h | 用 Firefox cookies-from-browser 方案(免维护) |
|
|
877
893
|
| `Unable to download webpage: HTTP Error 403` | YouTube | IP 被识别为非 YouTube 地区 | 确保代理运行(端口 7897),检查 `YOUTUBE_PROXY` |
|
|
878
894
|
| `n challenge solving failed` | YouTube | 无 JS 运行时 | 安装 Node.js,确保 `YOUTUBE_JS_RUNTIMES=node` |
|
|
879
895
|
| `Requested format is not available` | YouTube | n-sig 未解开,格式不可用 | 同上,安装 JS 运行时 |
|
|
880
|
-
| `HTTP Error 412` | B站 | 缺少 Chrome UA 或 cookie 过期 | 重新导出 `cookies/bilibili.txt` |
|
|
896
|
+
| `HTTP Error 412` | B站 | 缺少 Chrome UA 或 cookie 过期 | 重新导出 `cookies/bilibili.txt` 或使用 Firefox 直读 |
|
|
881
897
|
| `HTTP Error 403` | B站 | 地区限制或视频已删除 | 检查视频是否可访问 |
|
|
882
898
|
| `dpapi decryption failed` | YouTube | Windows Chrome cookie 加密 | **改用 Firefox**(`.env` 中设 `YOUTUBE_COOKIES_FROM_BROWSER=firefox`) |
|
|
883
899
|
|
|
@@ -895,7 +911,7 @@ node process_videos.js --content-column "content" --concurrency 2 # 执行
|
|
|
895
911
|
3. 克隆或下载项目文件(`.env.example`、`.env`、`cookies/` 等)
|
|
896
912
|
4. 安装必装工具:`yt-dlp`、`ffmpeg`、`ffprobe`,确保均在 PATH
|
|
897
913
|
5. 用 Firefox 登录 YouTube,设置 `YOUTUBE_COOKIES_FROM_BROWSER=firefox`
|
|
898
|
-
6. B站 cookie 仍需手动导出 `cookies/bilibili.txt`
|
|
914
|
+
6. B站 cookie 仍需手动导出 `cookies/bilibili.txt`(或设置 `BILIBILI_COOKIES_FROM_BROWSER=firefox`)
|
|
899
915
|
7. 启动代理(Clash Verge 等),确认端口匹配 `YOUTUBE_PROXY`
|
|
900
916
|
8. `video-pipeline --dry-run` 验证
|
|
901
917
|
|
|
@@ -906,7 +922,7 @@ node process_videos.js --content-column "content" --concurrency 2 # 执行
|
|
|
906
922
|
3. 安装 Python 依赖:`pip install pandas openpyxl requests python-dotenv questionary`
|
|
907
923
|
4. `cp .env.example .env`,根据实际情况修改 `.env` 中的路径、代理端口和字段映射
|
|
908
924
|
5. 用 Firefox 登录 YouTube,设置 `YOUTUBE_COOKIES_FROM_BROWSER=firefox`
|
|
909
|
-
6. B站 cookie 仍需手动导出 `cookies/bilibili.txt`
|
|
925
|
+
6. B站 cookie 仍需手动导出 `cookies/bilibili.txt`(或设置 `BILIBILI_COOKIES_FROM_BROWSER=firefox`)
|
|
910
926
|
7. 启动代理(Clash Verge 等),确认端口匹配 `YOUTUBE_PROXY`
|
|
911
927
|
8. `python process_videos.py --dry-run` 验证
|
|
912
928
|
|
package/package.json
CHANGED
package/process_videos.js
CHANGED
|
@@ -71,35 +71,40 @@ const COL_ID = process.env.COL_ID || 'extra.id';
|
|
|
71
71
|
const COL_TITLE = process.env.COL_TITLE || 'title';
|
|
72
72
|
const COL_CONTENT = process.env.COL_CONTENT || 'content';
|
|
73
73
|
const COL_KEYWORDS = process.env.COL_KEYWORDS || 'keywords';
|
|
74
|
-
const COL_TENCENTVID = process.env.COL_TENCENTVID || 'extra.
|
|
75
|
-
const COL_BILIBILIBVID = process.env.COL_BILIBILIBVID || 'extra.
|
|
76
|
-
const COL_YOUTUBEID = process.env.COL_YOUTUBEID || 'extra.
|
|
77
|
-
const COL_YOUKUID = process.env.COL_YOUKUID || 'extra.
|
|
74
|
+
const COL_TENCENTVID = process.env.COL_TENCENTVID || 'extra.tencent';
|
|
75
|
+
const COL_BILIBILIBVID = process.env.COL_BILIBILIBVID || 'extra.bilibili';
|
|
76
|
+
const COL_YOUTUBEID = process.env.COL_YOUTUBEID || 'extra.youtube';
|
|
77
|
+
const COL_YOUKUID = process.env.COL_YOUKUID || 'extra.youku';
|
|
78
78
|
|
|
79
79
|
// ============================== 平台配置 ==============================
|
|
80
80
|
const PLATFORM_COL_MAP = {
|
|
81
|
+
tencent: COL_TENCENTVID,
|
|
81
82
|
tencentVid: COL_TENCENTVID,
|
|
83
|
+
bilibili: COL_BILIBILIBVID,
|
|
82
84
|
bilibiliBvid: COL_BILIBILIBVID,
|
|
85
|
+
youtube: COL_YOUTUBEID,
|
|
83
86
|
youtubeId: COL_YOUTUBEID,
|
|
87
|
+
youku: COL_YOUKUID,
|
|
84
88
|
youkuId: COL_YOUKUID,
|
|
85
89
|
};
|
|
86
90
|
|
|
87
91
|
// ============================== 工具函数 ==============================
|
|
88
92
|
function c(color, text) {
|
|
89
93
|
const colors = {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
94
|
+
bold: '\x1b[1m',
|
|
95
|
+
dim: '\x1b[2m',
|
|
96
|
+
yellow: '\x1b[93m',
|
|
97
|
+
cyan: '\x1b[96m',
|
|
98
|
+
green: '\x1b[92m',
|
|
99
|
+
red: '\x1b[91m',
|
|
100
|
+
blue: '\x1b[94m',
|
|
101
|
+
magenta: '\x1b[95m',
|
|
102
|
+
reset: '\x1b[0m',
|
|
98
103
|
};
|
|
99
104
|
return (colors[color] || '') + text + colors.reset;
|
|
100
105
|
}
|
|
101
106
|
|
|
102
|
-
const PLATFORM_PRIORITY = (process.env.PLATFORM_PRIORITY || '
|
|
107
|
+
const PLATFORM_PRIORITY = (process.env.PLATFORM_PRIORITY || 'bilibili,youtube,tencent,youku')
|
|
103
108
|
.split(',').map(s => s.trim()).filter(Boolean);
|
|
104
109
|
|
|
105
110
|
const _VIDEO_SHEETS_RAW = process.env.VIDEO_SHEETS || '';
|
|
@@ -108,9 +113,13 @@ const VIDEO_SHEETS = _VIDEO_SHEETS_RAW
|
|
|
108
113
|
: [];
|
|
109
114
|
|
|
110
115
|
const _PKEY_ENV_PREFIX = {
|
|
116
|
+
tencent: 'TENCENT',
|
|
111
117
|
tencentVid: 'TENCENT',
|
|
118
|
+
bilibili: 'BILIBILI',
|
|
112
119
|
bilibiliBvid: 'BILIBILI',
|
|
120
|
+
youtube: 'YOUTUBE',
|
|
113
121
|
youtubeId: 'YOUTUBE',
|
|
122
|
+
youku: 'YOUKU',
|
|
114
123
|
youkuId: 'YOUKU',
|
|
115
124
|
};
|
|
116
125
|
|
|
@@ -137,11 +146,11 @@ function buildPlatformConfig() {
|
|
|
137
146
|
const ua = process.env[`${prefix}_USER_AGENT`] || '';
|
|
138
147
|
const extraHeaders = [];
|
|
139
148
|
if (ua) extraHeaders.push('--user-agent', ua);
|
|
140
|
-
if (pkey === '
|
|
149
|
+
if (pkey === 'bilibili') {
|
|
141
150
|
const referer = process.env[`${prefix}_REFERER`] || '';
|
|
142
151
|
if (referer) extraHeaders.push('--add-header', `Referer:${referer}`);
|
|
143
152
|
}
|
|
144
|
-
if (ua || (pkey === '
|
|
153
|
+
if (ua || (pkey === 'bilibili' && process.env[`${prefix}_REFERER`])) {
|
|
145
154
|
extraHeaders.push('--add-header', 'Accept-Language:zh,en;q=0.9');
|
|
146
155
|
}
|
|
147
156
|
if (extraHeaders.length) cfg.extra_headers = extraHeaders;
|
|
@@ -151,7 +160,7 @@ function buildPlatformConfig() {
|
|
|
151
160
|
if (cf) cfg.concurrent_fragments = parseInt(cf, 10);
|
|
152
161
|
|
|
153
162
|
// Extra args (YouTube)
|
|
154
|
-
if (pkey === '
|
|
163
|
+
if (pkey === 'youtube') {
|
|
155
164
|
const jsRt = process.env[`${prefix}_JS_RUNTIMES`] || '';
|
|
156
165
|
const rc = process.env[`${prefix}_REMOTE_COMPONENTS`] || '';
|
|
157
166
|
const extraArgs = [];
|
|
@@ -173,13 +182,13 @@ const PLATFORM_CONFIG = buildPlatformConfig();
|
|
|
173
182
|
|
|
174
183
|
// ============================== 日志 ==============================
|
|
175
184
|
function logInfo(msg) {
|
|
176
|
-
console.log(`${timestamp()} [INFO] ${msg}`);
|
|
185
|
+
console.log(`${timestamp()} ${c('cyan', '[INFO]')} ${msg}`);
|
|
177
186
|
}
|
|
178
187
|
function logWarn(msg) {
|
|
179
|
-
console.log(`${timestamp()} [WARN] ${msg}`);
|
|
188
|
+
console.log(`${timestamp()} ${c('yellow', '[WARN]')} ${msg}`);
|
|
180
189
|
}
|
|
181
190
|
function logError(msg) {
|
|
182
|
-
console.log(`${timestamp()} [ERROR] ${msg}`);
|
|
191
|
+
console.log(`${timestamp()} ${c('red', '[ERROR]')} ${msg}`);
|
|
183
192
|
}
|
|
184
193
|
function timestamp() {
|
|
185
194
|
return new Date().toTimeString().slice(0, 8);
|
|
@@ -209,7 +218,13 @@ class OverallProgress {
|
|
|
209
218
|
}
|
|
210
219
|
summaryLine() {
|
|
211
220
|
const pct = this.total ? (this.completed / this.total * 100).toFixed(1) : '0.0';
|
|
212
|
-
|
|
221
|
+
const parts = [];
|
|
222
|
+
parts.push(c('dim', `[总进度 ${this.completed}/${this.total} (${pct}%)]`));
|
|
223
|
+
parts.push(this.success > 0 ? c('green', `✅${this.success}`) : c('dim', '✅0'));
|
|
224
|
+
parts.push(this.failed > 0 ? c('red', `❌${this.failed}`) : c('dim', '❌0'));
|
|
225
|
+
parts.push(this.partial > 0 ? c('yellow', `⚠️${this.partial}`) : c('dim', '⚠️0'));
|
|
226
|
+
parts.push(this.noVideo > 0 ? c('cyan', `⏹️${this.noVideo}`) : c('dim', '⏹️0'));
|
|
227
|
+
return parts.join(' ');
|
|
213
228
|
}
|
|
214
229
|
}
|
|
215
230
|
|
|
@@ -328,7 +343,7 @@ function buildUrl(pkey, vid) {
|
|
|
328
343
|
const URL_PLATFORM_MAP = [
|
|
329
344
|
{
|
|
330
345
|
platform: 'bilibili',
|
|
331
|
-
pkey: '
|
|
346
|
+
pkey: 'bilibili',
|
|
332
347
|
patterns: [
|
|
333
348
|
/bilibili\.com\/video\/(BV[a-zA-Z0-9]{10})/,
|
|
334
349
|
/b23\.tv\/([a-zA-Z0-9]+)/,
|
|
@@ -337,14 +352,14 @@ const URL_PLATFORM_MAP = [
|
|
|
337
352
|
},
|
|
338
353
|
{
|
|
339
354
|
platform: 'youtube',
|
|
340
|
-
pkey: '
|
|
355
|
+
pkey: 'youtube',
|
|
341
356
|
patterns: [
|
|
342
357
|
/(?:youtube\.com\/(?:watch\?v=|embed\/|shorts\/|live\/)|youtu\.be\/)([a-zA-Z0-9_-]{11})/,
|
|
343
358
|
],
|
|
344
359
|
},
|
|
345
360
|
{
|
|
346
361
|
platform: 'tencent',
|
|
347
|
-
pkey: '
|
|
362
|
+
pkey: 'tencent',
|
|
348
363
|
patterns: [
|
|
349
364
|
/v\.qq\.com\/x\/page\/([a-zA-Z0-9]+)\.html/,
|
|
350
365
|
/v\.qq\.com\/x\/cover\/[^/]+\/([a-zA-Z0-9]+)\.html/,
|
|
@@ -353,7 +368,7 @@ const URL_PLATFORM_MAP = [
|
|
|
353
368
|
},
|
|
354
369
|
{
|
|
355
370
|
platform: 'youku',
|
|
356
|
-
pkey: '
|
|
371
|
+
pkey: 'youku',
|
|
357
372
|
patterns: [
|
|
358
373
|
/v\.youku\.com\/v_show\/id_([a-zA-Z0-9=]+)\.html/,
|
|
359
374
|
],
|
|
@@ -689,7 +704,7 @@ async function stepAnalyze(text, maxRetries, retryDelay, timeout = 300, label =
|
|
|
689
704
|
const model = process.env.AI_MODEL || '';
|
|
690
705
|
const promptTpl = process.env.AI_PROMPT_TPL || '帮我归纳总结一下Keywords,尽可能全一点,这是内容:{content}';
|
|
691
706
|
const aiTemperature = parseFloat(process.env.AI_TEMPERATURE || '0.3');
|
|
692
|
-
const aiTimeout =
|
|
707
|
+
const aiTimeout = timeout;
|
|
693
708
|
|
|
694
709
|
if (!apiKey || !baseUrl || !model) {
|
|
695
710
|
return { text: null, retries: 0, error: 'AI config incomplete' };
|
|
@@ -711,7 +726,7 @@ async function stepAnalyze(text, maxRetries, retryDelay, timeout = 300, label =
|
|
|
711
726
|
const progressInterval = setInterval(() => {
|
|
712
727
|
if (!done) {
|
|
713
728
|
const elapsed = ((Date.now() - analyzeStart) / 1000).toFixed(0);
|
|
714
|
-
lockedPrint(` [${label}] AI analyzing... ${elapsed}s`);
|
|
729
|
+
lockedPrint(` [${label}] ${c('green', 'AI analyzing...')} ${elapsed}s`);
|
|
715
730
|
}
|
|
716
731
|
}, 5000);
|
|
717
732
|
|
|
@@ -742,7 +757,7 @@ async function stepAnalyze(text, maxRetries, retryDelay, timeout = 300, label =
|
|
|
742
757
|
lastErr = String(e.message).slice(0, 500);
|
|
743
758
|
if (attempt < maxAttempts - 1) {
|
|
744
759
|
const delay = Math.min(retryDelay * Math.pow(2, attempt), 30);
|
|
745
|
-
lockedPrint(` [${label}] AI attempt ${attempt + 1} failed: ${lastErr.slice(0, 100)}, retrying in ${delay}s...`);
|
|
760
|
+
lockedPrint(` [${label}] AI attempt ${attempt + 1} ${c('red', 'failed')}: ${lastErr.slice(0, 100)}, retrying in ${delay}s...`);
|
|
746
761
|
await sleep(delay * 1000);
|
|
747
762
|
}
|
|
748
763
|
}
|
|
@@ -796,13 +811,13 @@ async function stepDownload(row, sheetName, maxRetries, retryDelay, force, timeo
|
|
|
796
811
|
if (!force) {
|
|
797
812
|
const existing = findDownloadedFile(dlDir, stem);
|
|
798
813
|
if (existing) {
|
|
799
|
-
lockedPrint(` [${stem}] exists ${path.basename(existing)}, skip download`);
|
|
814
|
+
lockedPrint(c('dim', ` [${stem}] exists ${path.basename(existing)}, skip download`));
|
|
800
815
|
return { file: existing, retries: 0, error: null };
|
|
801
816
|
}
|
|
802
817
|
}
|
|
803
818
|
|
|
804
819
|
const videoUrl = buildUrl(pkey, vid);
|
|
805
|
-
lockedPrint(` [${stem}] start download (platform=${pkey})`);
|
|
820
|
+
lockedPrint(` [${stem}] ${c('cyan', 'start download')} (platform=${pkey})`);
|
|
806
821
|
lockedPrint(` [${stem}] ${videoUrl}`);
|
|
807
822
|
|
|
808
823
|
const cfg = PLATFORM_CONFIG[pkey];
|
|
@@ -863,7 +878,7 @@ async function stepDownload(row, sheetName, maxRetries, retryDelay, force, timeo
|
|
|
863
878
|
|
|
864
879
|
const downloaded = findDownloadedFile(dlDir, stem);
|
|
865
880
|
if (downloaded) {
|
|
866
|
-
lockedPrint(` [${stem}] download done -> ${path.basename(downloaded)}`);
|
|
881
|
+
lockedPrint(` [${stem}] ${c('green', 'download done')} -> ${path.basename(downloaded)}`);
|
|
867
882
|
return { file: downloaded, retries: 0, error: null };
|
|
868
883
|
}
|
|
869
884
|
logError(`[${stem}] file not found after download`);
|
|
@@ -957,7 +972,7 @@ async function stepTranscode(srcFile, sheetName, maxRetries, retryDelay, force,
|
|
|
957
972
|
|
|
958
973
|
try {
|
|
959
974
|
await retryCall(doTranscode, maxRetries, retryDelay, stem);
|
|
960
|
-
lockedPrint(` [${stem}] transcode done`);
|
|
975
|
+
lockedPrint(` [${stem}] ${c('green', 'transcode done')}`);
|
|
961
976
|
return { file: outFile, retries: 0, error: null };
|
|
962
977
|
} catch (e) {
|
|
963
978
|
logError(`[${stem}] ffmpeg transcode failed: ${(e.stderr || e.message).slice(-2000)}`);
|
|
@@ -979,10 +994,10 @@ async function stepTranscribe(audioFile, maxRetries, retryDelay, timeout = 600)
|
|
|
979
994
|
const fileSizeMB = (fs.statSync(audioFile).size / (1024 * 1024)).toFixed(1);
|
|
980
995
|
if (WHISPER_BACKEND === 'local') {
|
|
981
996
|
const langLabel = WHISPER_LANGUAGE || 'auto';
|
|
982
|
-
lockedPrint(` [${stem}] start transcribe [local(${WHISPER_MODEL}/${langLabel})] (${fileSizeMB}MB)...`);
|
|
997
|
+
lockedPrint(` [${stem}] ${c('magenta', 'start transcribe')} [local(${WHISPER_MODEL}/${langLabel})] (${fileSizeMB}MB)...`);
|
|
983
998
|
} else {
|
|
984
999
|
const modelLabel = WHISPER_SERVICE_MODEL || WHISPER_MODEL || '(server default)';
|
|
985
|
-
lockedPrint(` [${stem}] start transcribe [service(${modelLabel})] (${fileSizeMB}MB)...`);
|
|
1000
|
+
lockedPrint(` [${stem}] ${c('magenta', 'start transcribe')} [service(${modelLabel})] (${fileSizeMB}MB)...`);
|
|
986
1001
|
}
|
|
987
1002
|
|
|
988
1003
|
if (WHISPER_BACKEND === 'local') {
|
|
@@ -1018,7 +1033,7 @@ async function transcribeLocal(audioFile, stem, maxRetries, retryDelay, timeout
|
|
|
1018
1033
|
const { result: text, retriesUsed, error } = await retryCall(doTranscribe, maxRetries, retryDelay, stem);
|
|
1019
1034
|
const elapsed = ((Date.now() - startTime) / 1000).toFixed(0);
|
|
1020
1035
|
if (error) return { text: null, retries: retriesUsed, error };
|
|
1021
|
-
lockedPrint(` [${stem}] transcribe done (${elapsed}s, ${text.length} chars)`);
|
|
1036
|
+
lockedPrint(` [${stem}] ${c('green', 'transcribe done')} (${elapsed}s, ${text.length} chars)`);
|
|
1022
1037
|
return { text, retries: 0, error: null };
|
|
1023
1038
|
} catch (e) {
|
|
1024
1039
|
logError(`[${stem}] local whisper transcribe failed: ${e.message}`);
|
|
@@ -1082,7 +1097,7 @@ async function transcribeService(audioFile, stem, maxRetries, retryDelay, timeou
|
|
|
1082
1097
|
const { result: text, retriesUsed, error } = await retryCall(doTranscribe, maxRetries, retryDelay, stem);
|
|
1083
1098
|
const elapsed = ((Date.now() - startTime) / 1000).toFixed(0);
|
|
1084
1099
|
if (error) return { text: null, retries: retriesUsed, error };
|
|
1085
|
-
lockedPrint(` [${stem}] transcribe done (${elapsed}s, ${text.length} chars)`);
|
|
1100
|
+
lockedPrint(` [${stem}] ${c('green', 'transcribe done')} (${elapsed}s, ${text.length} chars)`);
|
|
1086
1101
|
return { text, retries: 0, error: null };
|
|
1087
1102
|
} catch (e) {
|
|
1088
1103
|
logError(`[${stem}] whisper transcribe failed: ${e.message}`);
|
|
@@ -1314,8 +1329,13 @@ async function processOneTask(row, sheetName, steps, maxRetries, retryDelay, for
|
|
|
1314
1329
|
const result = new TaskResult(sheetName, key, title, pkey, videoUrl, stem);
|
|
1315
1330
|
|
|
1316
1331
|
const tag = positionLabel ? `${positionLabel} ` : '';
|
|
1317
|
-
lockedPrint(
|
|
1318
|
-
|
|
1332
|
+
lockedPrint('');
|
|
1333
|
+
lockedPrint(c('dim', '─'.repeat(62)));
|
|
1334
|
+
lockedPrint(c('bold', ` ▶ Task ${positionLabel || '?'}`)
|
|
1335
|
+
+ c('dim', ` [${stem}] sheet=${sheetName} platform=${pkey || 'N/A'}`));
|
|
1336
|
+
if (title) lockedPrint(c('dim', ` title: ${title.slice(0, 50)}`));
|
|
1337
|
+
lockedPrint(c('dim', '─'.repeat(62)));
|
|
1338
|
+
logInfo(`[${stem}] start (sheet=${sheetName}, platform=${pkey || 'N/A'}, title=${title.slice(0, 40)})`);
|
|
1319
1339
|
|
|
1320
1340
|
// ── download ──
|
|
1321
1341
|
let dlFile = null;
|
|
@@ -1414,9 +1434,9 @@ async function processOneTask(row, sheetName, steps, maxRetries, retryDelay, for
|
|
|
1414
1434
|
const { text: kw, retries, error } = await stepAnalyze(txt, maxRetries, retryDelay, analyzeTimeout, result.stem);
|
|
1415
1435
|
result.analyze = new StepResult(kw ? 'success' : 'failed', kw, error, retries);
|
|
1416
1436
|
if (kw) {
|
|
1417
|
-
lockedPrint(` [${result.stem}] AI analysis done (${kw.length} chars)`);
|
|
1437
|
+
lockedPrint(` [${result.stem}] ${c('green', 'AI analysis done')} (${kw.length} chars)`);
|
|
1418
1438
|
} else {
|
|
1419
|
-
lockedPrint(` [${result.stem}] AI analysis failed: ${error}`);
|
|
1439
|
+
lockedPrint(` [${result.stem}] ${c('red', 'AI analysis failed')}: ${error}`);
|
|
1420
1440
|
}
|
|
1421
1441
|
} catch (e) {
|
|
1422
1442
|
result.analyze = new StepResult('failed', null, String(e.message).slice(0, 500), maxRetries);
|
|
@@ -2084,6 +2104,8 @@ async function run({
|
|
|
2084
2104
|
}
|
|
2085
2105
|
results.push(result);
|
|
2086
2106
|
overall.addResult(result.overall_status);
|
|
2107
|
+
lockedPrint('');
|
|
2108
|
+
lockedPrint(c('dim', '─'.repeat(62)));
|
|
2087
2109
|
console.log(`\n${overall.summaryLine()}\n`);
|
|
2088
2110
|
return result;
|
|
2089
2111
|
})
|
|
@@ -2284,6 +2306,8 @@ async function runFromReport(reportPath, steps, maxRetries, retryDelay, concurre
|
|
|
2284
2306
|
}
|
|
2285
2307
|
results.push(result);
|
|
2286
2308
|
overall.addResult(result.overall_status);
|
|
2309
|
+
lockedPrint('');
|
|
2310
|
+
lockedPrint(c('dim', '─'.repeat(62)));
|
|
2287
2311
|
console.log(`\n${overall.summaryLine()}\n`);
|
|
2288
2312
|
return result;
|
|
2289
2313
|
})
|