dsh-bili-watch 0.2.30 → 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 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
  ## 安装
@@ -45,7 +45,7 @@ dsh plugin --profile web add dsh-bili-watch@latest
45
45
  dsh plugin --profile web remove dsh-bili-watch
46
46
  ```
47
47
 
48
- 升级后重启 DSH。`deps/` 在包目录内,卸载或重装会连模型、环境、凭证一起删掉;要长期保留就把 `BILI_WATCH_PROJ` / `BILI_WATCH_SEEN` 指到包外。**抽帧缓存根在包目录外**(默认 `~/.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`,删了要重新扫码。** 日常清理在面板里逐项删即可。
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
49
 
50
50
  ## 模块架构
51
51
 
@@ -80,7 +80,7 @@ flowchart TD
80
80
 
81
81
  - **依赖状态**:ffmpeg、yt-dlp、ASR 环境、OCR 环境、转写模型、登录凭证逐项列出是否就绪、实际路径、占用体积。缺的点击行末「一键下载」,后台跑 `scripts/fetch-deps`(体积是估算值,实际以跑完为准)。
82
82
  - **扫码登录**:凭证未登录时点「去扫码登录」(已登录是「重新扫码」),面板里弹出二维码,手机 B 站扫完自动刷新。凭证保存在本机 `$DSH_HOME/bili-watch/credential.json`。
83
- - **删除**:面板按实际路径判断——装在插件目录里的(yt-dlp、转写模型、登录凭证)可直接删;装在插件目录外的(比如复用系统 ffmpeg 或已有的 Python 环境)按钮置灰,旁边标出它实际在哪。
83
+ - **删除**:面板按实际路径判断——yt-dlp、转写模型、登录凭证可直接删;ffmpeg / ASR 环境 / OCR 环境不提供删除按钮(环境删了要重跑 `fetch-deps` 装回来,代价大于省下的空间),旁边标出它实际在哪。
84
84
  - **批量**:标题行的「批量下载 / 批量清理」打开勾选清单,默认勾上所有可操作的项,删除前会再确认一次。
85
85
  - **抽帧缓存**:显示当前占用与文件数,「立即清除」一键清空;不点则会按规则自动清理——保留天数(默认 7 天)与总量上限(默认 2048 MB),超过则从最旧的删起。规则存在 `deps/cache-config.json`,插件启动和打开面板时各检查一次。
86
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 生效,面板「抽帧缓存 → 位置」会显示当前实际路径。
@@ -103,7 +103,7 @@ flowchart TD
103
103
 
104
104
  `ffmpeg` 查找顺序:`--ffmpeg` 参数 → `BILI_WATCH_FFMPEG` → `deps/deps-config.json` 里的 `ffmpeg` → 插件 `deps/ffmpeg.exe` → PATH。
105
105
 
106
- `deps/` 用来放运行期文件:Python venv、模型、yt-dlp、凭证,默认不进 git(见 `.gitignore`)。ASR 和 OCR 的 Python 环境是可选的,不装也能用,只是少这两档。
106
+ `deps/` 用来放运行期文件:Python venv、模型、yt-dlp,默认不进 git(见 `.gitignore`)。登录凭证**不在** `deps/`,见上文的 `$DSH_HOME/bili-watch/credential.json`。ASR 和 OCR 的 Python 环境是可选的,不装也能用,只是少这两档。
107
107
 
108
108
  `deps/_bili-asr.py` 默认从 `hf-mirror.com` 下模型,国内快。海外网络可以先设 `HF_ENDPOINT=https://huggingface.co` 再跑 `fetch-deps`。
109
109
 
@@ -144,7 +144,7 @@ macOS/Linux 的 ffmpeg 用系统包管理器装,脚本只给提示。其余目
144
144
  1. `login`:插件出二维码,手机 B 站扫码。凭证存在 `$DSH_HOME/bili-watch/credential.json`,不出本机。
145
145
  2. `--cred <文件路径>`:读一份现成的 `{sessdata, bili_jct, buvid3, buvid4}` JSON。
146
146
 
