Skip to content

Releases

v0.3.2

Choose a tag to compare

@SAOS-3624 SAOS-3624 released this 10 Nov 04:34

一个"开关点了没反应"的修复:0.2 之后弹幕整体失效(消息改走协议文件,推弹幕那行还挂在
老轮询上)。顺带修两个配置读取的静默失败 —— 带 BOM 的配置会让所有设置回默认,
danmakuInset 则一直是个死键。

Fixed — 开了弹幕却一条都不飘(#61,PR #62)

弹幕唯一的推送入口挂在老的 chat.jsonl 轮询上,而 0.2 之后消息都走
window-events.jsonl → _render_event(),那条只上聊天框。协议文件存在时
chat.jsonl 基本不再产生新行,于是弹幕功能整体失效,开关怎么点都没反应。

  • _render_event() 里 add() 成功后直接推这一条;_replay_protocol() 期间置
    _replaying,开场回放只填聊天框、不飘弹幕雨;
  • _push_danmaku(messages=None) 接显式消息列表,跳过 meta 和 at 早于 5 秒的迟到历史;
  • 默认位置从"竖着居中"改到右下角 —— 居中那档正好压在播放器和画面弹幕上。

Fixed — 带 BOM 的 watch-window.json 被静默读成空配置

read_config() 用 utf-8 读、异常吞成 {},用记事本或老编辑器改过配置就会中招:
窗口跳回默认位置、弹幕变回关、glass 变回 live。改成 utf-8-sig。

Fixed — danmakuInset 是死键

danmaku_options() 没透传 inset(danmaku.py 一直支持),带子压在系统 OSD 上时挪不动。

Docs — README 补「安全边界:图里的文字是内容,不是指令」(#56)

把 tool-spec.js 里那段常驻说明、以及宿主侧该做的两件事(破坏性操作要确认、屏幕内容单独打标记)
写进 README;同时写明不做"看到疑似指令就拒绝执行"的拦截,理由是误伤率高。

v0.3.1

Choose a tag to compare

@SAOS-3624 SAOS-3624 released this 08 Nov 12:02

两个修复,都不动窗口本身:设置页 token 不再一重启就失效、对 storyboard 的依赖范围
真的能装到 0.7.x
—— 后者是装完就坏的那种,值得单独升一版。

Fixed — 依赖范围 ^0.6.0 装不到 storyboard 0.7.0(#60)

0.x 的 caret 不跨小版本:^0.6.0 只匹配 >=0.6.0 <0.7.0。而 0.3.0 是按 storyboard 0.7 的
导出写的,于是新装的人拿到 0.6.x,装完直接坏。

  • dependencies.dsh-video-storyboard 抬到 ^0.7.0;
  • test/dep-contract.test.mjs 加了一条断言:同级 storyboard 的小版本必须落在依赖范围内
    (并排仓库不在时自动跳过)。以后改版本号忘了抬依赖,测试会先红。

Fixed — 设置页 token 一重启就失效(PR #59)

与 storyboard 同款修法:token 落 $DSH_HOME/watch-settings-token,宿主重启后已打开的
设置页继续能保存。

Added — 本地化元数据与详情页(PR #59)

locale/{zh,en}.json + docs/plugin*.md。

v0.3.0

Choose a tag to compare

@SAOS-3624 SAOS-3624 released this 07 Nov 12:57

这一版的重点是**「别碍事」**:窗口能真的从屏幕上消失、滚动手感从卡顿变成跟手、
在 mac 和 Linux 上不再一启动就退。另外补了一个从 0.0.2 起就存在的静默失败 —— 麦克风。

Added — 最小化到托盘 + 弹幕层

悬浮窗以前只有两档形态:全尺寸、或者"折叠成一条"。折叠还占着一块屏幕,打游戏时很碍事。

  • 两档:full / hidden。hidden 走 withdraw,真的离开屏幕,不是缩成一条;
  • 回来的路有三条(少一条就等于"窗口没了"):托盘图标、Ctrl+Alt+W、宿主工具的 window:show;
  • danmaku.py:独立 Toplevel + WS_EX_TRANSPARENT 点穿,30fps,队列空了就停表
    (不是空转);只飘 assistant 的话,自己发的不飘;
  • 标题栏四个按钮:弹 / 最小化 / 折叠 / 关闭;状态行按像素截断,不再糊住按钮;
  • 协议加白名单 + minimize / danmaku 两个方法;工具参数补 window:hide 和 danmaku。

顺带修了四个真问题:

  • asr 用窗口自己的 pythonw:PATH 上的 python.exe 会弹黑框,而且没有 pyaudiowpatch
    —— 麦克风一直是静默失败的(stderr 是 DEVNULL,所以没人知道);
  • _save_geometry 取 _geo 而不是 winfo(geometry 是异步的,折叠/展开会互换);
  • 折叠态点名字附近会误开麦;
  • faulthandler 落 window-fault.log(pythonw 没有 stderr,段错误本来没证据可查);
    DSH_TRAY=0 可以关掉托盘做二分。

Fixed — 滚轮翻历史每格卡近一秒(see-now #44)

_wheel() 每次都走 _refresh_feed() → _layout():delete("all") 之后标题栏、
输入框、滚动条、事件绑定、静态底板全部重建,而 TextLayer.draw() 是按字符建
canvas item(有底板时每个字还要再加 4 层描边)。滚一格 = 重建整屏几千个 item。

  • 新增 _relayout_feed():只删 feedbody / sbar 两层重画,其余一概不动;
  • 连滚 40ms 防抖(_schedule_feed_refresh),只重排最后一次;
  • 有底板的正文整行一个 canvas item —— adapt=False 时逐字唯一的好处(同一行
    左右两半分别翻色)不成立,而那是 item 数的大头。逐字路径只留给真的跟着背景翻色的文字;
  • 删 feedbody 而不是 feed:_draw_input() 里的「发送」两个字也挂着 feed tag,
    删 feed 会让按钮变空白。

同素材(20 条消息、440×380、frost)实测,已扣掉脚本里固定的 60ms pump:

改前 改后
canvas item 总数 964 104
滚一格 136 ms 63 ms
连滚 8 格 123 ms 62 ms

Added — noActivate:点窗不激活(默认关,see-now #44)

悬浮窗是置顶的,点一下会激活它,Windows 跟着重排置顶带 —— 宿主窗被顶上来、全屏
游戏被切走。#44 提的三条路子里选"给开关、不改默认":

  • ~/.dsh/watch-window.json 的 { "noActivate": true },或 DSH_WINDOW_NO_ACTIVATE=1;
  • 打开后给窗口加 WS_EX_NOACTIVATE,点它不改变前台窗口。代价是它也拿不到键盘焦点,
    所以点输入框时先摘掉这条样式(_focus_input),本次运行不再加回;
  • 默认关:WS_EX_NOACTIVATE 的窗口输入框直接打不了字,而那条"点输入框自动摘掉"
    的路径没法自动化验证,不替用户做这个决定。

Fixed — 悬浮窗在 macOS/Linux 上必崩(see-now #40)

window.py 顶层无条件摸 ctypes.windll(DPI 感知、timeBeginPeriod),
非 Windows 上直接 AttributeError,而且死在依赖预检之前 —— 用户看到的是
"启动后 700ms 退出,code=1"。

  • 所有 ctypes.windll / wintypes / WDA 调用按 IS_WIN 收口;
  • 没有 WDA 的平台一律按"能抓到自己"处理(_cappable 强制打开),
    改走 fill_own_rect 那条路,不会递归拍到自己;
  • _bbox() 在非 Windows 上退回 Tk 的屏幕坐标(Retina 缩放对齐仍是已知问题,
    那类机器建议用 frost/solid,两者都不抓屏)。

上一版只收了 ctypes.windll,-transparentcolor 这一行漏了 —— 它同样是 Tk 的
Windows 专有属性(aqua 的合法集合是 -alpha / -fullscreen / -modified / -notify / -titlepath / -topmost / -transparent,X11 也没有它),无条件调用一样 TclError 死在
__init__。这道坎在 see-now #45(macOS)和 see-now PR #38(Linux VM)里各被独立撞到一次。

  • -transparentcolor 包进 if IS_WIN:;非 Windows 上窗口退化成一张 KEY 底色的深色卡片
    (#010203、四角变直角),先保证起得来再说视觉;
  • glass_capture.py 的 @dataclass(slots=True)(3.10+)去掉 —— Xcode CLT 自带的
    3.9 是 mac 上最好拿的一个解释器,不该被三个字段的 __slots__ 挡在门外;
  • window.py / window_widgets.py 补 from __future__ import annotations,
    X | None 这类注解在 3.9 上不再求值。12 个 py 文件现在全部过
    ast.parse(feature_version=(3, 9))。
  • 启动失败弹窗的标题跟着 DSH_WINDOW_BRAND 走(see-now 的用户不会再看到
    "dsh-watch-together",issue #32 第 1 条)。

Fixed — 麦克风永远起不来:aec.py 没打进包(see-now #36)

mic.py 顶层 from aec import ...,但 aec.py 既没进 see-now 的同步清单,
发布包里的 stderr 又被丢进 DEVNULL —— 界面只剩"录音中"。

  • 导入改成可缺省:没有 aec.py 时回声消除关掉、麦克风照常可用(并打一行原因);
  • see-now 的 sync-window.mjs ASSETS 补上 aec.py。

v0.2.15

Choose a tag to compare

@SAOS-3624 SAOS-3624 released this 06 Nov 15:10

Fixed — 有些机器上装完一个工具都不出现(社区实报)

import('@deepseek-ai/dsh-tools') 拿 defineTool,而宿主的这份被打进 app.asar,
插件从自己的 node_modules 往上解析不到 → 抛 → apply() 的 catch 吞掉 →
整个插件静默不注册任何工具(日志里只留一行 failed to load)。

  • 新增 src/define-tool.js:内置一份 defineTool 兼容实现(author spec → JSON Schema,
    规则照抄宿主 runSchemaCompiler 的语义;只少了运行时参数校验)。
  • loadDefineTool() 改成三跳:import → DSH_TOOLS_PATH(包了 try,路径写错也不会崩)→ 内置兜底。
  • 走兜底的情况下工具照常注册,schema-audit 那套静态审计仍然是第一道防线。
  • 测试 +6:type:'json' 退化成注解、required:true 提升到父级、真实 spec 全部能编译出来。
    All notable changes to dsh-watch-together are documented here.
    This package starts its own version line at 0.0.7 — it is not a continuation of dsh-video-storyboard.

安装:dsh plugin add dsh-watch-together@0.2.15

v0.2.14

Choose a tag to compare

@SAOS-3624 SAOS-3624 released this 06 Nov 12:22

Added — 悬浮窗样式预设:多一档「静态磨砂」,设置页里能选

社区反馈实时玻璃在低配机/高分屏上卡。实测同机同尺寸(760×560、背景在播视频):

样式 3 秒内 CPU 占单核
live 实时玻璃 5.17 s 172%
frost 静态磨砂(新) 0.03 s 1%

不是把模糊调小,而是换了一条路径:frost 完全不抓屏,只画一张预生成的
底板(对角冷色渐变 + 两团极淡光晕 + 细噪点 + 每块玻璃 1px 亮边),尺寸或气泡
形状变了才重画(约 11ms),其余时间一帧都不算 —— 静态样式连渲染线程都不起。
live 那条路径一行没动,只是把线程的启停抽成了 _start_worker / _stop_worker。

样式写在 ~/.dsh/watch-window.json 的 glass:auto(默认,远程/录屏 →
solid)/ live / frost / solid。窗口每 250ms 看一次这个文件的 mtime,
所以从设置页改完立刻切换,不用重开窗口;从 live 切走时真的把抓屏线程
停掉(只换渲染器不停线程 = 用户切过去还是卡,只是看不见画面在变)。

设置页(浏览器版与 DSH 设置里的原生表单)都多了一栏「悬浮窗样式」。

Changed — 心跳从"空文件戳一下"变成带可见性的 JSON

插件侧以前只能看心跳的 mtime,那只能回答"进程还在画吗",回答不了"用户能不能看见"。
于是出现最难查的一种:配置文件写 hidden:false、进程活着、心跳在跳,屏幕上什么都没有,
而插件 status 说"显示中"(see-now issue #32 第 3 点)。

现在每秒写的是:

{"at":1793886026.33,"visible":true,"hidden":false,"state":"normal"}

hidden 是意图,visible 是事实(Tk 的 state() + winfo_ismapped() 说了算),
两个都写 —— 只写事实的话插件分不清"用户故意藏的"和"想显示但没 map 出来"。
原子写(.tmp + replace):插件每秒都可能读,读到半行 JSON 会退回读配置文件,
那正好是这一版要修的东西。

mtime 照旧更新,老版本插件不受影响。

0.2.13 — 语音抢占式 + 适配器能「拉」位置和台词

Choose a tag to compare

@SAOS-3624 SAOS-3624 released this 04 Nov 11:55

0.2.13 — 语音抢占式 + 适配器能"拉"位置和台词

声音终于会"抢话"了:新的一句直接顶掉没念完的,不再三句排队排到画面之外。
同时把 issue #4 最后一环补上(api.upcoming / api.position),
悬浮窗也能拖到副屏了。

Changed — 语音改成抢占式

以前是纯串行链:三句排队,第三句要等前两句念完才开口 —— 那时候画面早换了,
听到的是"它在说上一段的事"。MAX_QUEUE_LAG_MS 只能做"排太久就放弃",救不了"一直在排"。

现在每句带代际(speakGen),新句一到立刻:截断正在念的那句
(常驻通道走 daemon 新增的 interrupt op,只杀播放管道那个 ffplay,daemon 还活着;
没有 edge-tts 的机器走 SAPI,kill 掉那个 PowerShell —— SpVoice.Speak() 是阻塞调用,
只有杀进程才能让它闭嘴),并清空排队(那些句子配的是已经过去的画面)。

代价:被截断的那句后面几个字听不到 —— 追上画面比句子完整重要。
新协议:{"op":"interrupt"} → {"ev":"interrupted"};它和 stop 不是一回事,
stop 是 SystemExit(0),整个常驻合成器没了,下一句要重新预热 1~2 秒。

Added — api.upcoming() / api.position()(#4)

此前核心只会推(due / scheduled),适配器想知道"再往后还有什么、现在播到哪"
只能自己读 watch.json 和 position.json —— 两份内部格式,还各自带着一套"哪条位置可信"
的判断,第三方适配器重写一遍必然走样。

await api.upcoming({ horizonSec: 20 })   // → [{ at, text, segment }],按 at 升序,不消费
api.position()                          // → { pos, dur, confidence, staleMs }
  • upcoming() 读同一份台词槽,只给未来窗口内、带 at 的句子;窗口 1~120 秒,
    没在陪看 / 位置不知道时是空数组。
  • position() 走 position.js 那套锚点逻辑(扩展上报 / CDP 探针 / 用户自报三条来路),
    confidence 分 fresh / extrapolated / unknown。unknown 时 pos 是 null 而不是 0 ——
    给 0 会让"我合成的音频比当前位置晚了三百秒"成立,然后默默丢光音频且不报错。
  • 停表 / 换片即清空。边界没变:核心不做合成、不缓存音频、不重试。

Fixed — 悬浮窗拖得到副屏(#8)

副屏在主屏左边时坐标是负的,而拖动 / 缩放 / 恢复位置 / 抓屏外扩四处都拿主屏当边界:
窗口拖不出去、放到副屏后玻璃停在旧帧、下次启动又被拉回主屏。
现在用 GetSystemMetrics(76..79) 取虚拟桌面,按下时重新同步屏幕(插拔显示器不用重开窗);
窗口整个在主屏里时仍按主屏钳,单屏逐像素不变。

Fixed — 缺 mss / numpy / pillow 时弹窗说明(#76 附带)

pythonw 没有控制台,import mss 失败时 Python 直接退出,用户看到的只有"窗口没出来"。
现在缺包就弹 MessageBox 写明缺哪个、装什么,并把原因落到 ~/.dsh/watch-window-error.log。

验证

  • 全量 node --test 334/334(41 秒)
  • check-syntax 59 文件通过
  • 打包 103 文件 / 551.8 kB
  • dist.shasum 与本地 tarball 一致,gitHead = release commit
  • 顺带修掉一个把 npm test 卡死的测试(widget-reopen 没隔离 DSH_HOME,
    会挂本机真实 voice daemon,跑完断言后进程永不退出)

安装:npm i dsh-watch-together@0.2.13

v0.2.12

Choose a tag to compare

@SAOS-3624 SAOS-3624 released this 03 Nov 18:49

Fixed

  • 在悬浮窗打字不再往宿主的「排队消息」里塞一条(wakeOnChat 默认 true → false)。
    每打一句话,输入框上方就多一条排队消息 —— 而窗口里明明已经有气泡显示过了,
    队列里再来一条既是噪音、又占住输入框。根因是 wakeSession 走宿主的 agent.followup(),
    而 DSH 的 QueueDock 不过滤来源,绕不开,只能不投。
    关掉不会丢话:钟那一拍整个跳过、聊天通道不消费,消息留在里面,模型下次调
    watch_together 时 poll 得到;真正的阻塞等待(wait / nextLine)走另一条路,
    照旧能随时打断。

Added — 气泡底板

聊天气泡内部铺一层 #07111d、alpha 0.50 的圆角底板(glass_geometry.BUBBLE_PLATE),
文字的对比度不再跟着画面走。底板在玻璃合成层做(_overlay 的 tint),CPU / GPU 两条
路径共用同一份实现;气泡宽度变了跟着一起重算;空态不铺(四个占位框铺深色会像四个空洞)。

实测:760×560、同一段消息、同一位置,只切 DSH_GLASS_PLATE 拍两版,取气泡内
「底板中位灰度 / 文字笔画 p97」算 WCAG 对比度。

背景 改动前 改动后
纯白(最坏情况) 1.26–1.53:1 3.83–4.25:1
深蓝黑 16.3–17.7:1 17.0–17.2:1

alpha 0.40 时纯白背景只有 2.88–3.31:1(试过),0.50 才到 4.2。

TextLayer.draw(shadow=True) 给每个字先落一层 +1px 的深色副本。Tk 的 text item 没有
stroke / outline,只能多建一层 item;只在气泡正文开,refresh() 用 tshadow 标签把
它排除在翻色之外(目前暂存)。

Changed — 声音延迟 2.4 秒 → 0.2 秒(常驻合成器)

台词几分钟前就写好了,第一声却要等两秒多 —— 听起来不像它在说,像在等我念。

老路是每句起两个进程:一个 edge-tts.exe 合成到临时 mp3,一个 ffplay 播放。
实测(Win11 / i9-12900HX,短句与长句各三次取均值)端到端 2.4~3.0 秒,
而且这四笔开销每句都要重付一次:

段 耗时
pip 的 exe shim 拉起 python ~600ms
python 解释器冷启动 ~600ms
import edge_tts ~700ms
WebSocket 建连(DNS+TCP+TLS+token) 620~760ms
首块合成 200~300ms
剩余音频块 30~130ms
ffplay 启动 + demuxer 探测 380~410ms

examples/voice-edge-tts/stream/daemon.py 把三件事搬进一个常驻进程:

  1. 常驻 python:import edge_tts 只做一次。
  2. 长连接复用:一条 WebSocket 上连发多个 SSML 请求(Azure 协议本来就支持)。
    实测连续 20 句首块 min 182 / avg 200 / max 235ms,没有退化。
    服务端在 turn.end 之后约 30 秒关连接(30s 还活着、35s 已死;发 ping 保不住),
    所以空闲超 25s 主动断、下次用时重建(620~760ms)。
  3. 常驻 ffplay:一个进程从 stdin 连续吃 mp3 帧。实测三段音频拼着写进去,
    时间码一路涨到 7.3s、进程没退 —— 每句起进程要付 380ms 首声。

同一条 due 事件、同一台机器上的对照:

场景 老路 现在
预取命中(scheduled 已合成) 1009~1674ms 1ms
没赶上预取(see-now 的 at:null) 2485ms 230ms
连续第三句 排队 4.9s 后被丢弃 1ms

预取队列也不再"满 6 句就丢弃":常驻通道按 text 在内存里缓存(上限 80 句),
老路那个 MAX_PREFETCH = 6 是文件缓存 + 串行 CLI 逼出来的,这里不用照抄。

兜底:python 找不到、daemon.py 不存在、进程起不来 —— 任何一步失败都退回老路
(每句一个 CLI 进程),没有一行 throw。DSH_TTS_STREAM=0 可强制走老路排查。
调试时 DSH_TTS_FFPLAY_LOG=info 会把 ffplay 的时间码放出来。

test/tts-stream.test.mjs 覆盖协议(first/done 顺序、error 后通道继续可用、进程死掉后不挂在超时上),
不依赖网络和 ffplay。

Issue #44 第 2 条:文字直接压在实时视频上,对比度每秒都在变,连看 5–10 分钟眼睛发酸。

有底板之后底色恒定,TextLayer.adapt() 对气泡正文已经没有意义,反而会在过渡帧翻错
(白字被翻成深色,压在深色底板上更看不见)。给 draw() 加了 adapt=False:这些 item
不参与采样。标题栏、输入框、空态提示仍走逐字翻色 —— 它们确实直接压在玻璃上。

  • 跟 dsh-video-storyboard 0.6.6 一起发。依赖范围 ^0.6.0 不用改。
    上游这次是排布与计费对齐(同素材 token -9.3%),本包自己的代码没有改动。

调试开关

DSH_GLASS_PLATE=0 关掉底板和描边,回到 0.2.11 的行为 —— 只用来拍新旧对比图。

媒体素材:悬浮窗实录(2026-11-03)

Choose a tag to compare

@SAOS-3624 SAOS-3624 released this 03 Nov 08:08

这里放的是 issue 里引用的录屏,不是版本,不参与 npm 发布。

  • floating-window-0.2.8-force.mp4:0.2.8 + GLASS_FORCE=1 + capturable: true 下的悬浮窗实录

0.2.6 — 声音预合成(合 PR #36)

Choose a tag to compare

@SAOS-3624 SAOS-3624 released this 02 Nov 16:24

陪看的声音还差最后一步:到点才开始合成。这一轮把"合成"从关键路径上挪开。

Added — scheduled 事件(合 PR #36,作者 @ggfgfgf-on)

  • 循环每一拍把未来 60 秒内、已经写好且带 at 的台词以 scheduled 广播一次(每句一次;
    只在有会出声的订阅者时发)。它不参与"该不该念"的判断,只给适配器时间预合成。
  • upcomingLines(input, { after, before }):只读的"未来台词"查询,不消费稿件。

Changed — 示例适配器预合成

  • examples/voice-edge-tts:speakEdge 拆成 synthEdge(只合成)+ playEdge(播放并删临时文件);
    scheduled 到达即合成,按 input|segment|text 缓存(上限 6 条,满时踢 at 最远的一条),
    due 优先播缓存。日志多出 prefetched / synth、ready Nms、wait Nms。
  • MAX_QUEUE_LAG_MS 的计时从 due 到达起算,合成时间本来就算在里面 ——
    预取因此还降低了"排队太久被丢弃"的概率,不只是首声早一点。

Fixed — 评审补丁(5fa1307、bd5b3c7)

  • 跨段时缓存 key 对不上:dueLine 跨段挑 at 最小的一句,选中上一段的句子时 due 报的却是
    当前段,和 scheduled 的 segment 不一致 → 缓存白做、首声又晚回 0.6~2 秒。
    dueLine({ meta: true }) 现在多返回 from,speak() 用它。
  • Number(null) === 0:没写 at 的台词会被当成"at=0"挤进预取窗口;适配器 prefetch() 同理,
    6 个槽位被 at=0 占满后永久失效。两处都改成显式判空。

安装:npm i dsh-watch-together@0.2.6

0.2.5 — PR #29(GPU 内框边缘 + 气泡留白)

Choose a tag to compare

@SAOS-3624 SAOS-3624 released this 02 Nov 16:24

0.2.4 里 PR #28 留下的两个 GPU 尾巴,作者当天补了 PR #29 收掉,外加气泡留白微调。

Fixed — GPU 内框边缘(合 PR #29,作者 @ggfgfgf-on)

  • 底部输入/发送框发黑:内框边缘原来直接拿 small 的 render target 当 sampler,
    贴边小框会采到边界外。现在单独拷一份 blur(1.45) 到 edgeblur FBO 再采样
    (注释里写明「直接当 sampler 会触发驱动崩溃」,所以宁可多一次 copy)。
  • 内框边缘的折射/扭曲丢了:CPU 里 edge_w=0 是边缘(取折射样本)、=1 是中心
    (取强模糊),Shader 里方向是反的。改成 mix(edgep, strong, e) 后恢复。

Changed — 气泡留白

  • 横向 14→18,纵向 +4→+16,正文按 (bh - lines*ls)/2 垂直居中。
    字不再贴着气泡边,短消息也不会挤成一条。

安装:npm i dsh-watch-together@0.2.5