DSH HUB
HomePlugin StorePlugin PacksCommunityRankingsResourcesPublish Guide
Plugin source
Back to catalog

SAOS-3624 /

SAOS-3624/dsh-watch-together

Verified

陪看:让只会读文字的模型跟着你的真实播放进度一段段看视频(DSH 插件)。A DSH plugin that watches a video together with a text-only agent — one segment at a time, lines spoken at the right second.

★ 0 Stars1 Forks1 IssuesN/A Community rating0 Confirmed installs
View on GitHub
READMESource: main@5f4ad7ad

dsh-watch-together

test npm license

陪一个只会读文字的模型一起看视频:跟着你的真实播放进度一段段往下看,在你快看完这一段时把画面和台词递过来。

名字全小写,带横杠 —— 和 dsh-video-storyboard 同一个风格。

English TL;DR — a DSH plugin that watches a video together with a text-only agent: it follows the real playback position reported by a browser extension, hands the agent one storyboard + transcript per segment just before that segment ends, and speaks the scripted lines at the exact second. Code, identifiers and the tool schema are English; the docs below are Chinese.

  • 它是什么:一个 DSH 插件,只注册一个工具 watch_together
  • 它不是什么:不是全片概览,不是播放器,也不带 TTS 引擎(声音自己接,见 §接声音)
  • 依赖:node + ffmpeg + 一个浏览器扩展(可选但强烈建议)

30 秒开始

npm i dsh-watch-together          # 会连带装 dsh-video-storyboard
node bin/watch.mjs doctor         # 自检:ffmpeg / 扩展 / 字幕 / 声音,逐项 OK 才算就绪
node bin/watch.mjs install-extension   # 装一次浏览器扩展(以后正常开浏览器就行)

然后在会话里:

watch_together({ wait: true })    // 跟着你浏览器里正在放的那一支走

配置在哪、缺什么怎么补:全部集中在 docs/CONFIG.zh.md —— ffmpeg / yt-dlp / B 站 cookies / ASR / 浏览器扩展 / 调试端口 / 声音 / 悬浮窗,一节一个服务。


它和 dsh-video-storyboard 是什么关系

两个插件,一个依赖。 这不是一个包里的两个开关。

dsh-video-storyboard dsh-watch-together(本包)
版本 0.5.x 0.0.7
工具 video_storyboard / video_zoom / video_probe / video_locate watch_together
干什么 把一段视频变成一张可引用的宫格(一次性、全片或指定窗口) 跟着播放进度一段一段看下去
依赖方向 不依赖陪看(storyboard → watch = 0 处) 依赖视频识别,单向

陪看天生建立在视频识别之上:它每次交付的那一格画面和清单,都是调用 dsh-video-storyboard 的 runStoryboard() 算出来的。所以本包把 dsh-video-storyboard 写成普通依赖(^0.5.0), npm i dsh-watch-together 会连它一起装上 —— 装的是库代码,不会因此多注册四个 video_* 工具 (工具由各自的插件入口注册)。

想只要视频识别、不要陪看:装 dsh-video-storyboard 就行,本包不会被拖进来。

发布顺序 —— 但版本号不需要对齐

本包依赖 dsh-video-storyboard@^0.5.0。这是范围,不是等号:semver 对 0.x 的规定是 ^0.5.0 = >=0.5.0 <0.6.0,所以 storyboard 发 0.5.5、0.5.9 都不必动本包。 两个包的版本号本来就不同(本包 0.0.7,storyboard 0.5.4),没有"对齐"这回事。

范围定在哪,取决于本包到底从它身上拿了什么:7 个子路径、15 个导出, 全部列在 test/dep-contract.test.mjs 里。 那张表才是真正的契约,版本号只是它的代理 —— 少了导出,ERR_PACKAGE_PATH_NOT_EXPORTED 还算好查;签名悄悄变了才难查,本包不会报错,只是行为不对。

顺序上仍然有先后(npm 上必须先有满足这个范围的 storyboard,本包才装得上): npm publish dsh-video-storyboard@0.5.x → npm publish dsh-watch-together@0.1.x。 反过来会装不上:npm i dsh-watch-together 找不到满足 ^0.5.0 的版本。

安装

npm i dsh-watch-together        # 会一并装 dsh-video-storyboard

从"陪看还在 storyboard 里"的旧版本升级(重要)

