dsh-web
zhu1090093659/dsh-webDeepSeek Harness(DSH)Web 插件聚合生态包 · 一切皆插件,创意工坊分发(dsh-market.com)
让只有文字的智能体「看」实时屏幕或摄像头:每帧按画面变化量打分,只把真正变了的帧拼成一张带编号、带音轨条的宫格图——看视频、看演示、看游戏都行,token 只花在变了的地方。
AI 不再"听你描述屏幕",而是自己看 —— 而且只传"画面变了"的那几帧。
一个给 DSH 用的实时视觉插件:把你的屏幕(或摄像头 / 视频文件)变成一串按变化量筛选过的画面,压进一张几帧拼成的图,交给模型。
静止的屏幕几乎不花钱;一镜到底的视频按"运动量"给帧;硬切幻灯片每切一次留一帧。
Apache License 2.0 —— 随便用。个人、公司内部、闭源产品、SaaS、二次分发都可以, 不需要问我要授权,也不收费。只要保留 LICENSE 和 NOTICE 这两份文件。
如果你觉得它替你省了时间,欢迎扫码打赏 —— 纯自愿,不影响上面任何一个字:
提交 PR 前读一下 CONTRIBUTING.md —— 现在不需要签 CLA 了, 按 Apache-2.0 的规矩来就行。
调试窗(开发期用,不在发布包里):左边是抓到的每一帧,留下的带缩略图和格号,没留下的只剩一根分数条;右边是正在拼的那张图,按真实像素位置摆,新帧落格会亮一下,重排时格子会滑动。
逐场景模式(scene:true):每换一次画面单独出一张满幅图,右下角「已出图」每条都标着 1 帧 100%。
悬浮窗:AI 说的话在这里出现并被念出来(say 是唯一通道)。
外观可以在设置 › 看你看的东西 › 悬浮窗样式里换:实时玻璃(跟着桌面模糊,吃 CPU)、
静态磨砂(不抓屏,CPU 接近 0 —— 觉得卡就选它)、纯色、自动。改完立刻生效,
不用重开 DSH;它和陪看共用同一份 ~/.dsh/watch-window.json。
想让 AI 陪你看屏幕,最直觉的做法是"定时截全屏喂过去"。两个死结:
| 做法 | 问题 |
|---|---|
| 每 2 秒截一张全屏 | 一分钟 30 张图,全是重复画面,token 直接烧穿 |
| 每隔 30 秒截一张 | 中间发生了什么全丢了,AI 只会说"看起来你在用电脑" |
See Now 换了个思路:先判断这一帧值不值得看,再决定传不传。
结果:同样一段 30 秒的操作,别人烧 30 张图,这里常常只要 2~3 张。
| 场景 | 表现 |
|---|---|
| 静止屏幕(看文档、发呆) | 几乎不出图。20 秒给一张兜底帧,让你知道"确实没变" |
| 滚网页、切窗口 | 变化剧烈 → 留几帧 → 每 15 秒左右给你一张 |
| 一镜到底的视频 | 没有切点,靠"积分"攒够一个采样点才出帧 |
| 硬切幻灯片(每张停 2 秒) | 每切一次留一帧,6 张照片 → 6 帧 |
成本口径(按宿主 DeepSeek-V4 的 patch 网格算:14px patch 向上对齐 → 42px 一格 → rows×(cols+1)+2):
| 档位 | 画布(画面 + 音轨条) | 每张 token |
|---|---|---|
low(默认) | 798×450 + 12px = 798×462 | 222 |
standard | 1050×600 + 30px = 1050×630 | 392 |
high | 1260×720 + 36px = 1260×756 | 560 |
ultra | 1596×900 + 24px = 1596×924 | 860 |
一张图里塞 4 帧还是 8 帧,价格一样 —— 所以"多留一帧"几乎免费,"漏掉一帧"才贵。
宽度故意不对齐 16:9,而是对齐 42(800→798、1066→1050、1280→1260、1600→1596): 800 会被 pad 到 812 = 19.3 格,白算一整列;798 正好 19 列,面积只少 0.25%,每张省 11~24 token。 这是整个链路里唯一零代价的降本点(storyboard 那边也是这么干的)。
音轨条不额外花钱:token 是按 42px 一行算的阶梯函数,450 高的画面占 11 行、上界 462 —— 底下空着 12px 也照样收 11 行的钱。条就用这 12px(其余档位同理:600 余 30、720 余 36、900 余 24)。 所以"加条"和"不加条"的 token 完全一样,关掉它只是图干净一点,省不到钱。
每张 222 token 只是编码成本。真正吃钱的是累积:图在会话历史里是一段 base64, 一张 798×450 的 PNG 约 130 KB,base64 之后约 173 KB。
DSH 有个「图片省略」机制:单个请求里保留的 base64 图片总量超过 maxRequestImageBytes
(默认 20 MiB)时,它会把最老的图片永久替换成一段「附件路径」占位文字,然后重试。
关键词是永久 —— 它改的是会话历史本身。历史一改,DeepSeek 的前缀缓存从被改的那个 位置往后全部对不上:每省略一张老图,那一段上下文就重新计费一次。 (社区实测:图片从第 43 万 token 处才开始出现,每省略一张,后面 14 万 token 重新算钱。)
换算:20 MiB ÷ 173 KB ≈ 120 张。dense 档 110 秒出 6 张(社区实测),一小时约 200 张 ——
半小时出头就撞线;默认节奏 15~40 秒一张,也在 1 小时上下。
这个插件拦不住它:省略发生在宿主发请求之前,插件只看得到自己交付的那张图。能做的只有:
dense:出图频率是默认档的两三倍;真要根治得在宿主侧:让省略从最前面开始(连同更早的历史一起截),而不是在历史中间 挖一个洞 —— 那样前缀缓存仍然连续。这不在本插件的范围里。
已经发布到 npm,用 DSH 自带的 dsh plugin 装(它本质是 profile 维度的 pnpm 包装器 +
插件登记器,会在装前预检 engines/peer、装后自动把 dsh.bundle 写进 profile):
dsh plugin --profile web add dsh-see-now
web 是浏览器里用的那个 profile;桌面版换成 --profile desktop(那个 profile 需要先开过一次桌面端才会被初始化)。see_now
(宿主还会插一条 tool-addition 告诉模型多了这个工具)。所以「工具表在会话开头就定死了」
不成立 —— 挂不上多半是没重启,或者装的是旧副本。dsh plugin --profile web remove dsh-see-nowdsh plugin --profile web update dsh-see-now关掉悬浮窗之后想再打开:点 × 只是这一次不看,插件不会自己弹回来。手动叫:
npx dsh-see-window # 开窗(已经开着就什么都不做)
npx dsh-see-window --status # 只看它在不在
npx dsh-see-window --force # 杀掉重开
npx dsh-see-window --close # 关掉
git clone https://github.com/SAOS-3624/dsh-see-now.git
cd dsh-see-now
node scripts/install-dev.mjs # 拷进本机 DSH 的 desktop profile
install-dev是复制不是软链(Node 解析裸包名要走真实路径,软链会找不到宿主的@deepseek-ai/dsh-tools)。装完同样要重启 DSH。
| 平台 | 屏幕引擎 | 要额外做的 |
|---|---|---|
| Windows | ddagrab,不行退 gdigrab(自动探测) | 无 |
| macOS | avfoundation | 系统设置 → 隐私与安全性 → 屏幕录制,勾上跑 DSH 的 App |
| Linux | portal:xdg-desktop-portal ScreenCast → PipeWire → gst | python-gobject + gstreamer + gst-plugin-pipewire;首次弹一次选屏框(之后用 restore_token) |
「看屏幕」和「悬浮窗」是两件事,上表说的是采集引擎。悬浮窗(Tk)在 0.1.4 之前 在 macOS/Linux 上必崩(
ctypes.windll,issue #40),现在能起来了:Windows 专有的 DPI/WDA 调用都按平台收口,没有 WDA 的系统改走"抓到自己就填掉"那条路。 Retina 上窗口坐标是逻辑像素、和 mss 的物理像素会差一个缩放倍数,背景可能对不齐 —— 那类机器在设置里选frost(静态磨砂)或solid(纯色),这两档都不抓屏,不吃这个亏。
悬浮窗对解释器的要求(这块最容易踩成"窗口起不来但没日志"):
_tkinter。插件会自己跑一遍探针找解释器,一个都不合格就把
每个候选为什么不合格、以及最后一次启动的 stderr 尾巴一起报出来 —— 先看那段再动手。/usr/bin/python3(Xcode CLT 自带)就够用。homebrew 的 python 默认不带
tkinter,要单独 brew install python-tk@3.13,否则探针会报 tkinter 不可用。apt install python3-tk;另外要 python3-pil python3-mss python3-numpy
(缺哪个探针/启动报错里会点名)。~/.dsh/see-window.json 的 {"python": "…"} 或环境变量 DSH_SEE_PYTHON
(py -0p 能列出 Windows 上装了哪些)。~/.dsh/see-now-window.log。如果那个文件是 0 字节而你用的是
pythonw.exe,说明 stderr 被 GUI 子系统吞了 —— 改用 python.exe 复现一次就能看到
Traceback。ffmpeg 不用进 PATH:插件会按 DSH_FFMPEG_PATH / FFMPEG_PATH / DSH_FFMPEG_DIR /
~/.dsh/tools/ffmpeg/bin / ~/.dsh/bin 的顺序自己找(DSH 桌面版把 ffmpeg 装在第四个位置)。
Wayland 下 ffmpeg 没有正路(x11grab 抓的是 XWayland 根窗口,里面没有原生 Wayland
窗口;kmsgrab 要 root),所以 Linux 走的是 xdg-desktop-portal 的 ScreenCast:
portal 握手(一次授权,restore_token 存 ~/.dsh/see-now/portal-token.json)
→ PipeWire 节点(GNOME 上还要 OpenPipeWireRemote 拿 fd,节点在 portal 私有实例里)
→ gst:pipewiresrc → videorate → videoconvert → [videocrop] → videoscale → rgb24
依赖(缺了会直接影响能不能看屏幕,所以逐项列清楚):
| 组件 | Arch | Ubuntu 24.04 | 缺了会怎样 |
|---|---|---|---|
python3-gi(Gio) | python-gobject | python3-gi | 采集起不来:直接报错并给出对应发行版的装包命令 |
| GStreamer 基础插件 ( videorate / videoconvert / videoscale / videocrop / fdsink) | gstreamer | gstreamer1.0-tools + gstreamer1.0-plugins-base | 同上 |
pipewiresrc | gst-plugin-pipewire | gstreamer1.0-pipewire | 同上 |
| PySide6(只用于指示框) | pyside6 或 venv | 没有发行版包 → venv + DSH_SEE_PYTHON | 采集照旧,只是没有光晕和顶部状态条(SEE_NOW_INDICATOR=0 等价) |
tkinter + Pillow + mss + numpy(只用于悬浮窗,上游 window.py) | tk python-pillow python-mss python-numpy | python3-tk python3-pil python3-mss python3-numpy | 采集照旧,窗打不开;Wayland 下建议设置里选 frost/solid(这两档不抓屏) |
ffmpeg(只用于 source:"file" / "rtmp") | ffmpeg | ffmpeg | 屏幕采集不受影响 |
PySide6 装进 venv 之后,用 DSH_SEE_PYTHON=~/.venvs/pyside/bin/python 指给插件;Qt 的 xcb 插件
还需要 libxcb-cursor0(Ubuntu 上不装会 Could not load the Qt platform plugin "xcb")。
keepalive-time=250
(每 250ms 重发最后一帧),否则桌面不动时一帧都不来。region:坐标是采集流的坐标系(status 里的「采集流 W×H」)。取框窗拿到的是
Tk 的物理像素,两者比例不保证整数(实测 100% → ×1.0、125% → ×1.25、150% → ×1.5),
所以按 x/y 各自换算、再夹回流范围内。xdg-desktop-portal 卡住(表现为
握手成功但 0 帧、不报错);正常 stop 会调 Session.Close,不会留下这个状态。
真卡住了:systemctl --user restart xdg-desktop-portal(GNOME 上还要重启
xdg-desktop-portal-gnome)。window.py。它在 Linux 上要 0.2.15+(42d2f91 起 ctypes.windll
已按平台收口);-transparentcolor 这个 Windows 专有 Tk 属性还需要平台判断,
否则仍会 TclError(见 issue #40)。然后让模型调:
see_now({ action: "watch" }) // 阻塞到你攒够一张图,然后把图交回来
see_now({ action: "watch", say: "…" }) // 同时说一句话(只有 say 会进悬浮窗、会被念出来)
第一次调用约 1 秒返回当前屏幕;之后每次返回"这段时间里发生的事"。
包本身不依赖 DSH,capture / pipeline / png 都是独立导出的:
import { spawnFrames } from 'dsh-see-now/capture';
import { createPipeline } from 'dsh-see-now/pipeline';
const p = createPipeline({ width: 800, height: 450, fps: 12, queueDepth: 0 });
const run = spawnFrames({
source: { kind: 'screen' }, // 或 { kind:'file', path:'x.mp4' }
width: 800, height: 450, fps: 12,
onFrame: (f) => p.push(f),
});
setInterval(() => {
const sheet = p.take(); // 一张 PNG + 每帧的时间戳/档位/来源
if (sheet) require('fs').writeFileSync(`sheet-${sheet.at}.png`, sheet.png);
}, 200);
核心不内置任何 TTS 引擎,只 dynamic import 一个你指定的模块。配好之后,
say 会进悬浮窗并且被念出来;普通回复永远只有字。
// ~/.dsh/voice.json
{ "adapter": "/绝对路径/examples/voice-say-mac.mjs" }
自定义模型:适配器自己的配置走 options,原样交给 attach(api, options) ——
换自己训的模型不用改适配器源码:
// ~/.dsh/voice.json
{ "adapter": "D:/my-voice/index.mjs", "options": { "model": "D:/my/models/xxx" } }
macOS 自带一个:examples/voice-say-mac.mjs,走系统的 say,音色和语速从
~/.dsh/tts.json 读(和 dsh-watch-together 共用同一份):
// ~/.dsh/tts.json
{ "voice": "Tingting", "rate": 185 }
say -v '?' | grep zh_ 可以列出本机所有中文音色(Tingting / Meijia / Sinji / Reed / Rocko…)。
改完不用重启:每次调用工具前都会比对一次配置指纹(适配器路径 + tts.json 内容摘要),
变了就自动卸载重挂,没变就一次读文件的开销。所以改音色、换适配器,下一句就生效。
适配器要自己处理两件事:串行(两个
say抢音频设备会像结巴)和spoken上报 (在出声的那一瞬发,不能等念完 —— 展示层是"字跟着声音走"的)。
上一节是它说给你听,这一节是它听你在看的东西。
图只能说明画面,说明不了"他们在说什么"。开着这个之后,see_now 的清单里会多一段
台词,时间码和每帧那个 24.1s 是同一个坐标系 —— 模型能把一句话对到某一格画面上。
过去 24.1–45.6 秒的 4 个画面 · 798×462
1. 24.1s · 400×225 · cut · 2 档
...
台词(这段时间里视频里在说的,机器转写,可能有错字/听岔):
24.4s 评价一个镜头的好坏,一般来说有两个维度
31.2s 第二个维度是艺术维度
要什么:一条 WASAPI 回环(抓的是"电脑正在放什么",不是麦克风)+ 一个转写服务。
speechToText(SenseVoice),不用配任何东西;
命令行/其他宿主就在 ~/.dsh/asr.json 里指一个 OpenAI 兼容服务
(和 dsh-video-storyboard 共用同一份配置):
{ "baseUrl": "http://127.0.0.1:8000/v1", "key": "local-key", "model": "paraformer-zh-2024-03-09" }
pip install pyaudiowpatch numpy。不需要装虚拟声卡,也不需要开"立体声混音"。
装到哪个解释器都行,插件会自己找;找不到就把它写进 ~/.dsh/see-window.json:
{ "listenPython": "C:/Python313/python.exe" }
开销(i9-12900HX / Win11,实测):抓回环 0.3% 单核,转写 3 秒段 130–160ms。 跟画面采集(0.18–0.43 核)比是零头。
延迟("这句话说完" → "模型能读到它"):
| 环节 | 耗时 | 说明 |
|---|---|---|
| 静音判定 | 350ms | 要等这么久才确认"说完了" |
| 写盘 + 扫目录 | 5ms + 平均 50ms | pollMs=100,Python 是写完 .tmp 再 rename,读不到半截 |
| 转写 | 100–180ms | 实测 1.0 秒段 99ms、3.0 秒段 174ms(本地 paraformer,单线程,加并发没用) |
| 合计 | 约 0.6 秒 | 从最后一个字到文字可用 |
长句不等断句:连续说话时 --max-seg(3 秒)才会强切,这 3 秒里一个字都出不来。
所以现在说满 1.8 秒就先转一版(live-<t0>.wav,同名覆盖),整句落盘后再覆盖它 ——
真机实测首版字幕从 3.30 秒提前到 2.08 秒。临时版在台词里标「还在说…」,
整句永远盖过快照(迟到的快照改不回去)。
还有一段不归 ASR 管:一张图覆盖 15 秒(maxWaitSec),第 3 秒说的话要等第 15 秒出图时
才随图交付 —— 平均 7.5 秒。要更跟得上画面就压 maxWaitSec 或用 pace:"dense",
代价是 token 按比例涨。watch 返回前还会等最多 300ms(listener.settle),
把"差几百毫秒没转完、会被推到下一张"的那句捞回来。
要更快就把 listen.pollMs 调小(最低 50,再低只是空转),
或者把 listen.silence 从 0.35 再降到 0.25 —— 代价是句子更容易被切成两半。
这些参数都在 ~/.dsh/see-now.json 的 listen 段,改完重开 DSH。
图上长出了音轨条:图底下多一条带子(默认档 12px 高),画响度柱、静音灰块和切点刻度 ——
配色和 dsh-video-storyboard 一样。数据不用重新解码一遍音频:listen.py 断句时已经在算 RMS,
顺手按 0.1 秒一格记下来(levels.json),画的时候直接取。
它不额外花 token:条高取的是画面占行后剩下的余量(450 余 12),总高 462 仍在 11 行里。
台词的时间码给两个:15:57:32 / 9.3s —— 前面是本机时钟(回头对"当时几点"用),
后面是相对这张图起点的秒数(和每格画面同一个坐标系)。
没有或不想用:DSH_SEE_AUDIO=0 关掉;或者干脆不装那个 Python 包。
两种情况都只是没有字幕,看图一切照旧 —— status 里的「听声音」那一行会直说是
缺服务、缺包、还是这台机器抓不到回环:
听声音:在听(宿主 SenseVoice,累计 42 句,最近一句 3s 前)
听声音:没在听 —— 没有能用的 Python(要 pip install pyaudiowpatch numpy)
已知限制:
quiet-until.json(和 mic.py 共用),念的时候整段丢弃 —— 否则 AI 会把自己的话
当成"视频里说的",然后接着聊下去。| 参数 | 说明 |
|---|---|
action | watch(默认,阻塞取图)/ peek / start / stop / status / window / region / help |
show | 配 action:"window":true 把悬浮窗叫回来,false 藏起来,不传只报状态 |
region | 只看屏幕上一块:"x,y,w,h"(屏幕物理像素),或 "off" 取消。一般不用手填 —— 说"框这块"让它自己框,见下 |
say | 唯一能进悬浮窗、能被念出来的通道。普通回复不进气泡,但会被记进小窗的对话记录 |
quality | low(800×450) / standard / high / ultra。改它会重启采集(~0.6s),并立刻返回当前这一帧 |
pace | 节奏:calm / normal(默认)/ dense。见下 |
scene | true = 逐场景模式:一张图只装一个画面、铺满。见下 |
seconds | watch 的阻塞上限(默认 300,封顶 860)。只是上限,攒够一张就提前返回 |
minFrames | 少于这么多帧的图就别给我了(省 token) |
source / path / realtime | 采集源:屏幕 / 摄像头 / 回放一个视频文件 |
see_now({action:"help"})返回一份完整手册(各节奏的具体门槛、档位换算、区域成本、status每行什么意思、变化日志怎么用)。工具契约里只留"什么时候调、默认值是多少": description 是每一轮请求都要重发的常驻文案,"为什么/多少 token/实测数字"搬进手册现取, 不确定时调一次help就行。
region / action:"region")只想让它盯住屏幕上的一个窗口(手机竖屏通话、播放器的画面、某个聊天窗口)时,
不用自己量像素:说一句"框这块",模型会调 see_now({action:"region"}),
屏幕变暗后拖一个框,松手就行。坐标存在 ~/.dsh/see-now/region.json,
之后每次 watch 默认只看这块,region:"off" 取消。
三条边界(插件会如实告诉模型,模型也该如实转述):
source: { kind: "obs" } 或 { kind: "camera", device: "..." },走 dshow 设备;设备名带空格(
OBS Virtual Camera)时如果报input url video=OBS这类"名字被截断"的错, 把它换成 DirectShow 里的无空格备用名(@device_sw_{...}\{...},在 ffmpeg 的-list_devices输出里能看到)传给device。
带上区域后出图规则也变了:按区域宽高比出图(竖屏就出竖屏)、每帧长边固定 = 档位长边、 一张默认 2 帧最多 4 帧。token 会比全屏贵(帧不再被缩小):low 档 2 帧约 345 token。
pace)| 档 | 换画面门槛 | 最多多久叫醒一次 | 适合 |
|---|---|---|---|
calm | 20 分 | 30 秒 | 日常挂着,只想知道"大场面变了没" |
normal(默认) | 12 分 | 15 秒 | 滚网页、切窗口不会每下都留帧 |
dense | 7 分 | 8 秒 | 幻灯片、逐帧播报 —— 贵,别常开 |
觉得它太吵、太烧钱,就说一句「慢一点 / 省点钱」,模型会传
pace:"calm"。 觉得它漏了、迟钝,就说「不灵敏」,它会传dense。
scene)打开后每换一次画面单独给你一张满幅图,并且每换一次就叫你一次 —— 用来做"每翻一页说一句这是什么"。
see_now({ scene: true, say: "好,接下来每换一个画面我就说一句" })
它是粘性的,一直生效到 scene:false 或停止。
单帧铺满靠的是 strategy: 'single'(档位固定压到 1),不然场景一密,格子会缩到 1/4、1/9,中间全是灰条。
~/.dsh/see-now.json有三件事是"每次会话都一样的偏好",不该让模型每次传参:多久自己收工、
默认用哪一档节奏。写进这个文件(改完重开 DSH 生效,不进 profile,
dsh plugin update 不会覆盖):
{
"maxAutonomousWakes": 24,
"idleStopMin": 10,
"defaultPace": "calm",
"speechPacing": true
}
| 字段 | 默认 | 说明 |
|---|---|---|
maxAutonomousWakes | 8 | 连续这么多次自动叫醒都没人理 → 收工。悬浮窗开着时,这条就是实际的最长陪看时长(normal 15 秒一次 ≈ 2 分钟)。1–200 |
idleStopMin | 3 | 没开悬浮窗时,用户这么久没说话 → 收工。0 = 关掉这条 |
defaultPace | normal | 新会话/重开采集时的默认节奏:calm / normal / dense / game-economic。调用里显式传 pace 仍然优先 |
speechPacing | true | 按上一句的估计时长排下一轮(中文约 5 字/秒)。"guard" = 只在长句时推迟、不比旧节奏密;false = 退回固定间隔 |
changeLog | false | 交付时在图下面附一段「这期间屏幕怎么动的」(每张图一行:时刻 / 变化分 / 方位 / 占屏)。queueDepth 挤掉的画面只有这里能看到。每次交付约 +100 token,默认关 |
tierMode | density | 格子大小按什么算:density 看过去 1 秒平均每秒几帧;weight 按每帧变化分切面积(同图内会出现大小对比,省 28~45% token,但一张图覆盖的时长变长、出图变慢) |
tierCurve | docx | 七道阈值取哪一套:docx 原始 / perceptual 按人眼瞬时记忆定标(一格 3 s ~ 0.3 s)/ custom 走 tierThresholds |
tierThresholds | [0.26,0.6,0.9,1.2,1.8,2.4,3.0] | tierCurve:"custom" 时生效:7 个帧/秒的数,必须严格递增(不递增会整份退回默认 —— 滞回带互相盖住会让档位卡死) |
tierStyle | standard | 映射:standard(1 档压成 1/2)/ native 原样 / full 整体降一档 / compact 升一档 / custom 走 tierMap |
tierMap | [1,2,3,4,5,6,7,8] | tierStyle:"custom" 时生效:第 i 个数 = "档位 i+1 映射到哪一档" |
为什么要 changeLog:出图队列只留最新一张("宁可丢老画面,不给模型看十几秒前的事")。
这在陪看视频时是对的,但配合操作电脑时不够 —— 模型点完一下、或者上一轮思考的十几秒里
屏幕变了什么,回来时只剩一张"现在"的快照。打开这条之后,每张图(包括被挤掉的那些)都会
留一行摘要:
变化日志(3 段;图被挤掉时只有这个):
12.3s · 分 0.3 · 静止
27.4s · 分 35 · ⚡切场景 · 中右 51%
43.0s · 分 12 · 变化 · 中心 23%
它只记"变了多少、哪里变",不记"变的是什么"(那还得看图)。摘要按 changeLogMax(默认 60 条)
滚动淘汰,取走即清,不会在同一段里重复。
为什么要 speechPacing:固定间隔是从"叫醒那一刻"起算的(calm 30s / normal 15s / dense 8s / 逐场景 4s),
跟上一句说了多久没关系。现在默认改成跟随上一句:上一句 3 秒,下一轮最早 3.8 秒后(有 3 秒地板,防连珠炮);
上一句 12 秒,就顺到 12.8 秒。想保持旧节奏就写 "guard"(取旧间隔和"说完+气口"里更晚的那个),
写 false 完全关掉。用户插话不受限制。
估时长只看文本,误差约 ±20%(不同引擎/语速),目的是"跟住嘴",不需要精确。
see_now({action:"status"}) 的循环行会显示 上一句约 X.Xs、下一轮最早 Ns 后。
see_now({action:"status"}) 的「收工线」那一行会显示当前生效的值,
改过配置会带上文件路径 —— 否则"怎么陪一会儿就自己停了"很难查。
非法值会退回默认并在日志里留一条 warning,不会把插件弄挂。
两条判据,缺一不可:
① 切点 —— 这一帧的分数跳得又高又孤立
score ≥ cutAbs(12) // 绝对地板:变化够不够大
score > 2.5 × max(1, 前后帧) // 尖峰:是不是"一下子"
mc ≥ cutSpikeMcAbs(20) // 尖峰也要有 mc 覆盖,防过渡帧先切
距上一刀 ≥ 0.4 秒 // 不应期
// 补充出口(相机/机位场景)
score ≥ 6 && mc ≥ 60 // 运动补偿:整屏在动但邻域匹配不上 = 换镜头
score ≥ 80 && 跳 ≥ 12 && ≥ 1.5×邻居 // 强台阶:兜住 mc 关掉的配置
6 ≤ score < 12 && score > 3×邻居 && mc ≥ 8 && 包围盒 ≥ 20 && 填充率 ≥ 0.2 // 相似构图:压缩块,不是稀疏 UI 条
mc 是运动补偿后的"变了多少格"占比(0–255),搜索半径默认 4 格: 整屏平移 8 格时 radius=2 会误报 194,radius=4 只有 5;换镜头则两边都高。 B 站游戏片里 30.33s 的切机位只有 23.2 分,但 mc=121,靠这条能留下。
前景运动(手、勺子、遮挡物)也会让 mc 变高,所以低/中分切点再加一道背景模型:
最近 6 帧里一直没怎么动的格子算稳定背景;如果当前帧只改掉一部分稳定背景
(bg < cutBgAbs=100),绝对变化格数不超过 cutBgMaxChanged=600,
且变化区域没盖满高度(mcSpanY < 0.85),
就按前景运动处理,不切。真正换镜头会把稳定背景整片换掉,或至少盖满画面高度。
宽银幕的上下黑边不计入这个高度(contentHeightRatio 归一),否则 Sintel 这类
letterbox 片子的正常切点会被误挡。全黑帧仍按 0 跳过;暗场上下发黑、中间有亮内容时
改用 guardHeightRatio,不再因为边缘暗就跳过守卫。
高分候选(score ≥ 60)再等 0.5 秒做一次未来帧确认:用最近 24 帧的参考背景
看候选帧改掉的部分有没有回来。手/勺子离开后画面回到旧背景,就不切;真正换镜头
在新场景里稳定下来,仍然切。这项确认只作用于高分候选,低/中分仍走当前帧守卫。
前景停住超过 0.5 秒时,未来帧不会回到旧背景,但外圈通常还是旧背景:
bgKeep 之外再看 bgBorderChanged,背景变化不到整片且外圈几乎没动时也判前景。
这条只在 bg < 220 且外圈 < 10 时生效,避免挡整屏换镜头的真切点。
外圈已经判成前景的候选不再等未来帧;真正的高分切点仍最多等 0.5 秒。
低/中分候选如果外圈也被改掉(border ≥ 60)、稳定背景很少(≤ 700),
且背景变化不到整片(bg < 160;bg ≥ 220 且稳定背景 ≤ 300 也算),
也等 0.5 秒做一次未来帧确认;只有一部分旧背景回来
(keep < 210;稳定背景 ≤ 300 时放宽到 220)才判前景。
如果未来帧仍是整屏新画面,仍会切。这是在真实带货直播里
大手/产品盖住外圈时补的,静态外框的烹饪/开箱真剪不会被这条带走。
要整条关掉这个低分确认(只留高分路径):cutFutureLowBgMax: 0。
注意上面括号里那个 bg ≥ 220 且稳定背景 ≤ 300 是独立出口 ——
它曾经排在 bg ≥ cutFutureLowBgMax 之外,所以设 0 只关掉 bg < 160 那半边。
② 积分 —— 分不高但一直有,攒够才录一帧
need = 4分 × 12fps × 3秒 = 144 // 固定的绝对阈值,不跟着画面一起涨
每帧累加自己的分,累够就录、清零
为什么两个都要:尖峰是相对的,在低分区也会成立。实测缓慢放大(持续运动)有帧 s=13.6、前后邻居只有 1.1/1.3,尖峰判据完全放行 —— 没有绝对地板,它就会被切成一块块。
反过来只有地板也不行:一镜到底的视频每帧 4 分,永远不会"跳"。
npm run debug:live # 看屏幕
npm run debug:live -- --source=file --file=x.mp4 --realtime=1
npm run debug:live -- --source=file --file=x.mp4 --scene # 看逐场景模式
Windows 上可以直接双击 debug/调试窗.cmd,跟着菜单走。
窗口里有三样东西:帧流(每一帧都有一条,留下的带缩略图和格号)、正在拼的 sheet(按真实像素位置摆,新帧落格有动效)、两条曲线(每帧变化分 + 积分累积/阈值)。
代码在 debug/,不进 npm 包、不进插件,只是从外面驱动同一条流水线。
采集开销(12fps → 800×450):
| 源 | 首帧 | CPU |
|---|---|---|
ddagrab(桌面复制 API) | 370ms | 0.28 核 |
gdigrab(GDI 抓屏) | 310ms | 0.45 核 |
| OBS 虚拟摄像头 | 575ms | 0.13 核 |
| 源 | 首帧 | CPU |
|---|---|---|
avfoundation 屏幕 | ~700ms | ~0.16 核 |
首帧慢在 AVCaptureSession 冷启动(ffmpeg 进程本身 ~300ms),但稳态比 Windows 两种引擎都省。
像素格式是探针试出来的,不是写死的:屏幕输入不支持默认的 yuv420p,探针按 CPU 从省到费
(nv12 → uyvy422 → bgr0 → 设备默认)试到第一个真能出帧的,然后整个进程记住。
macOS 上有两个坑会伪装成"插件坏了",都写在
src/capture.js里: ①-framerate在 avfoundation 屏幕源上不压速(实测 30 帧只用 0.73 秒,≈40fps), 必须靠输出侧-r 12把速度压回来 —— 开发时少了这一条,管道被灌出过 240GB 的 raw 文件。 ② 没有「屏幕录制」权限时它不报错,exit 0、帧数正常,只是全黑。 探针会算平均像素,发现全黑就在start的返回里带一句提示,告诉你去系统设置里勾。
变化分刻度(0–255,同一套代码):
| 内容 | 分数 |
|---|---|
| 静止桌面 | 0.00(屏幕采集近似无损) |
| 光标在静止画面上移动 | 0.7 |
| 两张色调接近的照片切换 | 16~18 |
| 滚网页 / 切窗口 | 30~42 |
| 两张完全不同的照片切换 | 46~63 |
所以 cutAbs: 12 这个默认值的位置是:照片切换抓得住,光标和微小抖动碰不到。
src/
capture.js 唯一 spawn ffmpeg 的地方,一根 rawvideo 管道
binaries.js 找 ffmpeg(DSH 桌面版不进 PATH,所以不能写裸命令)
cuts.js 逐帧变化分(64×36 HSV 网格)
pipeline.js 唯一有状态的地方:帧 → 打分 → 切点/积分 → 留帧 → 档位 → pack → 出图
tiers.js 档位(格子边长 1/k)与策略
pack.js DP 装箱:把 N 个不同大小的格子铺满画布
sheet.js 合成 PNG(烧帧号、留缝)
runner.js 采集管线 + 阻塞取图 + 节奏
listen.js 听声音:扫 wav → 转写(宿主/本地 HTTP)→ 台词缓冲 → 挂到图上
loop.js 常驻循环:有新图就唤醒模型
index.js 工具注册与路由
examples/floating-chat/ 悬浮窗(Tk)+ listen.py(抓回环、按静音断句)+ mic.py(麦克风)
debug/ 调试窗 + audio-check.mjs(手工验听声音,不在 npm 包里)
test/ 181 条测试
dupMin 那道粗筛)。它不保证给你的每一帧都"有新信息"。bg ≥ 220 时仍挡不住。反过来,真正切到
"外圈固定、主体只在中部变"的近景,可能被这条误挡。pace 里的间隔 —— 语音播报是串行的,更快只会排队。ddagrab / gdigrab)和 macOS(avfoundation)都实测过;
Linux 走 portal(无桌面环境、或没装 Screencast 后端时会直接报错,不假装能用;
首次要授权一次,KDE/GNOME 的行为略有差异,见上面「Linux 怎么抓」)。Apache License 2.0 —— 商用、闭源、再分发、改完再卖都可以,保留 LICENSE 与 NOTICE 即可。
examples/floating-chat/ 来自同作者的另一个项目 dsh-watch-together,两边现在是同一个协议,
不再有"这个目录 Apache、其余非商业"的分裂。
愿意打赏就扫上面那个码 —— 不打赏一样能用全部功能,没有任何东西锁在付费后面。
Copyright © 2026 SAOS-3624