147
- 凭证顺序:给了 `--cred` 就只用它,读不到就按匿名跑;没给 `--cred` 才用 `$DSH_HOME/bili-watch/credential.json`;两者都没有就匿名。
147
+ 凭证顺序:给了 `--cred` 就只用它,读不到就按匿名跑;没给 `--cred` 才用 `$DSH_HOME/bili-watch/credential.json`;两者都没有就匿名。**例外:** 本机若同时装有 bili-duty,且它的 `$DSH_HOME/bili-duty/bili_credential.json` 比 watch 自己那份更新,watch 会优先用那一份(同一台机器复用登录态)。因此「在面板里删掉凭证」不等于「立刻变匿名」——bili-duty 那份还在时会继续用它。
148
148
 
149
149
  这份文件等同账号登录态。请勿分享、提交、上传。
150
150
 
@@ -154,23 +154,24 @@ macOS/Linux 的 ffmpeg 用系统包管理器装,脚本只给提示。其余目
154
154
  |---|---|
155
155
  | `view <BV或链接或URL>` | 元数据 + 简介 + 热评(不下载、不依赖 ffmpeg,永远能跑) |
156
156
  | `peek <BV或链接>` | 元数据 + 热评 + 5 张均匀帧(需下载 360p 约每分钟 1 MB + 抽帧约 2 秒;装了 OCR 环境时会顺带识别这 5 帧;仅 B 站) |
157
- | `sub <BV或链接> [--format srt|json|md|txt] [--out 文件]` | AI 字幕 + 三判据(字/秒、关键词命中、时长);`--format` 输出 SRT、JSON(带官方时间戳)、Markdown(每条带跳回原片的链接)或纯文本 txt(不带时间戳) |
157
+ | `sub <BV或链接> [--format srt\|json\|md\|txt] [--out 文件(需配合 --format)]` | AI 字幕 + 两判据(字/秒、关键词重合;时长只用作语速的分母);`--format` 输出 SRT、JSON(带官方时间戳)、Markdown(每条带跳回原片的链接)或纯文本 txt(不带时间戳) |
158
158
  | `comments <BV> [N]` | 评论区,默认按热度排序 |
159
159
  | `asr <BV或URL>` | 本地转写音频,并给出「可信 / 疑似幻觉」判定(见下节) |
160
160
  | `frames <BV或URL> [--step 秒] [--ocr]` | 抽帧;`--ocr` 顺带识别画面文字 |
161
161
  | `ocr <BV>` | 抽帧并 OCR |
162
- | `text <BV或URL> [--format json|md]` | 四级链:字幕 → 转写 → 抽帧 OCR;`--format json` 带上时间轴口径(字幕 exact,转写与 OCR 是估算);`md` 有字幕时出时间轴,没有就出标题+全文;SRT 只有 `sub` 支持 |
162
+ | `text <BV或URL> [--format json\|md]` | 四级链:字幕 → 转写 → 抽帧 OCR;`--format json` 带上时间轴口径(字幕 exact,转写与 OCR 是估算);`md` 有字幕时出时间轴,没有就出标题+全文;SRT 只有 `sub` 支持 |
163
163
  | `sheet <BV> [--grid 3x3]` | 均匀挑帧拼成一张图(`NxM` 如 `3x3`、`4x4`,默认 9 帧) |
164
164
  | `substrip <BV> [--step 秒]` | 底部字幕竖拼成一张图 |
165
165
  | `danmaku <BV>` | 弹幕热区(按时间桶统计峰值) |
166
- | `search <关键词> [--days 3] [--limit 10] [--order pubdate|click]` | 搜视频 |
166
+ | `local <文件路径> [--ocr] [--frames]` | 本地视频文件走同一条四级链(字幕层不适用,直接从转写起)。**仅 CLI 可用**,`bili_watch` 工具与 `/bili` 不提供 |
167
+ | `search <关键词> [--days 3] [--limit 10] [--order pubdate\|click]` | 搜视频。`--days` 生效时最多抓 3 页再筛,结果仍按 `--limit` 截断 |
167
168
  | `hot [--limit 10]` | 综合热门榜 |
168
169
  | `login [--check] [--poll] [--key KEY]` | 扫码登录。`--check` = 检查本地凭证是否还有效(不会重新登录);`--key KEY` 需配合 `--poll` 才生效(单独 `login --key X` 会被静默忽略) |
169
- | `clean` | 清空缓存根(默认 `$DSH_HOME/bili-watch/cache`;只清本插件自己写的 `BV*` 与 `frames/`、`asr/`、`_logs/` 等目录,同一层里别人的文件不动,凭证在上一层不会被碰到) |
170
+ | `clean` | 清空缓存根(默认 `$DSH_HOME/bili-watch/cache`;只清本插件自己写的 `BV*` 与 `frames/`、`asr/`、`_logs/` 等目录,同一层里别人的文件不动,凭证在上一层不会被碰到)。有任务在跑(存在 `*.lock` 且持有进程还活着)时会拒绝清理并返回非零退出码;进程已被强杀的残留锁不算占用 |
170
171
 