旧版 dsh-video-storyboard@0.4.x 自己就注册了 watch_together。如果宿主 profile 里还留着那份旧副本, 而这个包装上之后同一个工具会被注册两遍。升级顺序:

node ../dsh-video-storyboard/scripts/install-dev.mjs   # 先把 storyboard 更新到 0.5.0(只剩 4 个 video_*)
node scripts/install-dev.mjs                            # 再装陪看(它会连带把 storyboard 再同步一次)

本包的 scripts/install-dev.mjs 会把自己和同级目录里的 dsh-video-storyboard 一起复制进 profile —— 因为陪看是裸包名 import 后者的,profile 里缺了它就会 ERR_MODULE_NOT_FOUND。--check 只读,--remove 只卸自己、不动 storyboard。

前置条件(ffmpeg / yt-dlp / cookies / ASR / 声音)逐个服务的配法见 docs/CONFIG.zh.md。一句话版本:

要什么 最低配 配好之后
出图 ffmpeg 在 PATH 上 能看、能写台词
准位置 装一次浏览器扩展 跟着你平时的浏览器走,不用专用窗口
有字幕 yt-dlp + B 站 cookies 台词锚点有真实秒数
没字幕的片子 ~/.dsh/asr.json 用 ASR 给句子时间码
出声 ~/.dsh/voice.json + ~/.dsh/tts.json 到点用你选的音色念

陪看自己没有额外依赖:位置从浏览器读,不装 Python、不装播放器插件。 唯一要准备的是一个能上报位置的浏览器 —— 见下文「进度从哪来」。

怎么用

一个回合就是一段,不要按定时器轮询:

watch_together({ waitForVideo: true })   // 接上用户浏览器里正在放的那一支(不用问链接)
watch_together({ speakNow: true })       // 先看用户已经看到的那一段
watch_together({ lines: [                // 把这一段的台词写进去(at 给字幕原文最准)
  { from: 180, at: "1/4螺口", text: "注意这个接口,它决定了你以后能换什么镜头。" },
] })
watch_together({ wait: true })           // 阻塞到这一段快放完;用户插话会提前返回

input 可以省略 —— 省略时就从浏览器读。传了也只是"这一支",不是每次都要贴。

它怎么工作

用户看到 05:12  →  插件算下一段(05:12–07:12)   ← 这 8 秒用户还在看,等得起
用户看到 07:00  →  这一段早算好了,直接把图递出来  ← 感知延迟 0

AI 先看完,卡着用户的进度说话。 计算发生在用户在看的当口,所以感觉像一起看。

参数 默认 说明
ramp [60,120,180,240] 变速启动:开头短、逐段加长,第一次说话不用等 5 分钟
segment 300 秒 稳定后的批次长度(阶梯走完后固定在这一档)
lookahead 300 秒 余量按时间算:保证当前位置之后已备好的时长不少于它(不是"留两段")
depth 自动 只给 depth 时按段数算余量(旧口径,一般别用)
lead 15 秒 段末前多少秒交付。不是段末——两格交界处插话最突兀
cells 5 每段一张图
cursor 自动 用户看到第几秒;不给就自己读,读不到才问你

变速启动:先短后长

1 分钟 → 2 分钟 → 3 分钟 → 4 分钟 → 5 分钟 → 5 分钟 → …
 └─ 45 秒就说第一句           └─ 之后稳定,不再加长

固定 5 分钟一档意味着第一次开口要等将近 5 分钟。刚点开视频时最需要的是"它已经跟上了", 所以开头几批短一些,代价只是那几分钟多跑几次分镜。

watch_together({ ramp: [30, 60, 120] })   // 更激进的起步
watch_together({ ramp: [] })              // 全程等长,不要阶梯

双缓冲:永远留一个批次在后面

批次 1 正在看  ┃  批次 2 已建好  ┃  (空闲时开始建批次 3)
批次 1 用完    →  批次 2 顶上,同时开始建批次 3

建一个批次要 2–10 秒,而看一个批次要 segment 秒,所以总有时间在空闲里把它建完。 实测 4 次交付全部 0.0 秒(命中预取),批次之间无缝。

进度从哪来:两条通道,装个扩展就不用管

位置只有一个来源:你正在看的那个页面里 <video> 的 currentTime。它准到毫秒,不用猜。 把这个值取出来有两条路:

