dsh-bili-watch 0.2.28 → 0.2.29

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.
Files changed (3) hide show
  1. package/index.js +17 -7
  2. package/package.json +1 -1
  3. package/tools/look.mjs +15 -13
package/index.js CHANGED
@@ -30,7 +30,9 @@ function cleanKey(s) {
30
30
 
31
31
  /** 按视频分目录:CACHE/<BV>/<cmd>.txt —— 同一个视频各命令的输出收在一处 */
32
32
  function videoDir(key) {
33
- const base = cleanKey(key)
33
+ // T34-P0-1:模型可能把整条链接当 key 传进来,先取 BV 再建目录,别在缓存根下建乱码目录
34
+ const bvHit = /(BV[0-9A-Za-z]{10})/.exec(String(key || ''))
35
+ const base = cleanKey(bvHit ? bvHit[1] : key)
34
36
  mkdirSync(CACHE, { recursive: true })
35
37
  // 同一个 BV 已经有目录(可能被 CLI 改成「BV_标题」)就用它,别建第二个兄弟目录(2026-10-26 审查 T01/T02)
36
38
  try {
@@ -687,7 +689,13 @@ function parseCommandLine(raw) {
687
689
  let cred = ''
688
690
  let badOut = false
689
691
  while (tokens.length) {
690
- const t = tokens.shift()
692
+ let t = tokens.shift()
693
+ // T15-4:支持 --option=value(原来整串被当位置参数静默吞掉)
694
+ if (t.startsWith('--') && t.includes('=')) {
695
+ const eq = t.indexOf('=')
696
+ tokens.unshift(t.slice(eq + 1))
697
+ t = t.slice(0, eq)
698
+ }
691
699
  if (t === '--step' && tokens.length) {
692
700
  step = tokens.shift()
693
701
  extra.push('--step', step)
@@ -724,10 +732,10 @@ function parseCommandLine(raw) {
724
732
  positionals.push(t)
725
733
  }
726
734
  }
727
- return { command, bv: positionals[0] || '', extra, step, cred, badOut }
735
+ return { command, bv: positionals[0] || '', positionals, extra, step, cred, badOut }
728
736
  }
729
737
 
730
- async function lookup({ command, bv, extra = [], badOut = false }) {
738
+ async function lookup({ command, bv, positionals = [], extra = [], badOut = false }) {
731
739
  if (badOut) return { ok: false, text: '--out 只接受工作区内的相对路径(不能用绝对路径、盘符、~ 或 ..)' }
732
740
  if (!command || !COMMANDS.includes(command)) {
733
741
  return { ok: false, text: `用法:/bili <${COMMANDS.join('|')}> <BV> [--step 2] [--cred 凭证路径]` }
@@ -742,7 +750,9 @@ async function lookup({ command, bv, extra = [], badOut = false }) {
742
750
  ? ['login', ...extra]
743
751
  : command === 'hot'
744
752
  ? ['hot', ...extra]
745
- : [command, bv, ...extra]
753
+ : command === 'comments' && String(positionals[1] || '').trim()
754
+ ? [command, bv, String(positionals[1]).trim(), ...extra] // T34-P0-3:评论条数是第二个位置参数,别静默丢掉
755
+ : [command, bv, ...extra]
746
756
  const r = await runLook(args)
747
757
  // login 只生成二维码,真正的登录成功由面板 poll 写;命令层不记 lastRun,避免「生成成功」被读成「已登录」
748
758
  if (command !== 'login') writeLastRun(command, r.code)
@@ -774,8 +784,8 @@ function apply(ctx) {
774
784
  hint: '<search 关键词|hot|peek|view|sub|comments|danmaku|asr|frames|sheet|substrip|ocr|text|login|clean> [BV] [--p N] [--step 2] [--days 3] [--order pubdate|click] [--cred 凭证路径] [--fast] [--scene] [--grid 3x3] [--format srt|json|md|txt] [--out 文件(需配合 --format)]',
775
785
  },
776
786
  handler: async (invocation) => {
777
- const { command, bv, extra, badOut } = parseCommandLine(invocation?.rawInput)
778
- const r = await lookup({ command, bv, extra, badOut })
787
+ const { command, bv, positionals, extra, badOut } = parseCommandLine(invocation?.rawInput)
788
+ const r = await lookup({ command, bv, positionals, extra, badOut })
779
789
  return { kind: r.ok ? 'success' : 'error', text: trimForModel(r.text, command, bv) }
780
790
  },
781
791
  })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-bili-watch",
3
- "version": "0.2.28",
3
+ "version": "0.2.29",
4
4
  "private": false,
5
5
  "description": "把 B 站视频读成文本给 DSH agent 用:元数据、评论、弹幕、字幕、本地转写、抽帧 OCR;支持 b23.tv 短链与 av 号,抽帧缓存目录可换。只读命令零依赖。",
6
6
  "type": "module",
package/tools/look.mjs CHANGED
@@ -579,7 +579,9 @@ async function cmdPeek(bv, opts = {}) {
579
579
  async function grabFrames(bv, { step = 0, outDir, ffmpeg, fresh = false, p: page = 1, scene = false } = {}) {
580
580
  const v = await viewOf(bv, page)
581
581
  const useStep = step > 0 ? step : autoStep(v.dur)
582
- const dirTag = scene ? `scene-${useStep}s` : `${useStep}s`
582
+ // T34-P0-2:时长拿不到(0/异常)时别用 2 秒间隔——3 小时视频会抽几千帧
583
+ const safeStep = (v.dur > 0) ? safeStep : Math.max(safeStep, 10)
584
+ const dirTag = scene ? `scene-${safeStep}s` : `${safeStep}s`
583
585
  const dir = outDir || join(CACHE, 'frames', `${bv}-p${v.p}-${dirTag}`)
584
586
  // --out 指向用户目录时绝不整目录递归删:只覆盖本工具产物(下面会重下 mp4、清 jpg)
585
587
  // --fresh 的清理**必须等拿到锁之后**:锁外删会把别人刚写的 .lock 一起删掉,两个进程都以为持锁(2026-10-26 审查 T24)
@@ -588,13 +590,13 @@ async function grabFrames(bv, { step = 0, outDir, ffmpeg, fresh = false, p: page
588
590
  const jpgs = () => readdirSync(dir).filter((f) => f.endsWith('.jpg'))
589
591
  // 只有 manifest 说这批帧抽完了才算缓存有效;残帧(中途被打断)会重抽
590
592
  const m0 = readManifest(dir)
591
- if (!fresh && m0?.ok && m0.step === useStep && m0.scene === scene && jpgs().length > 0) return { dir, mp4, cached: true, frames: jpgs().length }
593
+ if (!fresh && m0?.ok && m0.step === safeStep && m0.scene === scene && jpgs().length > 0) return { dir, mp4, cached: true, frames: jpgs().length }
592
594
  const lock = tryLock(dir)
593
595
  if (!lock) {
594
596
  for (let i = 0; i < 60; i++) {
595
597
  await sleep(1000)
596
598
  const m = readManifest(dir)
597
- if (m?.ok && m.step === useStep && m.scene === scene && jpgs().length > 0) return { dir, mp4, cached: true, frames: jpgs().length }
599
+ if (m?.ok && m.step === safeStep && m.scene === scene && jpgs().length > 0) return { dir, mp4, cached: true, frames: jpgs().length }
598
600
  }
599
601
  throw new Error('同目录有另一个实例在抽帧,等不到结果')
600
602
  }
@@ -619,7 +621,7 @@ async function grabFrames(bv, { step = 0, outDir, ffmpeg, fresh = false, p: page
619
621
  let sceneTh = 0
620
622
  if (scene) {
621
623
  // 等间隔模式会抽多少帧,scene 模式就挑多少个变化点
622
- const targetN = Math.max(3, Math.ceil(v.dur / useStep))
624
+ const targetN = Math.max(3, Math.ceil(v.dur / safeStep))
623
625
  for (const th of [0.3, 0.15, 0.08]) {
624
626
  const pts = sceneTimes(mp4, th, bin)
625
627
  if (pts.length >= targetN) { sceneTh = th; frameTimes = pickEvenly(pts, targetN); break }
@@ -638,12 +640,12 @@ async function grabFrames(bv, { step = 0, outDir, ffmpeg, fresh = false, p: page
638
640
  } else {
639
641
  // 默认等间隔
640
642
  const pattern = join(dir, `${bv}-%02d.jpg`)
641
- execFileSync(bin, ['-y', '-threads', '2', '-i', mp4, '-vf', `fps=1/${useStep}`, '-q:v', '2', pattern], { stdio: 'ignore', timeout: 300000 })
643
+ execFileSync(bin, ['-y', '-threads', '2', '-i', mp4, '-vf', `fps=1/${safeStep}`, '-q:v', '2', pattern], { stdio: 'ignore', timeout: 300000 })
642
644
  }
643
645
 
644
646
  const n = jpgs().length
645
647
  if (!n) throw new Error('抽帧没产出图片(可能源流异常)')
646
- writeManifest(dir, { ok: true, bv, p: v.p, step: useStep, scene, sceneTh: scene ? sceneTh : 0, frames: n })
648
+ writeManifest(dir, { ok: true, bv, p: v.p, step: safeStep, scene, sceneTh: scene ? sceneTh : 0, frames: n })
647
649
  return { dir, mp4, frames: n, sceneTh: scene ? sceneTh : 0 }
648
650
  } finally {
649
651
  unlock(lock)
@@ -1070,7 +1072,7 @@ async function cmdComments(bv, ps = 10, opts = {}) {
1070
1072
  if (format === 'json') {
1071
1073
  const payload = { ok: true, source: 'comments', bv, title: v.title, count: items.length, items }
1072
1074
  const s = JSON.stringify(payload, null, 2)
1073
- if (out) { writeFileSync(out, s + '\n', 'utf8'); console.error(`✓ 已写入 ${out}(${items.length} 条)`) }
1075
+ if (out) { try { mkdirSync(dirname(out), { recursive: true }) } catch {}; writeFileSync(out, s + '\n', 'utf8'); console.error(`✓ 已写入 ${out}(${items.length} 条)`) }
1074
1076
  else process.stdout.write(s + '\n')
1075
1077
  return
1076
1078
  }
@@ -1133,7 +1135,7 @@ async function cmdDanmaku(bv, p = 1, opts = {}) {
1133
1135
  const items = all.map((x) => ({ time: x.t, mode: x.mode, size: x.size, color: x.color, text: x.text }))
1134
1136
  const payload = { ok: true, source: 'danmaku', bv, cid: v.cid, count: items.length, items }
1135
1137
  const s = JSON.stringify(payload, null, 2)
1136
- if (out) { writeFileSync(out, s + '\n', 'utf8'); console.error(`✓ 已写入 ${out}(${items.length} 条)`) }
1138
+ if (out) { try { mkdirSync(dirname(out), { recursive: true }) } catch {}; writeFileSync(out, s + '\n', 'utf8'); console.error(`✓ 已写入 ${out}(${items.length} 条)`) }
1137
1139
  else process.stdout.write(s + '\n')
1138
1140
  return
1139
1141
  }
@@ -1793,12 +1795,12 @@ async function cmdText(bv, credPath, opts = {}) {
1793
1795
  say(`[track] 字幕: miss(${s.why})`)
1794
1796
 
1795
1797
  // ② 本地 ASR 转写(无字幕时最靠谱,零 token;--fast 跳过,直接走画面)
1796
- if (!opts.fast && asrReady() && v.dur > 1800) {
1797
- say(`[track] 转写: skip(时长 ${fmtSec(v.dur)} 超过 30 分钟,转写会超时)`)
1798
+ if (!opts.fast && asrReady() && v.dur > 900) {
1799
+ say(`[track] 转写: skip(时长 ${fmtSec(v.dur)} 超过 15 分钟,转写会超时)`)
1798
1800
  }
1799
1801
  if (!opts.fast && asrReady() && v.dur <= 1800) {
1800
- say(`[track] 转写: 开始(faster-whisper,最多等 3 分钟)…`)
1801
- const a = await transcribe(bv, { timeoutMs: 180_000, p: v.p })
1802
+ say(`[track] 转写: 开始(faster-whisper,最多等 4 分钟)…`)
1803
+ const a = await transcribe(bv, { timeoutMs: 240_000, p: v.p })
1802
1804
  if (a.ok) {
1803
1805
  // 口播类正常 4–6 字/秒;低于 2 字/秒多半是背景音乐、外语歌或纯画面,
1804
1806
  // 这时候转写出来的「字」只是把杂音听成了字,继续往下走 OCR 更靠谱。
@@ -2152,7 +2154,7 @@ async function transcribeLocal(wav, tag, { model = 'small', timeoutMs = 900_000
2152
2154
  }
2153
2155
  } catch {}
2154
2156
  if (!text && existsSync(cacheTxt)) text = readFileSync(cacheTxt, 'utf8').trim()
2155
- if (!text) return { ok: false, why: `转写零字(stderr 末尾:${String(r.stderr || '').slice(-160)})` }
2157
+ if (!text) return { ok: false, why: `转写零字(stderr 末尾:${String(r.stderr || '').split(String.fromCharCode(10)).filter(Boolean).slice(-6).join(' | ').slice(-1200)})` }
2156
2158
  return { ok: true, text, chars: text.length, verify }
2157
2159
  } catch (e) {
2158
2160
  return { ok: false, why: `转写失败:${String(e?.stderr ?? e?.message ?? e).slice(-200)}` }