171
- 工具侧的等价入口:`bili_watch` 工具(参数 `command` + `bv`),或 `/bili` 命令。
172
+ 工具侧的等价入口:13 个内容命令在 `bili_watch` 工具、`/bili` 命令、CLI 三处等价(工具参数是 `command` + `bv`,另有 `format` / `out` / `limit` 等字段);`login` 与 `clean` 只有 `/bili` 和 CLI;`local` 只有 CLI。
172
173
 
173
- 表里只列常用参数。完整参数(`--out`、`--fresh`、`--scene`、`--ffmpeg`、`--cred`、`--fast`、`--mark-seen`、`--p` 等)跑 `bili-watch help` 看。
174
+ 表里只列常用参数。完整参数(`--out`(**必须配合 `--format`,只给 `--out` 会报错退出**)、`--fresh`、`--scene`、`--ffmpeg`、`--cred`、`--fast`、`--mark-seen`、`--p`、`--asr-max`(转写时长闸门,默认 30 分钟,单位毫秒)等)跑 `bili-watch help` 看。
174
175
 
175
176
  ## 单条链接软提示
176
177
 
@@ -222,7 +223,7 @@ AI 字幕(需登录)
222
223
  - 关键词命中率低(字幕和标题/UP 名对不上):不单独触发,要和语速异常同时出现才算补充理由
223
224
  - 字幕里自称「实验 / 钓鱼 / 测试」
224
225
 
225
- 想进一步确认就加 `--verify`:它会抽 5 帧做 OCR,拿画面文字跟字幕比一遍,报「首帧写着某实验说明」或「画面与字幕几乎不重合」。
226
+ 想进一步确认就加 `--verify`:它会抽 5 帧做 OCR,拿画面文字跟字幕比一遍,报「首帧写着某实验说明」或「画面与字幕几乎不重合」。**注意 `--verify` 只在字幕已被上面三条信号判为可疑时才生效**;字幕正常时它不抽帧、不 OCR,也不会额外提示(行为与不加 `--verify` 完全一样)。
226
227
 
227
228
  ```
228
229
  node tools/look.mjs text BV1xxxxxxxxx --verify
@@ -272,7 +273,7 @@ MIT
272
273
 
273
274
  ## 长文本保护(2026-10-26)
274
275
 
275
- `text` / `sub` / `asr` 的正文超过 **800 字**时,终端只给开头 400 字摘要 + 一行落盘路径,全文写进缓存目录下按视频分的文件夹:`<缓存目录>/<BV>_<标题>/<命令>.txt`。
276
+ `text` / `sub` / `asr` 的正文超过 **800 字**时,终端只给开头 400 字摘要 + 一行落盘路径,全文写进缓存目录下按视频分的文件夹:`<缓存目录>/<BV>[_<标题>]/<命令>.txt`。标题段只在先跑过 `view` / `peek` 时才有——直接 `text BVxxx` 建的是纯 `<BV>` 目录,之后再跑 `view` / `peek` 会把它改名成 `<BV>_<标题>`。
276
277
 
277
278
  - `--head N`:自定义摘要长度
278
279
  - `--full`:打全文
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:0;color:var(--dsw-alias-label-tertiary,#8a9099);font-size:12px;word-break:break-all;}
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
  }
@@ -223,6 +258,31 @@ window.__ModuleLoader__.load({
223
258
  }
224
259
  }
225
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} 分钟)。`)
284
+ }
285
+
226
286
  const rows = st?.deps || []
227
287
  // 打开批量弹层:默认全勾选(可下载 = 缺的且不是凭证;可清理 = 插件目录内且已存在)
228
288
  const openDlg = (kind) => {
@@ -414,6 +474,74 @@ window.__ModuleLoader__.load({
414
474
  h('button', { 'data-bl-btn': '', onClick: saveRule }, '保存规则')),
415
475
  h('div', { 'data-bl-note': '' },
416
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,
417
545
  h('div', { 'data-bl-sec': '' },
418
546
  h('span', { 'data-bl-sec-title': '' }, '最近一次调用'),
419
547
  h('div', { 'data-bl-row': '' },