A. 浏览器扩展(推荐) B. 调试端口(备用)
怎么开始 装一次扩展,一分钟 每次开一个专用窗口
用哪个浏览器 你平时那个,书签登录都在 另一个窗口,要重新登一次
位置精度 页面真值,每秒上报 页面真值,每 15 秒读一次
手工步骤 装扩展(一次性) 双击 scripts/start-edge-debug.cmd
适合 所有人 不想装扩展 / 扩展读不到的站点

装扩展:双击 scripts\install-extension.cmd —— 它复制好扩展、把路径放进剪贴板、打开扩展管理页, 然后一直在那儿等:你点完三下它自己会说「✓ 扩展装上了」,播个视频它再说「✅ 全部完成」。 一个文件、一次双击,装和验证都在里面。步骤与常见错误:docs/BRIDGE.zh.md。

装了之后:你正常打开 Edge 播 B 站,插件就能看到你在第几秒。不用开专用窗口,不用配端口。

扩展上报什么:只有当前可见标签页的 currentTime / duration / title / url / paused, POST 到 127.0.0.1:9377。不读页面内容、不发外网。源码一共 170 行: extension/content.js(读)+ extension/background.js(发)。

插件不会自己开浏览器(这条老路 2026-10-31 已删):多开一个窗口并不能让用户正在看的 那一页报出位置。读不到就问一句「你看到几分几秒了」。

两条通道任意通一条就行,插件优先用扩展那条(更新更勤)。

B. 备用通道:专用窗口(为什么不能直接用你现在的浏览器)

Chromium 136 之后(这台机器的 Edge 是 152)不允许在默认 profile 上开调试端口, 这是微软/谷歌的安全修复,绕不过去。所以那个窗口有它自己的 --user-data-dir=%USERPROFILE%\.dsh\edge-watch:

  • 第一次要在里面登录一次 B 站,之后它一直记着(Cookie 存在那个 profile 里)
  • 你的日常 Edge 完全不受影响,可以同时开着
  • 书签/扩展不在里面 —— 它只是个"看片的窗口"

scripts/start-edge-debug.cmd 就是干这个的,双击即可:

scripts/start-edge-debug.cmd          # 端口 9333
scripts/start-edge-debug.cmd 9444     # 换一个端口

端口写进配置(桌面版读不到 setx 的环境变量,所以推荐写文件):

// ~/.dsh/video.json
{ "browserPort": 9333 }
DSH_BROWSER_DEBUG_PORT=9333      # 或者环境变量
watch_together({ browserPort: 9333 })   // 或者单次传参

端口别用 9222:那是 Chrome DevTools 的惯例端口,WorkBuddy 自己就占着(实测读到的会是它的界面)。

为什么进度会"读不到"(重要)

插件每 15 秒真读一次位置,中间按墙钟外推。三种情况要分清:

状态 判断依据 行为
读数新鲜(≤5 秒) 刚读到 直接信
读数 6–16 秒旧 正常的外推窗口 按墙钟往前推,误差下一次读取就纠回来
超过 20 秒没读到 错过了一次读取 推出来的位置要多越过 5 秒才敢开口(宁可晚,不能早)
超过 60 秒没读到 连错过三次 不再推,明确要你报进度

你半路做的动作,它怎么应对

你做什么 它怎么做
暂停 位置不再变,它安静等着。恢复播放后接着说,不会打扰你
拖进度条跳过去 位置立刻跟上;跳过的部分不补说。跳超过 1 分钟会提醒模型一次
拖回去重看 位置对了,但看过的段不会重说一遍(避免复读)。也会提醒模型一次
视频播完 停下来,叫模型收个尾、问下一支看什么
换一支片子 时长/地址变了就认出来,停下来并叫模型重新开始跟这一支
切到别的标签页 只有当前可见的标签页上报,所以不会读错标签页
关掉浏览器 约 20 秒没消息就问你现在看到哪儿;一直没回应就停表,不空转

暂停、跳转、回看这些都不需要你通知它 —— 它每秒都在读真实进度, 位置永远是对的,只是"说不说话"的策略不同。

读不到最常见的原因:扩展被停用了 / 陪看服务没在跑 / 那个窗口被关了 / 页签切走了。 watch_together({ statusOnly: true }) 可以随时看钟的状态:读到第几秒、可不可信、为什么没说话。 node bin/watch.mjs doctor 会分别列出两条通道各自通不通。

