@orderg-x/dsh-video-preview 0.5.0 → 0.5.2

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
@@ -20,23 +20,27 @@
20
20
  >
21
21
  > **什么时候用**:「录个屏」「把视频转成 mp4」「生成一段配音」之后,想直接在对话里看、听。
22
22
  >
23
- > **安装**:`dsh plugin add @orderg-x/dsh-video-preview`(npm 包),重启 DSH。
23
+ > **安装**:DSH「插件」页 →「添加插件」,填 `@orderg-x/dsh-video-preview`(npm 包,连不上 npm 会自动换国内镜像),重启 DSH。
24
24
  >
25
- > **English:** a DeepSeek Harness (DSH) plugin that plays local video and audio inline in the chat. Files the model makes (screen recordings, transcodes, voiceovers) get a player under its reply automatically, and the `play_video` tool plays any other file. The `look_video` tool lets the model see a video itself: a contact sheet of frames, plus checks for silence, black frames and phone compatibility. Install with the `dsh plugin add` command above, then restart DSH.
25
+ > **English:** a DeepSeek Harness (DSH) plugin that plays local video and audio inline in the chat. Files the model makes (screen recordings, transcodes, voiceovers) get a player under its reply automatically, and the `play_video` tool plays any other file. The `look_video` tool lets the model see a video itself: a contact sheet of frames, plus checks for silence, black frames and phone compatibility. Install: on the DSH Plugins page, click Add plugin and enter `@orderg-x/dsh-video-preview`, then restart DSH.
26
26
 
27
27
  ![demo](assets/demo.gif)
28
28
 
29
29
  ## 安装
30
30
 
31
- ```sh
32
- dsh plugin add @orderg-x/dsh-video-preview
31
+ 在 DSH「插件」页点「添加插件」,填包名:
32
+
33
33
  ```
34
+ @orderg-x/dsh-video-preview
35
+ ```
36
+
37
+ 这是 npm 包:按包名装时,DSH 连不上 npm 官方源会自动改用国内镜像(npmmirror),不经过 GitHub。也可以填发布包地址 `https://github.com/OrderG-X/dsh-video-preview/releases/latest/download/dsh-video-preview.tgz`,那样要能连上 GitHub。
34
38
 
35
- 也可以直接装发布包:`dsh plugin add https://github.com/OrderG-X/dsh-video-preview/releases/latest/download/dsh-video-preview.tgz`。装完**重启 DSH**(客户端代码在 DSH 启动时打包,不重启看不到播放器)。DSH「插件」页里显示为「视频播放」。
39
+ 装完**重启 DSH**(客户端代码在 DSH 启动时打包,不重启看不到播放器)。DSH「插件」页里显示为「视频播放」。
36
40
 
37
41
  > **从 0.3.x 升级**:0.4.0 起包名改成 `@orderg-x/dsh-video-preview`(npm 上的 `dsh-video-preview` 是别人的包)。装上新版后,到「插件」页把旧的「dsh-video-preview」移除,再重启 DSH。旧版还在时新版不会启动,旧版照常能用;以前对话里的视频链接都还能播。
38
42
 
39
- 开发时可以直接链到本地目录:`dsh plugin --profile desktop add link:/path/to/dsh-video-preview`,改了代码同样要重启。
43
+ 开发时可以直接链到本地目录:在「添加插件」里填本仓库的绝对路径,装上的是 `link:`,改了代码同样要重启。
40
44
 
41
45
  ## 它解决什么
42
46
 
