Repository navigation
Releases: SAOS-3624/dsh-see-now
Release list
v0.1.1
为什么跳过 0.1.0:这个版本号在 11-03 14:26 被误发过一次(内容不可用,已标废弃)。
npm 的规则是「用过的package@version永远不能再用,即使你已经把它 unpublish」,
所以这一版记为 0.1.1。
Changed — 台词早 1.3 秒出现:长句边说边出字幕
社区反馈"字幕比画面晚 510 秒"。拆开量了一遍,**转写本身只要 100180ms**,
时间全花在"等断句"上:
开口 ──等静音 0.35s / 等强切 3.0s──▶ 落盘 ──轮询 200ms──▶ 转写 180ms ──▶ 可读
└────────────── 这一段占了 90% ──────────────┘
实测(本机 paraformer-zh,真实旁白素材):
| 音频长度 | 转写耗时 |
|---|---|
| 1.0s | 99ms |
| 1.5s | 139ms |
| 2.0s | 158ms |
| 3.0s | 174~181ms |
等断句的时间是转写本身的 20 倍。 所以不再等:
- 长句说到 1.8 秒就先转一版(
live-<t0>.wav,同名覆盖),整句落盘后用准确结果
覆盖它。真机实测:快照在开口后 2.08s 落盘、整句 3.30s —— 首版字幕提前 1.2 秒。 - 轮询 200 → 100ms,净省 100ms。
- 快照是"说了一半",只在句末加「还在说…」标记,整句永远盖过快照:
createLineBuffer.upsert()按t0(句首墙上时间)认同一句,迟到的快照不许把整句改回去。
出图前等最多 300ms(listener.settle()):台词的取数窗口是「上一张图的 wallTo →
这一张的 wallTo」,差几百毫秒没转完的那句会被推到下一张 —— 那就是十几秒,
而 ASR 本身只有 0.7 秒。边界效应才是"字幕晚十几秒"的主因之一。
积压(seg ≥ 3 个)时不参与等待,免得拖慢出图。
顺带修的
- 本地转写服务自己睡了没人管:
asr.json的manage.idleMs常配成几秒,视频安静
一会儿服务就退出,再说话时第一个请求必然失败。旧逻辑要等 3 次失败才拉起(白丢 30 秒
字幕),现在第一次失败就 spawn(不 await、60 秒内不重复拉)。 status里加了延迟诊断:上一句开口→可读 X.Xs、临时字幕 N 次。
以后问"怎么半天不出字幕"直接看这两个数,不用猜。- 快照转写失败不静默了(记一条 log),但不计入连续失败 —— 它只是"提前给一版",
挂了不该把主链路暂停 30 秒。
参数走 ~/.dsh/see-now.json 的 listen 段(改完重开 DSH):
{ "listen": { "live": true, "liveMin": 1.8, "liveStep": 1.2,
"silence": 0.35, "maxSeg": 3.0, "pollMs": 100 } }live:false 退回纯整句(省一半转写调用);liveMin 别低于 1.2
(1.0 秒的片段实测会丢一半字)。
⚠️ 还有一段延迟不归 ASR 管:一张图覆盖 15 秒(maxWaitSec),第 3 秒说的话
要等第 15 秒出图时才随图交付 —— 平均 7.5 秒。要更跟得上画面就压maxWaitSec
或改用pace:"dense",代价是 token 按比例涨。
Performance — 打分快一半、出图快四成,顺手清掉一批死代码
实时链路里最贵的两段都是纯 JS(打分每帧跑、出图每 15 秒跑一次)。这次把里面的
浪费拿回来,画面逐字节不变:同一批素材新旧实现输出的 PNG 完全一致,
打分分数逐帧相等。
| 环节(798×450,i9-12900HX) | 改前 | 改后 |
|---|---|---|
| 打分 · 每帧 | 1.84 ms | 0.95 ms(-48%) |
| 打分 · 静止画面 | 1.30 ms | 0.50 ms(-62%) |
| 出一张 4 帧图 | 73 ms | 42 ms(-43%) |
| 出一张 1 帧图 | 62 ms | 39 ms(-37%) |
出图那两行是同一进程内新旧实现交替跑、每次前强制 GC、取 15 次中位数 ——
绝对值会被 GC 放大,看比例就好。
打分(cuts.js)
mcStats:先测同位置再扫邻域。旧写法dy从-radius开始,radius=4 时
同位置排在第 41 个,静止画面每格白算 40 次才命中。屏幕最常见的状态就是"没变",
这一条单独省掉 90% 的静止帧开销。bgStats:每个格子原来都new [0,0,0]一次并调TypedArray#sort
(2304 格 × 12fps = 每秒 2.7 万个短命数组),改成复用缓冲 + 手写插入排序
—— m ≤ 8 时插入排序比内建快一倍多。
出图(scale.js / sheet.js / pipeline.js)
- 帧直接缩进画布(
coverIntoAt):不再"每格 alloc 中间缓冲 → coverInto →
blit 到画布",一张 4 帧图省掉 4 次 267KB 分配 + 4 次等大拷贝。 - 1×1 / 2×2 内联:
SEAM会把"整幅铺一格"变成 797×449 这种差 1 像素的尺寸,
旧代码让每个像素都进通用平均循环求 1 个点的"平均"(还带一次除法)。 - 轴映射表按
(源长, 目标长, 裁切长)缓存,同一档清晰度的每格尺寸都一样。 - 画布底色从逐像素循环改成
buf.fill(Buffer)一次原生填充。 - 空洞掩码只在真有空洞时才建:4 帧 2×2、单帧铺满都覆盖整张画布,扫 36 万像素是白干。
- 音轨条就地画(
drawAudioStrip):composePacked直接把条高预留进画布,
不再"分配一张 1.1MB 的新图 + 整图拷贝"。 - 点阵编号直写像素:旧版每个点亮像素都调一次
fillRect,一次要做 4 次 clamp +
2 次边界判断,画一个两位数就是 200+ 次函数调用。
采集(capture.js)
- 拼帧从
Buffer.concat([pending, chunk])改成"一次allocUnsafe(need)+
按需copy"。旧写法 pipe 每给 64KB 就拼一次,一帧 1.03MB 要拼 16 次,
累计拷贝约 8.5MB/帧(12fps 下 100MB/s),全是白干。
顺手修的
- 文件源放完被当成异常(
capture.js)。source:"file"播完是code 0,而
close里除了code === null(我们自己 kill 的)一律 reject。后果不是"多一条日志":
runner只在.then()里写endedSource,走 reject 分支那个硬闸就永远不生效 ——
下一次watch会把同一个文件从头再放一遍(社区实报的"24 秒解 1955 帧、
刷几十张图、关窗也停不干净")。scripts/sim.mjs也因此在最后一步抛异常。 DEFAULTS补上floorMin/floorMax。三档节奏都显式传了,但直接
createPipeline({floorGapK: 2})时它们是 undefined → 兜底间隔算出NaN→
elapsed >= NaN永远 false → 画面静止时兜底帧一帧都不出。静默坏死,最难查。index.js的loopLine/cookieWarnLine/listenLine从route()函数体内部
提到模块顶层(原来靠函数提升才没报错,缩进还是 0)。
删掉的死代码
pack.bestPlan、sheet.headerHeight、capture.ENGINES、speech.speechObservers、
chat.lastSpoken / __resetSpokenLog / __resetChatWatcher、pipeline.plan0、
stats.msDecode / stats.msTotal / pipeline.dropped,以及 index.js 里两个
从未使用的 import。
Changed — 台词追得上画面了:断句窗口砍半,台词直接标帧号
用户实报"字幕比画面落后四五秒"。那不是转写慢(130~250ms),是断句等太久:
原来连续说话要攒到 6 秒才强切,加上静音判定 0.45 秒,最坏情况下台词要等
6.5 秒才出现 —— 画面早换了。
| 参数 | 改前 | 改后 | 效果 |
|---|---|---|---|
max_seg(强切) |
6.0s | 3.0s | 最坏延迟腰斩 |
silence(判定说完) |
0.45s | 0.35s | 短句更快落盘 |
| 扫目录 | 350ms | 200ms | 平均省 75ms |
从"说完最后一个字"到"模型能读到":约 0.6~0.7 秒(静音判定 350 + 扫盘 100 + 转写 150)。
从"开口"到"能读到":最长 3.35 秒(原 6.45)。
台词直接标帧号:每条前面加 #n,表示这句最接近第 n 格画面 ——
模型不用再拿时间码自己算,一眼知道"这句配那张图"。
台词(…):
#2 15:57:32 / 9.3s 评价一个镜头的好坏,一般来说有两个维度
#2 15:57:36 / 13.4s 第二个维度是艺术维度
代价:3 秒强切比 6 秒更容易把一句话切成两半("第一个维" / "度是技术")。
实时场景下"追上画面"比"句子完整"重要 —— 碎句模型能自己拼,晚 6 秒就救不回来了。
Changed — 插话不再被冷却挡住
自动唤醒的 3 秒冷却是防刷屏的,套在人的输入上就是画蛇添足:你说完一句、
3 秒内再补一句,第二句被静默吞掉,然后以"排队消息"的形式堆在宿主里 —— 比不叫醒还糟。
现在插话完全不受冷却限制,只有"同一拍连发"(500ms 内刚叫醒过)才放回队列等下一拍。
Fixed — 本地转写服务从来不会自动拉起("看不到字幕"的真因)
2026-11-04 实报:重启后模型说"一次都没收到台词"。查下来是三个 bug 叠在一起:
asrConfig()把manage丢了。它只挑了baseUrl / key / model / language四个字段,
而"服务没起时怎么拉起"全在manage.start里 —— 于是ensureService()永远报
asr.json 里没写 manage.start,服务永远不会被拉起。httpTranscriber把异常吞成空串。catch里return '',上层只当"这段没人说话":
失败计数永远是 0、不会停、status永远显示"在听、0 句"。服务根本没起的时候,
界面上和"视频是默片"一模一样。- 失败 3 次直接
stop()。而最常见的失败(服务没起 / 正在冷启动 30 秒)本来会自己好,
停掉等于永久放弃,还会连带 kill 采集进程、把音量条一起弄没。
修完的行为:
- 开始听时先探
GET <origin>/health,不通就按manage.start拉起(不 await,不挡采集;
实测 5.1 秒就绪)。只管拉不管关 —— 服务自己有idleMs。 - 转写失败改成暂停 30 秒后重试,文件留着不删(服务起来后接着转),目录最多留 40 段。
status的「听声音」那行会直接写在听但转写停了:<原因>,不再是含糊的"在听、0 句"。
Added — 图底下长出音轨条,台词带上本机时钟
上一版能听见了,但"听见了什么"只在文本里。这一版画到图上、把时间码写在台词旁边。
- 音轨条(
sheet.js的addAudioStrip):图底下接一条width×0.055高的带子
(高度 = 画面占行后剩下的余量,默认档 12px)。三样东西:响度柱(青绿,一眼看出是安静旁白还是配乐轰鸣)、
静音灰块(没人说话处铺深灰,柱子压在上面 —— 早先试过"静音不画柱子",灰块会变成
一根根实心竖条把波形切断)、切点刻度(顶部黄短线,via==='cut'的时刻,和宫格编号对得上)。
配色和dsh-video-storyboard逐字节一致。 - 音量数据是白捡的:
listen.py的LevelMeter复用断句已经在算的 RMS,每秒写一次
levels.json(0.1 秒一格、留 180 秒)。不像 storyboard 要再开一次 ffmpeg 跑ebur128。
纯音乐没人说话、甚至转写服务挂了,音量条照样有。
关键:levels.json按时间格子存,静音格子显式补-120dB—— WASAPI 静音时一个字节都不推,
照"有数据才写"出来的序列在时间轴上是断的,画成条就是"柱子之间一片黑"。 - 台词两个时间都给:
15:57:32 / 9.3s。本机时钟给用户回头找(实时看屏幕没有"视频进度",
浏览器放到第几秒我们不知道);相对秒和每帧那个24.1s同坐标系,模型才能把话对到格。
Changed — 画布宽度对齐 42、音轨条改成"白捡的",每张 254 → 222 token
两刀,都不损失东西:
① 宽度向下对齐 42(800→798、1066→1050、1280→1260、1600→1596)。
800 会被 pad 到 812 = 19.3 格,白算一整列;798 正好 19 列,面积只少 0.25%。
② 音轨条不跨行。token 是按 42px 一行的阶梯函数:450 高的画面占 11 行、上界 462,
底下空着 12px 也照样收 11 行的钱。所以条高取"这段余量"(450 余 12、600 余 30、720 余 36、900 余 24),
画面一点不少、条照样有、token 一行不涨。
| 档位 | 改前 | 改后 |
|---|---|---|
low(默认) |
800×494 = 254 | 798×462 = 222(-12.6%) |
standard |
1066×659 = 434 | 1050×630 = 392 |
high |
1280×790 = 610 | 1260×756 = 560 |
ultra |
1600×988 = 962 | 1596×924 = 860 |
对齐 + 白捡之后,222 比加条之前的 233 还低(因为宽度省了一列)。
副作用:low 档的条只有 12px(柱子最高 9px)。看得出"哪里有声音、哪里安静、切点在哪",
但看不出精细包络 —— 这是"不跨行"的硬约束,想要粗一点的条就得占一整行(+20 token)。
LONG_EDGE(region 模式的单格长边)故意不动 —— 那个尺寸是 cellSize() 按区域比例算的,
本来就不是 42 的倍数,改它只会连带 2/4 帧拼图,收益是个位数 token。
想更干净就打掉音轨条(但不省钱)
audioStrip: false → 798×450 = 222 token,和带条一模一样(450 和 462 都在 11 行里)。
Fixed
flushNow补的"此刻那一帧"没带wall→sheet.wallTo是 null,最后一张图取不到台词。
Added
- 按"上一句说了多长"排下一次叫醒。固定间隔(calm 30s / normal 15s / dense 8s / 逐场景 4s)
是从叫醒那一刻起算的,跟上一句说了多久没关系:上一句长的时候,下一轮会在它还在说时被叫起来。
现在默认跟随上一句——下一轮最早落在「上一句说完 + 0.8 秒气口」,并保留 3 秒地板防连珠炮。
想保持旧节奏写"speechPacing": "guard"(取旧间隔与"说完+气口"更晚的那个),写false完全关掉。
用户插话不受限制。时长按文本估(中文约 5 字/秒、英文约 14 字/秒,±20% 足够),
不需要问 TTS 引擎要音频时长。有声音适配器时以spoken(开始播放)为起点;
没有声音时以due为起点,避免文字刷屏。status的循环行会显示
上一句约 X.Xs和下一轮最早 Ns 后。
v0.0.10
Added — 它开始听得见了:视频里的台词跟画面一起交给模型
之前只看画面:视频里的人在说什么、旁白讲到哪,模型全靠猜。
see_now 返回的清单里现在多一段 台词,时间码和每帧那个 24.1s 是同一个坐标系,
模型能把"这句话"和"第几格画面"对上。
链路:examples/floating-chat/listen.py(WASAPI loopback → 16k 单声道 → 按静音断句
→ seg-<epochMs>.wav)→ Node 扫目录 → 宿主 speechToText 或 ~/.dsh/asr.json 的
OpenAI 兼容服务 → 台词缓冲 → runner.attachLines() 挂到 sheet 上。
实测(i9-12900HX / Win11):抓回环绕 0.3% 单核,转写 3 秒段 130–160ms、
10 秒段 400ms;按静音断句,一句话说完到文字可用约 1 秒。
对比画面侧 ddagrab 0.18 核 —— 音频是零头。
三级降级,永远不会让"看屏幕"起不来:宿主 speechToText(DSH 桌面版自带
SenseVoice,社区用户零安装)→ 本地 HTTP → 都没有就只出图不出字幕,
status 里写明原因(听声音:没在听 —— …)。关掉:DSH_SEE_AUDIO=0。
-
"只听"模式:循环收工之后,窗口里的输入不再扔进黑洞。以前 idle 或连续叫不醒就
dispose()整条循环,可那扇窗还摆在屏幕上 —— 用户之后打的字没有任何人读,他当然
以为自己在跟它说话。
现在 idle 时先看窗还在不在(idleAction()):窗开着就只停采集、留着耳朵(mode: 'listen'),
窗没了才真收工。窗口里一开口立刻回到 active 唤醒模型。
采集不会自动重启 —— 模型要看图会调peek,那条路才按用户原来的 region / 清晰度起管道。
为此加了pausedByListen标志:退出只听后的第一拍会看到!alive,那本来是我们自己停的,
没有标志就会被判成capture-stopped,用户刚说一句话循环就整个收工。
status里也会显示「循环:只听(…)」。 -
缺 Python 包时打印安装命令。窗口脚本要 Pillow / mss / numpy,缺任何一个都是
"启动后 700ms 退出、code=1",而插件以前只把 code 原样抛出去。现在启动全失败后会跑一次
import 探测,把缺的包名和python -m pip install ...一起报出来。 -
test/pack.test.mjs:四条排布测试(容差、flat 判据、缓存键、部分拉伸上限)。
这套原来缺位 —— 三个新行为改回旧代码,138 条测试一条都不红。
Changed
see_now返回的文本多一段台词,status多一行「听声音」。pipeline.js的帧多带一个wall(epoch ms),sheet 多wallFrom/wallTo——
台词只有时间码没有帧号,对齐只能靠同一个时钟(t是"第几秒",掉帧就漂)。
声音适配器和 dsh-watch-together 共用同一个模块(~/.dsh/voice.json 只写一份),
所以那边把 examples/voice-edge-tts 换成常驻合成器之后,这里一起变快:
| 场景 | 之前 | 现在 |
|---|---|---|
say 一句(at: null,没有预取窗口) |
2.4~2.5 秒 | ~230ms |
see_now 的事件仍然发 at: null 的 due —— 它没有时间轴,本来也预取不了。
变快不是因为提前知道台词,是常驻进程省掉了"每句重付 python 启动 + import 建连 + ffplay 启动"
这四笔固定开销。协议和 spoken / skipped 的语义都没有变。
社区(Win10)报的 5 条。核心是关窗后再也回不来,以及出了问题只给一个 exit code。
- 跟 storyboard 0.6.6 对齐三个排布行为:
DEFAULT_STRETCH0.3、flat 判据改成(1+τ)²、
逐格部分拉伸。实时性没受影响:240 帧全流程 30.5 → 29.1ms(帧预算 83ms/帧),
而且排布只在判"这张该封了"的时候跑一次,不是每帧。fill 77.3% → 83.8%,箱数 -5%。 - 画布仍是 800×450,没跟着对齐 42px:本包的
width同时是采集管道的 rawvideo 尺寸
(ffmpeg 直接按它出帧),改它要动 scale / region / PNG 三处,收益只有每张 11 token。
Fixed
-
手动重开悬浮窗之后仍然被"挂断"拦住,只能重启 DSH。
hangUp()改的是本进程内存里的一个
布尔值,而bin/see-window.mjs --force是另一个 node 进程 —— 它只杀/重开 python 窗口,
碰不到那个值。用户体感就是"窗明明回来了,它还说我把窗关掉了"。
现在start()里会判断:挂断之后 pid 文件被重写过(= 有人主动开过窗)就解除挂断。
判据刻意不用windowAlive()—— 残留进程同样为真,那样会把窗重新弹出来(正是以前修过的问题)。
新增resumeFromReopen()纯判据 + 4 条回归。 -
采集提前退出时看不出原因。以前只有
code !== 0 且 stderr 非空才算失败,code === 0
会静默 resolve —— 上层看起来和"正常收工"一模一样。屏幕采集不存在自然播完,所以现在
code 0 和非 0 一律报错,并且带上运行时长与已收帧数(12fps 下 141s/1686 帧说明是跑着跑着停的,
不是启动就崩 —— 这正是区分"缺包秒退"和"中途掉线"的关键)。 -
capture-stopped之后还在连发「有新画面了」,文案里「上一句 say N 秒前」的秒数越来越大。
lastSaidNote()用Date.now()实时算,采集停了还在造新提词,那句当然是过期的。
现在采集死后只允许第一拍把已经攒好的那张报出去,之后不再产生唤醒(队列里的图仍能peek到,不丢东西)。 -
rowOptions缓存键只编码了stretch > 0.15这一个布尔,0.2 和 0.3 共用同一份候选。
改默认容差时踩到:先算 0.2 再算 0.3,后者拿到的是没有 flat 的候选,
[3,4,4,4,7,7,7,2,2]从 1 张装下变成 2 张。改成字符串键(档位|grow|stretch|fillRows)。
三个踩过的坑(都有回归)
-
静音段 WASAPI loopback 一个字节都不推(实测 3 秒静音 callback 触发 0 次)。
所以时间码只取每块到达的墙上时间,绝不累加样本数 —— 否则静音 10 秒后一开口,
字幕整体漂 10 秒。另外不能用阻塞stream.read():静音时它会一直挂着
(实测想读 4 秒、实际卡了 19 秒),断句判据根本没机会跑。改成回调 + 队列 +tick()。 -
立体声必须先混单声道再降采样。
reshape(-1, 3).mean()一步做完会让两个声道
相位抵消,ASR 只认出"嗯。"。正确顺序reshape(-1, ch).mean(1)→ 3 点箱式平均,
纯 numpy 就够(不用 scipy / ffmpeg)。 -
它听得见自己。TTS 念台词 → 音箱出声 → 回环收回去 → 变成"视频里的台词",
然后模型接着那句话往下聊,停不下来。修的时候才发现:
quiet-until.json只有陪看在写(bin/watch.mjs拿它防两个 SAPI 抢话),
see-now 从来没写过 —— 也就是说mic.py的tts_busy()在 see-now 下一直是空转。
现在在say的due事件上挂了一个只观察、不出声的订阅者(voice:false,标 true 会顶高
speechSubscribers(),展示层就会一直等一个不来的spoken),按字数估一个保守窗口
(中文 4 字/秒 × 1.5 余量 + 1.2 秒收尾)。这是"闭嘴不听",不是"屏蔽自己" —— 窗口内整段丢弃,
代价是用户正好在说话、或视频里正好有人说话时,那一段也没了。判据还必须按段的时间窗、不能按"落盘那一刻":一句话跨过窗口末尾时,落盘时已经出窗口了,
但它的前半段仍然是 AI 的声音 —— 实测窗口只剩 0.25 秒时,整段 6 秒被当成视频台词收了进去。
为什么是 pyaudiowpatch
ffmpeg -f dshow 在这台机器上枚举不到"立体声混音"(注册表里有、设备管理器也显示启用,
dshow 就是看不到);soundcard 的 loopback 两个端点、四组采样率恒返回 0;
sounddevice(PortAudio 原生)的 WASAPI host API 里根本没有 loopback 设备
—— 官方 PortAudio 不暴露,只有 pyaudiowpatch 那个 fork 打了补丁。它也不需要虚拟声卡。
缺这个包就静默降级,status 里会说缺什么。
v0.0.7 · 心跳抖一下不再把会话判死
社区反馈(1 例):「放着放着它自己就结束了,模型说了句『好,先停在这儿』就再没下文」。只有一个人报,本来当个例放着;查下来是真 bug,而且判错一次就回不来。
窗口每秒 touch 一个心跳文件,6 秒过期就算「这窗不能用了」——这是 issue #2 修的,为的是能重开卡死的窗。常驻循环拿同一个判据去回答「用户是不是不看了」,于是:
- 机器睡一觉醒来、或窗口主线程被拖住几秒 → 心跳过期 → 循环第一拍就
stop('window-closed') - 这个停因会落一条挂断说明「用户关掉了悬浮窗……不要再调
see_now」,还会hangUp() hangUp()之后watch一律被硬闸拦 —— 这个会话只能重开
修了什么
- 拆出一个只看进程的判据 —— 新增
windowPidAlive()(process.kill(pid, 0),不碰心跳)。windowAlive()复用它,行为不变,仍然负责「这窗还能不能用」。 - 「用户不看了」另用一套判据 —— 新增纯函数
windowVerdict():进程没了 =closed(照旧停 + 挂断,尊重「关窗就别再弹」);进程还在、只是心跳断了 =stalled,连续WINDOW_GONE_TICKS(3 拍 = 6 秒)才停,而且不挂断,下一条watch能自己把窗重开起来。 - 停因可区分 ——
see_now({action:"status"})里的循环:已停(…)现在能分出window-closed和window-stalled。
3 拍这个宽限不只是把误判推迟 6 秒:醒来后 Tk 主循环 1 秒内就会重写心跳,第二拍 windowAlive() 就真了、计数归零,循环活着。只有窗口真卡死(6 秒后仍不跳)才走 stalled。
验证
- 复现:pid 指向活着的进程、只把心跳文件的 mtime 往前拨 8 秒 → 老代码第一拍就
window-closed - 新增
test/heartbeat.test.mjs6 条回归(含「心跳过期不该在第一拍就停、也不该 hangUp」),全量 102 条全过 - 真实关窗的反应时间没变:进程退出 → pid 没了 → 还是第一拍就判
closed
升级:npm i dsh-see-now@0.0.7
v0.0.3 · 修掉文件回放的无限重播
社区反馈(B 站评论):「文件回放有 bug:回放会在 24 秒里解 1955 帧、攒下一堆唤醒,然后一直弹有新画面——关了悬浮窗也停不干净,得卸载才行」。查下来是三处共同根因,都在 watch 的「采集停了就自动重启」上。
修了什么
- 文件放完不再自动重播 —— 以前
watch看到采集停止就用prevSrc重启,而对文件源来说重启就是从头再放一遍,于是无限循环。现在文件放完记为一次性来源,返回file-ended,除非你显式再传source/path。 - 关窗变成硬闸 —— 原来
onStop只调了stop(),下一条watch会把采集重启并把悬浮窗重新开起来,用户体感是「关掉又弹出来,只能卸载」。现在走hangUp(),只有显式指定来源才解除。 - 常驻循环不再被 pending 吊住 —— 原来要
!alive && !pending才收工,一次塞进去几十张图就永远取不完。现在采集停掉后最多再给两拍播报,然后收工;非实时的文件回放干脆不起常驻循环。
验证
- 新增
guardReason()纯函数(三条规矩一眼可读)+ 3 条回归测试,共 82 条全过 - 端到端:3 秒文件 → 播完 → 再调
watch直接返回file-ended,确认没有重播
升级:npm i dsh-see-now@0.0.3
v0.0.2 · 首个公开版本
让 AI 实时看见你的屏幕 —— 但只传「画面变了」的那几帧。
它解决什么
定时截屏喂给 AI,要么贵(一分钟 30 张重复图),要么瞎(30 秒一张)。这个插件先给每帧打一个 0–255 的变化分,只留「突然换了一张」和「一直在动、攒够了量」的帧,再把几帧拼进同一张图。
这一版有什么
- 单根 rawvideo 管道采集:
ddagrab0.28 核 /gdigrab0.45 核(12fps → 800×450) - 两条判据:切点(12 分地板 + 2.5× 尖峰 + 0.4s 不应期)与积分(固定 144 阈值,不随画面涨)
- 档位 + DP 装箱:格子边长按 1/k,自动铺满画布
- 三档节奏
pace:calm(20分/30s) / normal(12分/15s) / dense(7分/8s) —— 模型可以真的调 - 逐场景模式
scene:一张图只装一个画面、铺满整张 - 调试窗:实时看抓帧与拼图,不在发布包里
- 79 条测试
实测(本机 2560×1600 / i9-12900HX)
| 内容 | 变化分 |
|---|---|
| 静止桌面 | 0.00 |
| 光标移动 | 0.7 |
| 色调接近的照片切换 | 16~18 |
| 滚网页 / 切窗口 | 30~42 |
| 完全不同的照片切换 | 46~63 |
许可
PolyForm Noncommercial 1.0.0 —— 个人、学习、非商业免费;商用需授权(见 COMMERCIAL.md,个人 ¥128/年,小团队 ¥999/年)。
examples/floating-chat/ 源自同作者的 dsh-watch-together,沿用 Apache-2.0。