它顺带报三块诊断信息 —— 用户说"后面越来越慢 / 它突然不说话了"时,靠这三块定位卡在哪一环:

字段 含什么
segment { index, from, to, delivered, total } 在第几段、起止、这一场已交付几段、整片共几段(片长未知时 total 是 null)
speech { queued, lastLateSec, busyUntil } 还欠几句台词、上一句比画面晚了几秒、串行声音引擎忙到什么时候
build { inflight, cached, lastMs } 正在建哪一段、缓存里已有几段、上一段建了多久

null 一律表示"没测到",不是 0。queued 要读 watch.json,所以只在 statusOnly 里算 (循环每 2 秒付这个 IO 不值得)。

接声音(核心不发声,声音你自己接)

这个包不带任何 TTS 引擎,也不读 ~/.dsh/tts.json。它只做两件事:

  1. 在正确的播放时刻把台词交出去;
  2. 每次结果里带上 speech 事件,at 是这句话在时间轴上的位置。

念不念、用什么念、念多快,全由你(或宿主)决定。

手把手的声音配置教程(挂载适配器、选晓萱等音色、验证、排错)见 docs/VOICE.zh.md。

事件长什么样

watch_together 交付一段时,返回值里有:

"speech": [
  { "at": 191, "text": "钓上来了,池子里那条比他想的配合。", "state": "scheduled", "segment": 180 }
]

后台循环里某句到点时,会把同一条推给你,state 变成 "due"。

同进程:订阅

import { onSpeech } from 'dsh-watch-together/voice';

const off = onSpeech(async ({ state, text }) => {
  if (state !== 'due') return;      // scheduled 只是清单,不是"现在念"
  await mySpeak(text);              // ← 你自己的引擎
});

有人订阅时:到点的台词直接交给订阅者,循环不会再叫醒模型(省 token,也避免同一句被说两遍)。 没人订阅时:台词照旧走"叫醒模型、模型自己说"那条路 —— 功能完整,只是安静。

只做展示的订阅者要传 { voice: false }(聊天框就是这么干的):否则循环会以为"有人会念", 从此不再叫醒模型 —— 陪看会默默地变成哑巴。

别忘了上报"真的出声了":due 只是"该念了",合成还要 0.6~2 秒。悬浮窗里的字等的是 state === 'spoken'(开始播放那一瞬调 api.emitSpoken(e))。不上报也能用,但字会先于声音 出现(2.5 秒兜底),也就是这次修掉的那个不同步。

节奏:宁可少说一句,不要越说越晚

引擎是串行的(一句念完才轮到下一句),所以台词密度直接决定声音会不会越拖越远:

  • 钟会记"引擎到什么时候才空出来"(字数/5 秒),上一句还在说时不会再塞下一句;
  • 拖过迟到宽限(3 秒)的那句直接作废 —— 少说一句,而不是晚 20 秒说;
  • 适配器还能先放弃:排队超过 3.5 秒就跳过这一句,并发 state: 'skipped';
  • 展示层收到 skipped 会撤掉兜底定时器,没出声的字不会出现在悬浮窗里。

所以"台词准时"和"每句都念"是互斥的,这个包选前者。想少丢词就让模型每段少写两句 —— 交付说明里会把每句的落点列出来,平均间隔小于 6 秒时会明确警告"太密"。

现成适配器:examples/voice-edge-tts

带了一个可以直接用的适配器:edge-tts 合成 → ffplay 播放 → 失败退回系统声音。 每一句都会在 stderr 打一行时间码:

[voice 03:11.4] 钓上来了,池子里那条比他想的配合。

调试时最想知道的就是"它到底是第几秒吐出来这句话",这行比音频本身还有用。

启用只要一个文件:

// ~/.dsh/voice.json
{ "adapter": "<你的路径>/dsh-watch-together/examples/voice-edge-tts/index.mjs" }

或者环境变量 DSH_VOICE_ADAPTER=<同样的路径>。插件启动时 import() 它,挂不上只写一行 warn, 不会影响插件启动。

最小适配器(一分钟)

// my-voice.mjs
import { execFile } from 'node:child_process';

export function attach({ onSpeech }) {
  return onSpeech(({ state, text }) => {
    if (state === 'due') execFile('say', [text], () => {});   // macOS;换成你自己的引擎
  });
}