package/lib/index.js CHANGED
@@ -551,7 +551,7 @@ export function lookTool(ctx, getBrowser) {
551
551
  return defineTool({
552
552
  name: 'look_video',
553
553
  description:
554
- '看一个本地视频(或音频)的内容:在电脑上用看不见的 Chrome / Edge 打开,按时间取几帧拼成一张图交给你(每格左上角标了时间),' +
554
+ '看一个本地视频(或音频)的内容:在电脑上用看不见的 Chrome / Edge 打开,按时间取几帧拼成一张图交给你(每格上方标了时间),' +
555
555
  '再量声音(有没有声、多大、哪段没声、有没有爆音、是不是只有一边有声),顺便查手机上能不能放(编码、文件头位置)。' +
556
556
  '你做完、改完视频(录屏、剪辑、转码、配音、合成动画)后,交给用户之前先用它看一眼;用户说视频「不对」「黑屏」「没声音」「手机放不了」时也用它。' +
557
557
  `at 给几个时间点(如 ["0:12", "1:05"],最多 ${MAX_AT} 个)看那几刻的大图;from / to 只看一段;frames 改取几帧(最多 ${MAX_FRAMES})。` +
package/lib/look.js CHANGED
@@ -103,21 +103,24 @@ export function pageTools() {
103
103
  }
104
104
  const luma = (d, i) => d[i] * 0.2126 + d[i + 1] * 0.7152 + d[i + 2] * 0.0722
105
105
  /**
106
- * 一帧的亮度、反差、亮着的点占多少(千分之几,160×90 上数,黑底上的字也数得到),
107
- * 和一个 16×9 的灰度指纹(比前后帧是不是一样)
106
+ * 一帧的亮度、反差(16×9 上算)、亮着的点占多少(千分之几,160×90 上数,黑底上的字也数得到),
107
+ * 和一个 64×36 的灰度指纹(比前后帧是不是一样:录屏里只有一小块在变也看得出来)
108
108
  */
109
- function stats(sg, mg) {
109
+ function stats(sg, mg, fg) {
110
110
  const d = sg.getImageData(0, 0, 16, 9).data
111
- const fp = []
112
111
  let sum = 0
113
- for (let i = 0; i < d.length; i += 4) { const y = luma(d, i); fp.push(Math.round(y)); sum += y }
114
- const mean = sum / fp.length
112
+ const ys = []
113
+ for (let i = 0; i < d.length; i += 4) { const y = luma(d, i); ys.push(y); sum += y }
114
+ const mean = sum / ys.length
115
115
  let sq = 0
116
- for (const y of fp) sq += (y - mean) * (y - mean)
116
+ for (const y of ys) sq += (y - mean) * (y - mean)
117
117
  const m = mg.getImageData(0, 0, 160, 90).data
118
118
  let lit = 0
119
119
  for (let i = 0; i < m.length; i += 4) if (luma(m, i) > 48) lit++
120
- return { y: Math.round(mean), sd: Math.round(Math.sqrt(sq / fp.length)), lit: Math.round((lit / (m.length / 4)) * 10000) / 10, fp }
120
+ const f = fg.getImageData(0, 0, 64, 36).data
121
+ const fp = []
122
+ for (let i = 0; i < f.length; i += 4) fp.push(Math.round(luma(f, i)))
123
+ return { y: Math.round(mean), sd: Math.round(Math.sqrt(sq / ys.length)), lit: Math.round((lit / (m.length / 4)) * 10000) / 10, fp }
121
124
  }
122
125
  function label(g, text, x, y, size) {
123
126
  g.font = `600 ${size}px -apple-system, "PingFang SC", "Microsoft YaHei", sans-serif`
@@ -132,7 +135,7 @@ export function pageTools() {
132
135
  g.fillText(text, x + size * 0.4, y + h / 2)
133
136
  }
134
137
  /**
135
- * 取帧。sheet:拼成一张(cols 列、每格 cw×ch,左上角标时间);否则每帧一张大图(长边 edge)。
138
+ * 取帧。sheet:拼成一张(cols 列、每格 cw×ch,上面一条 lh 高写时间);否则每帧一张大图(长边 edge)。
136
139
  * 返回 { jpgs: [base64…], stats: [{ t, y, sd, lit, fp, ok }] }
137
140
  */
138
141
  async function frames(times, labels, o) {
@@ -146,13 +149,17 @@ export function pageTools() {
146
149
  mid.width = 160
147
150
  mid.height = 90
148
151
  const mg = mid.getContext('2d', { willReadFrequently: true })
152
+ const fpc = document.createElement('canvas')
153
+ fpc.width = 64
154
+ fpc.height = 36
155
+ const fg = fpc.getContext('2d', { willReadFrequently: true })
149
156
  const out = { jpgs: [], stats: [] }
150
157
  let sheet = null
151
158
  let g = null
152
159
  if (o.sheet) {
153
160
  sheet = document.createElement('canvas')
154
161
  sheet.width = o.cols * o.cw + (o.cols + 1) * o.gap
155
- sheet.height = Math.ceil(times.length / o.cols) * o.ch + (Math.ceil(times.length / o.cols) + 1) * o.gap
162
+ sheet.height = Math.ceil(times.length / o.cols) * (o.ch + o.lh) + (Math.ceil(times.length / o.cols) + 1) * o.gap
156
163
  g = sheet.getContext('2d')
157
164
  g.fillStyle = '#1b1d22'
158
165
  g.fillRect(0, 0, sheet.width, sheet.height)
@@ -161,12 +168,16 @@ export function pageTools() {
161
168
  const ok = await seek(times[i])
162
169
  sg.drawImage(v, 0, 0, 16, 9)
163
170
  mg.drawImage(v, 0, 0, 160, 90)
164
- out.stats.push({ t: times[i], ok, ...stats(sg, mg) })
171
+ fg.drawImage(v, 0, 0, 64, 36)
172
+ out.stats.push({ t: times[i], ok, ...stats(sg, mg, fg) })
165
173
  if (sheet) {
166
174
  const x = o.gap + (i % o.cols) * (o.cw + o.gap)
167
- const y = o.gap + Math.floor(i / o.cols) * (o.ch + o.gap)
168
- g.drawImage(v, x, y, o.cw, o.ch)
169
- label(g, labels[i], x + 6, y + 6, Math.max(13, Math.round(o.cw / 18)))
175
+ const y = o.gap + Math.floor(i / o.cols) * (o.ch + o.lh + o.gap)
176
+ g.font = `600 ${o.fs}px -apple-system, "PingFang SC", "Microsoft YaHei", sans-serif`
177
+ g.fillStyle = '#e6e6e6'
178
+ g.textBaseline = 'middle'
179
+ g.fillText(labels[i], x + 2, y + o.lh / 2)
180
+ g.drawImage(v, x, y + o.lh, o.cw, o.ch)
170
181
  } else {
171
182
  const k = Math.min(1, o.edge / Math.max(W, H))
172
183
  const c = document.createElement('canvas')
@@ -229,13 +240,16 @@ export function sheetLayout(w, h, n) {
229
240
  const gap = 8
230
241
  let cw = Math.floor((SHEET_W - (cols + 1) * gap) / cols)
231
242
  let ch = Math.round((cw * (h || 9)) / (w || 16))
232
- const tall = rows * ch + (rows + 1) * gap
243
+ // 每格上面留一条写时间,不盖住画面左上角(标题常在那)
244
+ const fs = Math.max(13, Math.round(cw / 18))
245
+ const lh = Math.round(fs * 1.6)
246
+ const tall = rows * (ch + lh) + (rows + 1) * gap
233
247
  if (tall > 1568) {
234
- const k = (1568 - (rows + 1) * gap) / (rows * ch)
248
+ const k = (1568 - (rows + 1) * gap - rows * lh) / (rows * ch)
235
249
  cw = Math.floor(cw * k)
236
250
  ch = Math.floor(ch * k)
237
251
  }
238
- return { cols, cw, ch, gap }
252
+ return { cols, cw, ch, gap, fs, lh }
239
253
  }
240
254
 
241
255
  /**
@@ -343,9 +357,11 @@ export function audioFindings(a, { duration, short = false } = {}) {
343
357
 
344
358
  const BLACK_Y = 16 // 平均亮度
345
359
  const BLACK_LIT = 3 // 亮着的点不到千分之三(黑底上有字、有画面的不算黑屏;只有鼠标指针的还算)
346
- const SAME = 2.5 // 两帧指纹平均差多少以内算一样
360
+ const CELL_DIFF = 8 // 指纹里一格亮度差这么多才算变了(压缩噪点不算)
347
361
 
348
- const fpDiff = (a, b) => a.reduce((s, x, i) => s + Math.abs(x - b[i]), 0) / a.length
362
+ /** 两帧指纹里变了的格子:64×36 里不超过 2 格(闪烁的光标、一点噪点)算一样 */
363
+ const changed = (a, b) => a.reduce((n, x, i) => n + (Math.abs(x - b[i]) > CELL_DIFF ? 1 : 0), 0)
364
+ const same = (a, b) => changed(a, b) <= Math.max(2, a.length * 0.001)
349
365
 
350
366
  /** 取到的帧 → 问题:黑屏、画面一直没变 */
351
367
  export function frameFindings(stats, { duration, short = false, sheet = true } = {}) {
@@ -368,13 +384,13 @@ export function frameFindings(stats, { duration, short = false, sheet = true } =
368
384
  if (last && sheet) issues.push({ level: 'warn', text: `最后一帧(${t(stats[stats.length - 1].t)})是黑的:结尾可能多了一段黑屏` })
369
385
  }
370
386
  if (sheet && stats.length >= 3 && duration > 2) {
371
- const same = stats.slice(1).every((s, i) => fpDiff(s.fp, stats[i].fp) <= SAME)
372
- if (same) issues.push({ level: 'warn', text: `取的 ${stats.length} 帧几乎一模一样:整段画面可能没动(录屏卡住了、录到了静止的窗口);要是本来就是静态画面就没事` })
387
+ const allSame = stats.slice(1).every((s, i) => same(s.fp, stats[i].fp))
388
+ if (allSame) issues.push({ level: 'warn', text: `取的 ${stats.length} 帧几乎一模一样:整段画面可能没动(录屏卡住了、录到了静止的窗口);要是本来就是静态画面就没事` })
373
389
  else {
374
390
  // 连着好几格一样,而且占了一大段
375
391
  let s = 0
376
392
  for (let i = 1; i <= stats.length; i++) {
377
- const cont = i < stats.length && fpDiff(stats[i].fp, stats[i - 1].fp) <= SAME
393
+ const cont = i < stats.length && same(stats[i].fp, stats[i - 1].fp)
378
394
  if (!cont) {
379
395
  if (i - s >= 4 && stats[i - 1].t - stats[s].t >= duration * 0.3) issues.push({ level: 'warn', text: `${t(stats[s].t)}–${t(stats[i - 1].t)} 画面没变(${i - s} 帧一样):要是本该有动作,可能卡住了` })
380
396
  s = i
@@ -427,7 +443,7 @@ export function formatLook({ name, size, probe, res, kind, ext = '', audioSkippe
427
443
  }
428
444
  if (res && res.jpgs.length) {
429
445
  if (res.sheet) {
430
- lines.push(`图 1:${res.times.length} 帧,从 ${fmtTime(res.times[0], short)} 到 ${fmtTime(res.times[res.times.length - 1], short)} 均匀取,从左到右、从上到下,每格左上角是时间。`)
446
+ lines.push(`图 1:${res.times.length} 帧,从 ${fmtTime(res.times[0], short)} 到 ${fmtTime(res.times[res.times.length - 1], short)} 均匀取,从左到右、从上到下,每格上方是时间。`)
431
447
  } else {
432
448
  res.times.forEach((t, i) => lines.push(`图 ${i + 1}:${fmtTime(t, short)} 那一刻(原画面缩到长边 ${BIG_EDGE} 以内)。`))
433
449
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@orderg-x/dsh-video-preview",
3
- "version": "0.5.0",
3
+ "version": "0.5.2",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "test": "node --test --test-concurrency=1 test/*.mjs"