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.
- package/index.js +17 -7
- package/package.json +1 -1
- 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
|
-
|
|
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
|
-
|
|
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
|
-
:
|
|
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
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
|
-
|
|
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 ===
|
|
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 ===
|
|
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 /
|
|
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/${
|
|
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:
|
|
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 >
|
|
1797
|
-
say(`[track] 转写: skip(时长 ${fmtSec(v.dur)} 超过
|
|
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,最多等
|
|
1801
|
-
const a = await transcribe(bv, { timeoutMs:
|
|
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(-
|
|
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)}` }
|