attach(api) 收到 { onSpeech, speechSubscribers, emitSpeech, emitSpoken, lastSpeech },返回一个退订函数即可。 想放弃某一句就发 state: 'skipped'(见 docs/VOICE.zh.md)。

聊天框 / 悬浮窗:一条统一的文本流

插话那句和剧本台词走同一条流,一起显示在聊天框里,也一起被念出来:

role 是什么 会念
line 剧本台词(到点由循环写进来) 是
assistant 模型插话时说的那句 是
user 你自己打的字 否

它们都落在 chat.jsonl(%LOCALAPPDATA%\dsh-video-storyboard\cache\ —— 缓存目录由 dsh-video-storyboard 管,两个插件共用一份)。

模型自己说的话怎么进到框里? 靠 say:

watch_together({ say: "你看这条,鱼比人还紧张。" })

插件读不到模型的正常回复,所以插话时想让用户看见并听见,就得把这句话交给 say。

插话之后:先说话,再干活

用户问一句话,模型常常先去取图/zoom,回来才开口 —— 而"回来"要十几秒,用户那边就是 "叫了它一声,它没理我"。所以 watch_together 有三条硬规则(src/interject.js):

  1. 回插话的那次调用必须带 say,字先写进流,图和清单后到:
    watch_together({ say: "我看一眼", speakNow: true })   // 一次调用,先说再看
    
  2. 没回就追问:下一次工具结果末尾会挂 [未回应] 用户 N 秒前说的「…」你还没回, 直到真的带了 say 为止(3 分钟后自动作废)。
  3. 回话和脚本台词不许挤在一起:你说话之后 12 秒内的脚本台词会被取走 ([lines·太近])交回给模型按刚说的那句重写;那段时间里循环也不会再发词 (src/reply-guard.js)。两句挨着念,声音引擎串行,后一句排太久会被适配器直接丢掉。

三种"停下来"的信号

用户不需要说"别看了"——这三件事本身就是指令,插件会停表,并且像插话一样叫醒模型, 让它在同一轮里知道"别再按旧的跑了":

信号 怎么发现 之后
关掉悬浮窗 钟每 2 秒看一次窗口 pid 停表 + [hangup]:不要再调 watch_together、不要再念台词;下一次调用直接返回,不建图、不起钟、不重开窗(除非 open: true)
关掉浏览器 / 切走页签 位置读数断了 20 秒后一句台词都不再念(位置只能外推,外推过期就不许出声);5 分钟仍读不到就停表
换片 / 播完 位置桥的 dur 或视频页地址变了(每 2 秒判一次) 停表 + 叫醒:"换片了/播完了";阻塞等待里会当场返回 [video-changed]

关窗为什么必须真的叫醒:wake: false(默认)关的是"这一段该说话了"那种打扰。 关窗/换片/播完不是打扰,是"事情变了"——它们走的是不受那个开关影响的通道。

悬浮窗(不用浏览器)

examples/floating-chat/window.py 是个真·独立悬浮窗:无边框圆角、置顶、 按住顶部拖动、右下角可拉伸、回车发送、Esc 关闭。用标准库 tkinter,不装任何第三方包。

pythonw examples/floating-chat/window.py     # 无控制台

它长这样:剧本台词是琥珀色气泡(左边一道竖线,表示"这句会被念出来")、 模型插话是浅灰气泡、你说的话是蓝色气泡靠右,每条上面一行小字写着谁、什么时候。

几个有意为之的细节:

行为 为什么
按真实 DPI 建窗(SetProcessDpiAwareness) 不声明的话 Windows 会把窗口当 96dpi 位图拉伸,高分屏上整窗发虚
只做内部焦点,不抢系统焦点 窗口一开就把焦点从浏览器抢走的话,敲空格想暂停会打进聊天框
往上翻历史时新消息不把你拽回底部 只有本来就贴着底才自动滚
关窗时记住位置和大小(~/.dsh/watch-window.json 的 geometry) 下次不用重新摆
增量读 chat.jsonl(开场只回放最后 60 条) 以前每 200ms 全量解析,两小时一场几千条会越来越慢
圆角外的像素用 -transparentcolor 抠掉 tkinter 没有圆角窗口;取不到这个能力就老实退成直角

一定要用带 tkinter 的 Python:Windows 商店版自带;python.org 官方安装包勾上 tcl/tk 也有。 WorkBuddy 托管的那份没有 tkinter。

它和聊天记录页(http://127.0.0.1:9377/,只读兜底)看的是同一个文件,两个一起开着也不打架。 浏览器置顶小窗(/widget + 画中画)那条老路已经删了 —— 要先开一个标签页、关掉就没了。

命令行(没有 DSH 宿主时)

bin/watch.mjs 是给没有 watch_together 工具的会话(比如普通的终端 AI)用的壳, import 的是同一份 src/。完整用法就是 node bin/watch.mjs help:

命令 干什么
doctor 自检:插件 / ffmpeg / 端口 / 扩展心跳 / 登录,逐项 OK/FAIL
start [url] 起一个带调试端口的专用浏览器窗口(备选通道)
login 从浏览器直读 cookie 落盘 + 验 B 站登录
wait [秒] 等用户真的放起了视频,返回地址 / 标题 / 时长 / 位置
status 当前位置 + 当前段 + 余量 buffer + 下一步该跑什么
fill 一次把 5 分钟余量内的段全建好(别一段一段挤)
seg [auto|from to] 出这一段的宫格 + 清单 + 句子时间轴
subs [from to] 只拉字幕时间轴(不出图,1 秒)
cue '[[at,"词"]]' 到点念台词;check 只核对不念
say "文本" / voices 立刻念一句 / 列出可用音色
chat / reply 起聊天服务 / 替用户回一句
bridge 前台盯着扩展上报(排查用)
install-extension 装浏览器扩展(三分钟,装完自己验证)

素材:图和字幕是两半,缺一不可

台词来自"看到什么"+"听到什么",所以一段的素材有两半,工具会记账:

一半 从哪来 记在过去哪
image storyboard 九宫格(一次交付给一张) watch.json 的 material[段起点].image
transcript 句子 + 每句真实起始秒(字幕轨或 ASR) material[段起点].transcript

两半都交出去过,交付结果里就会出现:

[素材] 图 ✔ 字幕 ✔ —— 两半都到了,现在写台词(`lines`)。

缺一半时它会直接写出下一步该调什么:

[素材] 图 ✔ 字幕 ✘ —— 句子的真实秒数还没有,先取字幕:
  watch_together({ transcriptOnly: true, input: <同一个 input>, from: <段起点>, size: <段长> })

transcriptOnly: true 就是"图与字幕分开返回"那个开关:只给句子和秒数、不出图、不读播放位置。 命令行等价物是 node bin/lines.mjs scan(读的是同一份字幕缓存)。

记住口径:它只记"我交出去过什么",不假装能验证"模型有没有看"—— 但没交出去过的东西,不会被当成素材用。

写台词的两条路(省 token)

写一段台词的代价,全在"看图"上:一张 storyboard 400~700 vision token。 但台词真正需要的只有两件事 —— 这段在说什么、那句话在第几秒,字幕里都有,而且是纯文本。

# 1) 纯文本候选:8~10 句 + 每句真实起始秒,一张图都不解码
node bin/lines.mjs scan --input <url|路径> --from 180 --size 180

# 2) 直接写进去(秒数可以写 195,也可以写 03:15)
node bin/lines.mjs set --input <url> --from 180      --at 200 --text "…" --at 250 --text "…"

# 3) 完全不经过模型:按打分选 3 句写进稿子(只是复述字幕,不会"评论")
node bin/lines.mjs auto --input <url> --from 180 --size 180 --count 3

node bin/lines.mjs list --input <url>     # 哪些段已经写过、各几句
node bin/lines.mjs cues --input <url> --from 180 --size 60   # 原始字幕,排查用

字幕会缓存在 <cache>/subs-<sessionKey>.json,同一支片子只抓一次; --refresh 强制重抓,--quiet 关掉头部那行说明。

代价对比(每段):看图写 ≈ 1,200~1,900 token,scan(只看候选句)+ set ≈ 300 token, auto ≈ 0(脚本自己挑,应急用)。auto 只会把字幕复述一遍,它的定位是「这段我懒得管」,不是默认路径 —— 正常还是看图 + 字幕写人话。

它不是全片概览

  • 一次只看一段,看完再下一段
  • 想细看某一格 → video_zoom({ cellId: 3 })(那是 dsh-video-storyboard 的工具)
  • 每段 5 格 ≈ 1,000–1,300 token;90 分钟片约 4.5–6 万 token(是一次性概览的 4–6 倍,换的是"有时间顺序")

长视频:一段短前情(默认关)

段数多了以后,模型可能丢掉上一段发生了什么。需要时显式打开,交付时会附一小段 [前情]:

  • recap: true 或环境变量 DSH_WATCH_RECAP=1 才开启;默认不产生任何前情;
  • 只取当前段之前最近 1~2 段(recapSegments,默认 2,最多 3);
  • 只读本地字幕缓存的原句,不读模型写过的台词槽;
  • 绝不读当前段之后的内容,不会剧透;
  • 整段上限 420 字符(recapMaxChars),超长截断;第一段没有前情,也不额外联网/跑 ASR。

默认关是因为没有实测数据前,每段 200+ token 是纯成本;开启后请自己对照后段是否真的少复述。这段不是全片总结;想看全片概览请另外用 video_storyboard 的概览能力。

代码结构

src/
  index.js          插件入口:只注册 watch_together,并挂上声音与聊天框
  register.js       watch_together 的注册入口
  tool-spec.js      watch_together 的 schema + 兜底校验
  handler.js        watch_together 的 execute

  watch-loop.js     后台循环:每段到点自己叫醒模型
  watch-run.js      一次陪看调用:读位置、建这一段、到点交付
  watch-modes.js    各种等待模式(waitUntil / nextLine / refill)
  watch-actions.js  单发命令(status / voices / speak / cookies / wake / stop)

  watch.js          门面:转发下面两个
  watch-state.js    看到哪儿了:watch.json 读写 + 段长/交付点算术
  watch-lines.js    该说什么了:位置轴上的台词队列
  lines-util.js     不读图也能写词:字幕 → 候选句 + 真实秒数
  watch-material.js 素材账本:图 / 字幕 各交出去过没有

  watch-source.js   这次看的是哪个源(就是当前页面的地址)
  watch-browser.js  "浏览器在不在"+从两条通道读一次位置(不负责开浏览器)
  position.js       位置可信度与推算
  position-bridge.js 扩展上报的位置:落盘 + 5 秒新鲜度
  latency.js        交付延迟记录与补偿
  batches.js        变速启动与批次窗口
  timer.js          到点提醒 + 叫醒会话
  interrupt.js      边等边听插话(共享 watcher + 等待者优先级)
  interject.js      插话交给模型的那段话 + "到底回了没有"(待回执)
  reply-guard.js    回话占住的那 12 秒:取走脚本台词、让循环别抢话

  voice.js          门面 ─┬ speech-bus.js    订阅总线(谁在听、谁真会出声)
  voice-adapter.js  ──────┘ 按路径挂外部引擎
  speech.js         语速估算:"何时可以插话" vs "引擎还要占用多久"

  chat.js           门面 ─┬ chat-store.js    chat.jsonl 读写(增量 tail)
  chat-view.js      ──────┤ 聊天记录页模板
  chat-server.js    ──────┘ localhost 五条路由
  input-surface.js  把服务 + 悬浮窗支起来(幂等)
  floating-window.js 拉起/判活 Python 悬浮窗

  time.js           mmss(秒 → "mm:ss"),只有一份实现

依赖是单向的:这里的模块可以 import 'dsh-video-storyboard/...',反过来永远不会发生 (那个包里出现一个指向本包的 import 就是循环依赖,构建会直接红)。

开发

node --test                    # 全量测试(不要传目录,node 自己会找 test/*.test.mjs)
node scripts/check-syntax.mjs  # 只做语法/导入检查,比跑测试快
node scripts/install-dev.mjs --check   # 看 dev 副本和 profile 里装的是不是同一份
node scripts/install-dev.mjs           # 把本包同步进 DSH profile(副本,不是软链)

改完代码要重启 DSH Desktop 才生效(插件树在进程启动时组合)。

许可

Apache License 2.0 —— 见 LICENSE(全文)与 NOTICE(这个项目不做什么:不托管视频、不上传位置、不绕过 DRM、不内置 TTS 引擎)。

—/ 5

No ratings yet

Verified DSH bundle

Commit 5f4ad7adc6fc

Community comments

No comments yet. Be the first to write one.

DSH HUB

A community index for DSH plugins. Not an official GitHub or DeepSeek AI product.

CommunityResourcesAPIAbout