dsh-video-storyboard

SAOS-3624Apache-2.0活跃维护

把视频按场景切成 9-16 格带时间戳和字幕的分镜图,连同音频包络压成一张 384 token 的图片,方便大模型低成本理解长视频。

Scene-aware storyboard for DeepSeek Harness: 9-16 numbered cells with timestamps + transcript + an audio envelope, packed into ONE image (384 tokens). Two tools: video_storyboard and video_zoom.

v0.7.2发布于 2026-11-10 04:34 UTC512star +52 / 7 天35fork同步于 2026-11-11 05:54 UTC

怎么上手

由 README 提炼,执行前请确认
winget install Gyan.FFmpeg
pip install --user -U yt-dlp
dsh plugin --profile web add dsh-video-storyboard
硬件要求按 README 说明

README 未说明显存或 GPU 要求;必须自行安装 ffmpeg + ffprobe,插件不打包。

下载:源码 zip·GitHub Releases(安装包)本站不镜像安装包

项目说明

由 README 自动翻译提炼

把整段视频压成一张带时间码的宫格分镜图,让多模态模型低成本看懂画面与台词

把视频按场景切成 9-16 格带时间戳和字幕的分镜图,连同音频包络压成一张 384 token 的图片,方便大模型低成本理解长视频。

核心能力

  • 一帧对应一个镜头,格子边长按该处切点密度分八档
  • 按行 DP 打包并裁到内容,单图约 233 token 封顶
  • 附时间对齐清单,含每格起止时间、刀数与台词
  • 提供 video_zoom 放大某段、video_locate 文本定位
  • dryRun 先报切点数与各档张数,再决定出图

容易踩的坑

桌面版 Electron 的 desktop profile 会被 dsh plugin 拒绝,需 git clone 后用 node scripts/install-dev.mjs 安装;不配 ASR 只能看图说话,清单里没有台词;贴 B 站高清或遇 HTTP 567 需导出 cookies.txt,且不能用 --cookies-from-browser。

内容由模型根据 README 生成,可能不完整或不准确,以官方文档为准。看原文 README

最近发版

5 个版本v0.7.2
  1. v0.7.2最新正式版2026-11-10 · 2026-11-10 04:34 UTC

    两个都是 #87 报出来的:maxFrames 反而把帧数压少(真 bug,装了这个参数就中招), 以及 PPT / 单机位讲座这类零切点长片一格 41 秒(调阈值救不了,只能按时间兜底)。

    Fixed — maxFrames 反而把帧数压少(#87,PR #88)

    fillGaps() 原来只在 maxFrames 不限时才跑,于是一刀长片:默认补到 4 帧, 而 maxFrames: 12 反而只剩 2 帧。现在 maxFrames 只是上限,补位照跑、不越过上限; 压张数那条路(tidy())的同一个门也补上了。 清单里"要了 N 格给了 M 格"那段不再把这种情况说成"窗口里只有这么多镜头"。

    Added — 静态长镜头的密度按时间兜底(#87)

    PPT / 课件 / 单机位讲座全片可能只有 1 个切点,密度算不出来 → 一格 41 秒、中间翻过的页面全看不见。 现在「窗口 ≥60 秒 + ≤1 个真切点 + 有段超过 30 秒」时按时间切开(每段 ≤30 秒,封顶 8 帧), 并在清单里写一段 density: static-longshot fallback …,附两条改法(video_zoom / from+segment)。 实测 164.5 秒零切点:4 格 × 41 秒 → 8 格 × 20.6 秒,张数不变。

  2. v0.7.12026-11-08 · 2026-11-08 12:02 UTC
    查看更新说明

    这一版全是社区反馈的修复,没有新参数。三件事:字幕抓取失败不再冒充「没配 ASR」、 设置页的 token 不再一重启就失效、tierMap 不再把 NaN 写进配置。

    Fixed — 站点字幕抓取失败被静默降级成 transcript="no-key"(#86,PR #84 之后的实报)

    no-key 的含义是「没尝试转写」(没配 ASR);而 no-ytdlp / failed 是「尝试了、但抓字幕 这一步坏了」。两者以前在清单里长得一模一样,用户看到 no-key 就去配 ASR,实际该修的是 yt-dlp。

    • 新增 transcript="subtitle-failed",带 reason(no-ytdlp / failed)和 detail; 只在「抓字幕自己坏了 + 没有内嵌字幕 + 又不会跑 ASR」时出现。no-subs 仍是 no-key —— 那确实是「这条片子没有站点字幕」;
    • 清单里对应一段 site subtitles FAILED (…),按 reason 给动作(装 yt-dlp / 读 detail 原文);
    • 字幕 memo 只缓存成功和 no-subs:no-ytdlp / failed / needs-login 不再缓存, 否则用户装好 yt-dlp 之后同一 URL 在本进程里永远不重试(「改了也没用」);
    • 清单新增 transcript-model="site-cc:ai-zh" / "embedded",站点 CC 和容器内嵌分得开。

    Fixed — tierMap 接受非整数键值,把 NaN 写进 video.json(#83,PR #84)

    旧代码 Math.round(Number(k)):foo 变 NaN、3.4 悄悄四舍五入成 3。NaN 键写进去之后 档位映射整段失效,而且没有任何提示。现在只收 1–8 的整数,非法值直接抛错、由设置页回显。

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

    token 以前每次 createSettingsPage() 都新生成。宿主一重启,浏览器里已经打开的那个设置页 就带着旧 token,保存全部报「token 无效」。现在落到 $DSH_HOME/video-settings-token (0600),跨进程稳定。

    Added — 插件市场的本地化元数据与详情页(PR #85)

    locale/{zh,en}.json + docs/plugin*.md:详情页不再直接把 README 铺出来。 exports 补 ./package.json 与 ./locale/*.json,files 加 locale。

  3. v0.7.02026-11-07 · 2026-11-07 12:57 UTC
    查看更新说明

    这一版只有一件「新功能」,其余全是补课:七道阈值多了一套按人眼感知定标的表; 另外修了长片 720p 副本下完就被自己删掉的问题,以及 Windows 上子进程闪黑框。

    Added — 第二套档位阈值 tierCurve: 'perceptual'(可选,默认不变)

    档位阈值原来只有一套 —— 照抄 docx 的七道「每帧切点概率」。这次把 see-now 0.2.0 里按 人眼瞬时记忆重标的那套同步过来,做成可选项:

    曲线七道阈值(每帧概率)一格代表
    docx(默认)0.0085 / 0.02 / 0.03 / 0.04 / 0.06 / 0.08 / 0.13.9 → 0.33 s
    perceptual0.0111 / 0.0163 / 0.0239 / 0.0351 / 0.0516 / 0.0757 / 0.11113.0 → 0.30 s

    单位换算是关键:see-now 那边用「刀/秒」,这边用「每帧概率」,差一个 fps。 新表 = see-now 的表 ÷ 30,和 docx 那套的换算关系完全一致(0.26/30 ≈ 0.0085、3.0/30 = 0.1)—— 形状没变,只是端点内收。

    两个端点有实验依据:下界 3 s(Pöppel 的「主观现在」23 s、电影镜头时长本身就有 3 s 的偏好)、 上界 0.3 s(注意眨眼 200500 ms、图标记忆的部分报告优势在 250500 ms 内消失)。中间六道等比 (公比 1.4678,时长辨别服从 Weber 律)。

    配置走 ~/.dsh/video.json 的 storyboard.tierCurve,认不出来的名字一律退回 docx(不抛)。 默认仍是 docx —— 这边没有做端到端 A/B(要真跑 SHOT/BBC 才知道),所以先只作为可选项。

    Fixed — 长片的 720p 副本下完就被自己的清理逻辑删掉(#81)

    pruneMedia() 按 mtime 从旧到新删整个 <key>/,而刚下好的那个目录同时是"最旧的" 也是"最新的"。缓存里只有它、且它本身超过上限时(实测 72 分钟的片 720p 副本 544 MB > 300 MB),下载完成的下一行就把自己删掉,后面 ffprobe/ffmpeg 报 No such file。

    • fetch.js 把刚下载的目录作为 exclude 传进去;
    • pruneMedia() 没拿到 exclude 时也保"最新的那个"(pipeline.js 的 pruneStorage() 走这条,同样安全);
    • 测试 +2:单目录超限不自删、exclude 的目录连统计都不算。

    Changed — 子进程统一 windowsHide(#37)

    spawn 控制台程序在 Windows 上会新建一个黑框,看视频时每隔几秒闪一下。 audio.js / scan.js / sheet.js / subtitle-strip.js / ytdlp.js 全部补上 windowsHide: true(非 Windows 平台该键被忽略)。

    Docs — 原理图解(合 PR #82)

    用一支 MV 把链路拆成 7 步,每步配真实中间图。图不在仓库里(NOTICE 不放第三方画面), 文档外链引用。顺手修掉两处和源码对不上的说明:README 的"格子尺寸全都一样"(0.6 起按 切点密度分档)、注释里的"假切点 +2 档"(常量是 MARK_BOOST = 3)。

  4. v0.6.92026-11-06 · 2026-11-06 15:10 UTC
    查看更新说明

    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 全部能编译出来。

    Added — 会话里的分镜图不再无限累积

    一张图进了会话历史就跟着每一轮请求走、宿主也不回收。现在涨到 imageTrigger(默认 300 张) 就把更早的一次性省到 imageKeep(默认 100 张):走宿主的 image/offload, 那些图从此不进请求,磁盘上那份也能删(tools/attachments-gc.mjs)。 两个数都能在设置页或 ~/.dsh/video.json 里改,0 = 关掉。

    注意它们不是"一到就省":涨到触发线才动手,一次省到留底 —— 否则每出一张图都要改一次历史, 前缀缓存每轮全废,比不省还贵。


    安装:dsh plugin add [email protected]

  5. v0.6.82026-11-05 · 2026-11-05 18:08 UTC
    查看更新说明

    Changed — 工具契约瘦身:长说明搬进 video_help

    五个工具的 description 和参数说明是每一轮请求都要重发的常驻文案。这次把"为什么 / 多少 token / 实测数字"搬进新工具 video_help(零图片 token、不需要有片子),契约里只留 "什么时候调、默认值是多少、价钱警告"。

    • 常驻 JSON(defineTool 编译出来的那份)11069 → 10527 字符,已含新增的 video_help;
    • 干跑印的说明书改成读 src/storyboard/help.js —— 以前这里另抄了一份,改档位只改一处就会 开始说不同的话;顺手补上它本来就缺的 tiers / tierMap 两节;
    • tierMap 不再声明 8 个子属性(两个工具省 ~480 字符):键和值的 1..8 仍由 validateToolArgs 兜,报错反而更具体(会说哪个键错);
    • 设置页「档位策略」下拉补上 native(原样)—— 它一直是「自定义」的底座,但之前选不到。

    Fixed — CI 的 ffmpeg 安装不再一次超时整步失败

    2026-11-05 那次 node20 的 job 在 apt-get install ffmpeg 上下 62.8MB 包超时,整步 exit 124,同一份代码的 node22 是绿的。现在改成短超时多次重试(apt 会复用已下好的 .deb),job 预算从 12 分钟提到 15 分钟。

    Added — 档位策略与逐档映射进设置页

    video.json 的 storyboard 段多了两项,设置页「视频与浏览器」里能改:

    • 档位策略:不指定(默认,自动选档)/ 全尺寸 / 标准 / 紧凑 / 速览 / 一图 / 自定义;
    • 逐档映射:选「自定义」才展开,八行"算出来的 i 档 → 落到哪一档"。

    三层优先序和 cutFloor 一致:本次调用显式传的 > video.json 里的 > 内核默认。 所以设置页设的是默认值,某一次调用显式传 strategy 仍然优先。

    几个实现上的取舍:

    • 新增 native 策略(bump 0、无 floor)。它只给「自定义」当底座 —— 映射表接在策略之后,底座必须是"按密度原样",否则用户设的 1→3 是在一个 已经被 bump 过的档位上再挪。standard 不能用:它把 1 档压成 1/2。
    • 恒等项(3: 3)不写进文件。写了没用,只会让下次打开看不懂上次改了什么。
    • 空值(strategy: '' / tierMap: {})是真的删掉那一项,不留空壳。
    • 越界档位直接报错(400),不默默吞掉 —— 用户以为设上了、结果没生效最难查。
    • 自定义下面有「一键填满」:八项一起改成同一个档。这就是"每帧固定多大"的做法 —— 整表吃掉切点密度算出来的档位,cutFloor 怎么调都不影响尺寸。没做成独立的"锁定"选项: 它在数学上和全映射到同一档逐字节相同,多一个概念只会让人以为还有别的东西。

用户点评

登录用户的真实使用记录,可按版本和硬件筛选
共 0 条

    还没有人点评。第一个说出真实体验的人最有价值。

    README 原文

    未翻译

    dsh-video-storyboard

    让 AI 真正"看过"一段视频:一张分镜图 + 一份带时间码的文本清单,模型据此回答"第 3 分钟发生了什么""这句话对应哪一格"。

    English: README.en.md

    已收录于 awesome-dsh-plugin。

    0.6.0 大改(2026-11-01):切点之后整条链路重写 —— 一帧 = 一个镜头,格子大小 = 那个位置的 切点密度(1/1 … 1/8 共八档),排布用 DP 按行打包,最后把画布裁到内容。 cells / sheets / need / maxPerSheet / adaptive / tileFirst 已删除, 换成 maxFrames / strategy / maxSheets / tierMap。迁移与完整差异见 CHANGELOG。

    协议变更(v0.5.0):本项目由 MIT 变更为 Apache-2.0。变更的目的是更好地保护代码版权 (要求保留署名),同时依旧允许大家自由使用、修改,甚至商用。此前基于 MIT 发布的版本仍是 MIT, 已经产生的衍生版本不受影响。感谢所有使用者和开发者。

    边界:本项目不托管、不分发任何视频内容,缓存只在本地且会自动淘汰;不提供付费内容访问、 DRM 绕过、区域限制绕过或批量抓取能力。能不能下载某个链接取决于你有没有相应授权, 请自行确认并遵守目标站点的条款与当地法律。完整说明见 NOTICE。


    它解决什么问题

    装完不能跑?先执行 node bin/doctor.mjs —— 它会检查 ffmpeg / yt-dlp / ASR / 缓存目录, 每条都跟一句"缺什么、怎么补"。用户反馈里过半不是 bug,是没配 ASR 或装完 ffmpeg 没重启 DSH。 新手步骤:docs/入门-五分钟.zh.md · 报错对照:docs/FAQ.zh.md

    没配转写 = 它只能看图说话。 能描述"画面上有个女孩在跑",但说不出她讲了什么。 本机 ASR 免费、不联网,装一次十几分钟 —— 这是这个插件价值差别最大的一步, 别等到问"她说了什么"发现答不上来才回头配。见 FAQ Q1。

    多模态模型看视频有两种笨办法:

    • 逐帧抽图 —— 10 分钟片子按 1 fps 抽就是 600 张图,token 直接爆。
    • 只喂字幕 —— 画面里的信息(构图、动作、场景变化)全丢。

    本插件走第三条:把画面压成一张宫格图(384 token 封顶),旁边配一份按时间对齐的文本清单(每格起止时间 + 这一格里的台词 + 切了几刀)。

    模型得到的是"这 90 秒里发生了什么、大概在第几秒",需要细节时再用 video_zoom 放大某一段。

    0.6 起,一格 = 一个镜头,格子大小不再一样:20 秒的长镜头占满整幅,0.3 秒的快闪只有 1/8 边长。 所以一张图里同时写着"这里是个长镜头"和"这里 24 个镜头连闪"——不读文字,看格子大小就知道节奏。

    六个工具

    工具干什么何时用
    video_probe只读容器头:多长、分辨率、帧率、有没有音轨/字幕、已分析过没有第一次接触一部片子时先调它,不花图片 token
    video_storyboard出宫格图 + 文本清单主体功能
    video_zoom只看某一段:一张图、n 帧、统一 ceil(√n) 档(9 帧就是 3×3 满格)清单看完觉得某段重要,再放大
    video_locate纯文本:一句话 → 时间码知道说了什么、不知道在第几分钟
    video_subtitles把画面里的硬字幕裁成一张竖长图 + 每条的秒数没有字幕轨、ASR 也转不出来(PV / 唱歌 / 双语艺术字)—— 只能看的时候
    video_help印一份手册:token 怎么算、六个策略各是什么、档位/tierMap、cutFloor、清单预算参数含义或价钱不确定时;用户说"6格/12格"时

    为什么有 video_help:工具的 description 和每个参数的 description 是每一轮请求都要重发 的常驻文案,写得越长,模型每一轮都付一次钱。所以契约里只留"什么时候调、默认值是多少", "为什么 / 多少 token / 实测数字"搬进 video_help,需要时现取(零图片 token)。

    video_storyboard 的 quality(low/medium/high)只影响贴链接时的下载清晰度, 本地文件永远读原片。默认 medium(720p);嫌糊传 high,嫌慢传 low。 价位和清晰度的取舍见 FAQ「关于钱和清晰度」。

    档位:格子为什么大小不一样

    图片按像素计费(42px 一格:rows×(cols+1)+2,地板 544²、封顶 994 @1300×1300),格子越小能塞的帧越多 —— 所以"这一帧占多大"本身就是信息量分配,也直接决定钱。

    帧 = 一个镜头(真切点切段 + 积分采样点补段),档位 = 那个位置的切点密度:

    档位格子边长800 宽画布上的实际像素典型场景
    11/1800×45020 秒的长镜头
    21/2400×2253–5 秒的常规镜头
    41/4200×1131 秒左右的快切
    81/8100×560.3 秒一闪而过的快闪

    档位是算出来的(100 帧卷积窗 → 每帧切点概率 → 50 帧平滑 → 7 阈值 + 5% 滞回), 由积分假切点切出来的帧再 +3 档(那里画面一直在变,一帧代表不了多久)。 两个旋钮可以覆盖它 —— 一个是预设,一个是精确映射:

    video_storyboard({ input, strategy: 'saver' })                    // 预设:每帧降一档
    video_storyboard({ input, tierMap: { 5: 4, 6: 4, 7: 4, 8: 4 } })   // 精确:5–8 档一律按 1/4
    

    六个档位(第一个是默认)

    strategy每帧多大90 秒 PV 实测什么时候用
    auto(默认)张数按时长定(≈4 分钟一张),再挑最小格还认得出的最省那档1~3 张什么都不填时就用它
    full升一档(1 档不再升)18 张 / 6912 token要读画面里的字、认脸
    standard1 档压成 1/25 张 / 1920想看清每个镜头
    compact降一档3 张 / 1152日常:看得清又便宜
    skim全部 1/8,无假切点,切点按 0.35 s 合并1 张 / 384一条 PV 一张图
    brief全 1/8,切点间隔自适应加大到整段装进一张图1 张 / 3845 分钟视频一张图看完

    auto 是 0.6.1 的默认值,起因是一次真实的账:新会话看到枚举里第一个名字是 full, 就选了它 —— 一条 90 秒 PV 花掉 6912 token,是 compact 的 6 倍。模型看不见"多一张图多少钱", 所以选档这件事收回工具自己:

    • 张数只按时长算(autoSheetCap,默认 240 s/张,DSH_STORYBOARD_SECONDS_PER_IMAGE 可调): 5 分钟 → 1~2 张,30 分钟 → 8 张,2.5 小时 → 39 张。同一时长的片子,钱是可预期的。
    • 在预算内挑最省的那档:张数一样时 compact 装得下更多帧。只有当最小格已经认不出东西 (< shot)才往 standard / full 退;压缩到色块就翻倍张数重试(最多三轮)—— 预算让位于可读性。
    • 结果里会写 size policy: auto chose … 和"同一批帧在别的档要几张",下一次它就有依据覆盖。
    • 不想要 auto:DSH_STORYBOARD_AUTO=0,或 ~/.dsh/video.json 里 {"storyboard": {"auto": false}}, 退回 compact(0.6.0 的默认值)。

    不知道这部片有多少刀时,先用 dryRun: true 问一次(扫描有缓存,第二次几乎免费):

    video_storyboard({ input, dryRun: true })
    // → cuts="46 (avg 2.0s)" · frames=21 · compact 1 image / standard 1 / full 2 / skim ≤1
    //   recommended: compact
    video_storyboard({ input, strategy: 'compact' })   // 再真正出图
    

    0.6.0 之前叫 quality / balanced / saver / frugal / thrifty / ultra(还有"乞丐/恭喜发财/丐中丐" 这种名字)。旧名仍然认,会自动映射到新名,不会报错。

    tierMap 是精确覆盖,接在 strategy 之后:没列出的档位保持原值,多档映射到同一档等于合并尺寸, 补位帧(自由尺寸)不参与映射,越界的键值直接忽略。清单里会回显 tierMap=1->1,2->1,…。

    为什么不是固定宫格

    旧版(≤0.5.x)是"你要几格,我就切成 k×k"。问题不在实现,在前提:它假设所有镜头一样重要。

    一段 90 秒的 PV 里,前 20 秒可能只有 6 个长镜头,后 20 秒有 24 个 0.25 秒的快闪。固定 16 格平均分, 结果是长镜头浪费了 12 格的面积,快闪被压成 100×56 的色块 —— 两头都没照顾好。

    现在这张图是这么排出来的:

    1. 帧 = 镜头。切点切段,一段一帧。长镜头就是一帧,快闪也是一帧,时间轴永远连续。
    2. 格子大小 = 那个位置的切点密度(8 档,边长 1/k)。快的地方小格、慢的地方大格, 同一张图里两种节奏同时可见 —— 不用读数字,看格子大小就知道哪里开始快剪。
    3. 按行打包 + 裁到内容。装不下才开下一张;排完把底部/右侧的空白裁掉,所以每张图高度不同, 但内容一点不少。
    4. 补位。某张图只剩 2 帧(片尾标题 + 演职员表)时,把那几段再切几刀,凑成能铺满的格数。

    实测同一段 90 秒 PV:

    版本张数token快剪区格子长镜头格子
    0.5.x 固定 16 格2768100×56(糊)200×113(浪费)
    0.6.0 档位排布3(compact)1152114×54 但按密度分配400×225

    多花 384 token,换来长镜头 4 倍清晰度 + 快剪区不再裁掉镜头。这是这套算法唯一想做的事。

    安装

    从零开始的完整步骤(含依赖、自检、排错)见 docs/INSTALL.zh.md。

    三句话版本:

    winget install Gyan.FFmpeg                    # 必须:ffmpeg + ffprobe
    pip install --user -U yt-dlp                  # 只有贴 B 站/YouTube 链接才需要
    dsh plugin --profile web add dsh-video-storyboard       # 装插件,然后重启 DSH
    

    dsh plugin 只管 web 这类普通 profile;桌面版(Electron)的 desktop profile 会被拒绝, 用源码安装:git clone 后 node scripts/install-dev.mjs(复制目录,改完要重跑)。

    依赖必须吗不装会怎样
    ffmpeg + ffprobe必须第一步就报错
    yt-dlp可选本地文件照常能用,贴链接不行
    ASR 服务可选能出图,但清单里没有台词

    插件不打包 ffmpeg 和 yt-dlp,只调用你自己装的那份。找不到时会给出安装指引,不会静默失败。

    怎么用

    贴一个链接

    https://example.com/some-video
    

    任何 yt-dlp 支持的站点都行(YouTube、Vimeo、各种自建站……完整列表见 yt-dlp 文档)。 插件会自己下载、缓存,然后用本地文件的那套流程跑。竖屏、DASH、多分P 都处理了。

    ⚠️ 能不能下,取决于你:请自行确认对目标内容有下载/使用授权,并遵守该站点的条款与 当地法律。本项目不提供任何绕过付费、会员、DRM 或区域限制的能力,也不代为判断授权。

    本地文件

    node bin/storyboard.mjs film.mp4 --out film.png                    # 默认:均衡,一条 90 秒 PV 5 张
    node bin/storyboard.mjs film.mp4 --strategy thrifty --out cheap.png # 全 1/8,一张图 384 token
    node bin/storyboard.mjs film.mp4 --strategy ultra --out one.png     # 丐中丐:间隔自适应到装进一张
    node bin/storyboard.mjs film.mp4 --tier-map '{"5":4,"6":4,"7":4,"8":4}'   # 快剪全部按 1/4
    node bin/storyboard.mjs film.mp4 --max-sheets 4 --out film.png      # 硬性限制张数
    node bin/storyboard.mjs film.mp4 --batch 300 --out film.png         # 长片按 5 分钟切窗
    node bin/asr-check.mjs film.mp4 --from 100 --seconds 12             # 只测 ASR
    

    参数

    参数默认说明
    input—本地路径或 http(s) 链接
    from / to全片只看这一段
    maxFrames0(不限)最多取多少帧(一段 = 一个镜头)。0 = 不封顶:几千刀就几千帧;填正数才按"合并最短的段"收缩
    strategyautoauto(默认):先按视频时长定张数预算(≈4 分钟一张),再在 compact/standard/full 里挑"最小格还认得出、张数最少"的那档,并把三档各自的代价写进清单。显式值:full 每帧升一档 / standard 只把 1 档压成 1/2 / compact 降一档 / skim 全 1/8 + 无假切点 + 切点按 0.35s 合并 / brief 切点间隔自适应加大到整段装进一张图(旧名 quality/balanced/saver/frugal/thrifty/ultra 仍认)
    dryRunfalse只扫描 + 排布,不出图、不取帧、不转写:回报切点数、帧数、每个档位要几张图。用来在花第一张图之前决定档位
    scanGap0两个切点之间至少隔几秒(0 = 不合并)。调大能把连闪的镜头并成一个边界
    integraltrue是否在长镜头里按"画面变化积分"补帧。关掉就只留真切点
    integralSeconds6一个采样帧代表多少累积变化(1–30)。阈值对着固定参考强度,不是"每 N 秒一帧":画面动得快就补得密,慢就补得疏,完全静止的镜头一个点都不补
    tierMap—档位映射表 {5:4,6:4}:接在 strategy 之后,未列出的档位保持原值;多档映射到同一档 = 合并尺寸
    maxSheets0(不限)最多几张图。0 = 排布器说几张就几张;填正数则超了合并段重排(时间覆盖不变)
    budget360000单图像素,默认 360k = 800×450(≈233 token)。上限 1.69M = 1300×1300(≈994 token):按像素线性计费,抬上去是加钱买清晰度,不是免费;低于 ~296k 也不再便宜(计费时按 544² 放大算,192 token)—— 所以给更小的 budget 会被抬到地板并在清单里说明
    pickmid取帧策略:mid 段中点 / sharp 3 候选选最锐(3 倍解码)/ thumb 段内最典型
    burnindex烧格号;none 关掉
    transcriptautoauto / off
    transcriptFile—直接喂已有的 SRT/JSON,跳过 ASR
    detailmed每格台词预算:low 60 / med 120 / high 400 字
    cellRange—分页看文本清单

    输出长什么样

    一张图 + 一段文本:

    <storyboard path="film.mp4" duration="18.40" window="0.00-18.40" image="800x469"
                grid="30 frames on 1 image(s)" burn="index" cell>=93x47
                tokens="233" cuts="29 (avg 0.6s)">
    plan: strategy=balanced (1 档压成 1/2 尺寸) · 30 frames on 1 image(s) ·
          tiers 1/2×5 1/4×1 1/6×2 1/7×2 1/8×20 · canvas=800x450 · fill=99.4% · maxFrames=96
    cost: 1 image(s) ≈ ~233 vision tokens (233/img avg; 42px token grid, floor 544², cap 994)
    
    cell 1 = 00:00.00-00:02.00  k=2 (1/2)
    cell 7 = 00:12.00-00:12.27  cuts 1  k=6 (1/6)
        "这段台词"
    

    一行一个镜头,k= 就是这一帧的档位(边长 1/k)。长镜头是大格、快剪是小格, 所以一张图里同时能看到"这里是一个 20 秒的长镜头"和"这里是 24 个 0.25 秒的快闪"。

    几条设计原则:

    • 清单只给原材料,不下结论。 不说"这是高潮""这里是角色 A",只给时间、刀数、台词 —— 判断是模型的事。
    • 每加一行都要能说出它挡掉了什么误判。 说不出来就不加。
    • 数字不可信时要说出来。 例如连续运镜被逐帧切碎时,清单会写 cut count unreliable here,而不是让模型拿 cuts=N 当节奏依据。

    算法要点

    1. 画面差异:缩到 64×36,逐帧比 HSV,色相按环形距离且按饱和度降权(灰色的色相没有意义),得到 0–255 的变化分。
    2. 切点:分数超过阈值即一刀。阈值不是固定的 —— 按本片自己的分数分布和时长推。
    3. 爆点重判:对每个 2 秒窗口,用一帧的间隔重新判一次。粗扫看不见 0.35 秒以下的刀,而快闪全在那一档。
    4. 帧 = 镜头:真切点切段,积分采样点在真切点之间补(画面变化率累够一个阈值就记一个假切点、 然后清零,往复)。一段一帧,时间轴永远被完整覆盖。
    5. 档位 = 切点密度:把切点看成 0/1 函数,用 100 帧宽的窗做卷积得到"每帧切点概率",再平滑; 7 个阈值 + 5% 滞回把它切成 8 个档。档位 k 的格子边长就是 1/k。 由积分假切点切出来的帧额外 +3 档 —— 那里画面在连续变化,一帧代表不了多久。
    6. 排布 = 按行打包:档位序列喂给一个 DP,按行精确最优地填满方形画布,装不下才开下一张。 排完再把画布裁到内容(底部/右侧不留灰条),所以每张图高度不同、内容一点不少。
    7. 补位:排完发现有图填充率 < 68%(片尾标题 + 演职员表那种),就把那张图里最长的几段 再切几刀多取几帧,把帧数凑成完全平方数(6 个正方形格子只能铺 2/3,9 个才能铺满), 重排到填满为止。时间轴不断,但不会为此多开图(一张图 384 token,张数才是成本)。

    实测数字

    档位怎么影响成本(同一份切点,90 秒概念 PV,1080p24,59 刀 / 13 个积分点):

    strategy帧张vision token最小格
    full73186912—
    standard735192095×62
    compact7431152114×54
    skim531384114×54
    brief561384100×56

    长视频会明显变贵(5 分钟 / 299 秒 / 150 刀):

    strategy帧张token
    standard195197296
    compact201103840
    skim14931152
    brief51(切点间隔自适应到 3.6 s)1384

    其它:

    项目数字
    单图 token≈233(800×450,42px 一格)—— 格子数不影响,像素才影响
    端到端90 秒约 8 秒;5 分钟约 40 秒(含解码、扫描、取帧、拼图)
    快闪重判零额外解码(复用逐帧分数)
    切点召回概念 PV 59 刀 / 正式 PV 67 刀 / 5 分钟口播 150 刀

    需要登录的视频(B 站 1080P+ / HTTP 567)

    先说结论:普通视频匿名就能看 360p;要高清、会员、或遇到 567,才需要 cookies。

    什么时候需要

    现象需要 cookies
    提示 HTTP Error 567✅ 这是 B 站风控,多半是"该视频要登录"
    Sign in to confirm / 需要登录✅
    想要 1080P 以上的清晰度✅(很多站点的高清要登录态)
    普通视频、360p、匿名❌ 不需要

    三步导出(Chrome / Edge)

    1. 装扩展 Cookie-Editor(作者 cgagnier)
    2. 在目标站点登录 → 点扩展 → Export → 选 Netscape 格式 → 存成 cookies.txt
    3. 告诉插件它在哪:
    // ~/.dsh/video.json
    { "cookies": "D:/cookies/cookies.txt" }
    

    或者每次调用时传:

    video_storyboard({ input: "https://example.com/some-video", cookies: "D:/cookies/cookies.txt" })
    

    也可以设环境变量 DSH_VIDEO_COOKIES(桌面版读不到 setx 的变量,优先用配置文件)。

    为什么不能用 --cookies-from-browser:Chromium 127+ 的 App-Bound Encryption 让 SESSDATA 只能由浏览器进程解密, 普通进程读出来是 v20 密文。所以必须导出文件,这是机制问题,不是你配错了。

    不想配 cookies?

    直接把视频下载下来,喂本地路径——插件对本地文件一视同仁,还更快:

    video_storyboard({ input: "D:/video/xxx.mp4" })
    

    隐私提醒

    cookies.txt 里的 SESSDATA 等同于你的登录凭证。别提交到仓库、别发群里、别贴到 issue 里。 用完可以退出登录让它失效。

    完整步骤(含格式示例、验证顺序、故障对照)见 docs/SETUP.zh.md §3。

    字幕与 ASR(怎么让它"听懂")

    想看完整可用的配置(目录结构、asr.json / video.json 全文、实测数字、排障对照): docs/CONFIG.zh.md。

    转写是可选的:不配也能出图,只是清单里没有台词。

    优先级(第一个能用的赢)

    1. 视频自带字幕轨 → 直接读,不跑 ASR
    2. transcriptFile → 喂已有的 SRT/JSON,完全不碰 ASR
    3. ASR 服务 → 上面两个都没有时才用

    我们用的是哪个模型

    后端模型冷启动体积标点热词
    sherpa(默认)paraformer-zh-2024-03-09(int8)+ ct-transformer 标点 + silero-vad约 5 秒500 MB另加载 onnx 模型❌ 无接口
    funasr(可选)seaco_paraformer_large + fsmn-vad + ct-punc约 30 秒2.1 GB内置✅
    云端FunAudioLLM/SenseVoiceSmall / whisper-1 …0(联网)—视服务而定视服务而定

    为什么默认是 sherpa:2024 版 paraformer 比旧版准(实测「像差」不加热词就对),而且冷启动 30 秒 → 5 秒、 内存 6.8 GB → 不到 1 GB。代价是它在 sherpa 运行时里没有热词接口,同音字只能靠 terms 表后处理。

    什么时候切回 funasr:你的片子里专有名词多、热词列表很长(比如人名一堆),热词的收益大过 30 秒启动成本时。

    // ~/.dsh/asr.json 里加一行即可切后端(重启服务生效)
    { "model": "paraformer-zh-2024-03-09" }   // 在 start-asr.cmd 里设 ASR_BACKEND=funasr 则走另一套
    

    配置 A:本机服务(免费、不联网)

    ~/.dsh/asr.json:

    {
      "baseUrl": "http://127.0.0.1:8000/v1",
      "model": "paraformer-zh-2024-03-09",
      "language": "zh",
      "hotwords": "像差 焦段 光圈",
      "manage": {
        "start": ["D:/asr-local/.venv/Scripts/python.exe", "D:/asr-local/server-sherpa.py"],
        "idleMs": 5000,
        "waitMs": 60000
      }
    }
    

    start 也可以直接写成 .py 路径 —— 插件会自动按同目录的 .venv 补解释器。 但数组最明确([exe, ...args])。字符串形式(.cmd)也能用,但 Windows 上会闪一个 控制台窗口,而且批处理对行尾极敏感——LF 行尾的 .cmd 会被 cmd.exe 当成命令逐片段执行, 弹出一屏 'wo' 不是内部或外部命令。已踩过。

    • 本机地址不需要 key(127.0.0.1 / localhost / [::1])。自己跑的服务一般不校验 Authorization,插件不会再逼你填一个假 key。
    • manage 是生命周期托管:调用时端点不通就拉起;最后一次运行结束后 idleMs 内没有新运行,就请它自己退出(走服务的 POST /shutdown)。

    idleMs 要按后端设,两者差 100 倍:

    后端建议 idleMs理由
    sherpa5000(5 秒)冷启动只要 5 秒,随用随关,内存立刻还回去
    funasr600000(10 分钟)冷启动 30 秒,关太早等于每次重付;关太晚又占着 6.8 GB

    hotwords 只对 funasr 后端有效 —— sherpa 的 paraformer 没有热词接口,同音字请写进 terms。

    服务端要满足两个约定:

    GET  /health        → 200 即可(拿不到就读 /v1/models)
    POST /shutdown      → 返回后退出(不支持也行,插件会退回杀进程)
    POST /v1/audio/transcriptions   → OpenAI 兼容,返回 verbose_json
    

    配置 B:云端服务(有 key 就行,任何 OpenAI 兼容)

    {
      "baseUrl": "https://api.siliconflow.cn/v1",
      "key": "sk-...",
      "model": "FunAudioLLM/SenseVoiceSmall",
      "language": "zh"
    }
    

    换成 OpenAI:

    { "baseUrl": "https://api.openai.com/v1", "key": "sk-...", "model": "whisper-1" }
    

    便宜的选择(自己按需改):

    服务baseUrlmodel 示例
    SiliconFlowhttps://api.siliconflow.cn/v1FunAudioLLM/SenseVoiceSmall
    OpenAIhttps://api.openai.com/v1whisper-1
    阿里云百炼https://dashscope.aliyuncs.com/compatible-mode/v1paraformer-v2
    Groqhttps://api.groq.com/openai/v1whisper-large-v3-turbo

    云端不填 key 会直接跳过 ASR(不会报错,清单写 transcript: none)。

    配置 C:直接用现成字幕

    video_storyboard({ input: "film.mp4", transcriptFile: "film.srt" })
    

    支持 SRT / VTT / JSON([{ "start": 1.2, "end": 3.4, "text": "..." }])。这条路零 ASR 成本,也最准—— 平台自带的字幕比任何识别都可靠。

    环境变量(优先级高于文件)

    DSH_ASR_BASE_URL   DSH_ASR_MODEL   DSH_ASR_LANG
    DSH_ASR_KEY        DSH_ASR_KEY_ENV
    

    桌面版 DSH 的进程环境由启动器决定,setx 设的变量宿主读不到 —— 优先用 asr.json。

    硬字幕校正(detail: high 时自动开)

    画面里烧进去的字幕是确定的文本,ASR 会听错。开 detail: high 时插件会:

    1. 抽 2–3 张原分辨率帧,找出字幕带(≈3 秒,只做一次)
    2. 对若干条台词,只裁那一条带子做 OCR(≈0.26 秒/条,不做检测)
    3. OCR 文本能覆盖 ASR 那句(≥60% 的字按顺序对得上)就替换,否则保留 ASR
    明细里会写:on-screen subtitles used to correct 1 of 3 checked lines:
                 4.4s "他也忘记。" → "也要忘记吗"
    

    实测(同一段 PV):ASR 出「所以。他也忘记。」,画面是「你连,也要忘记吗」——OCR 把整句救回来了。

    这是抽检,不是全片校对。 默认只抽查 16 条,清单里写的是 spot-checked N lines, corrected M —— 请按这个口径读,别当成"整片台词已核对"。

    贴链接时(URL 模式)跑的是下载的 360p 扫描副本,scale=1280 放大不回细节,硬字幕本来就糊; 清单会标 [from the 360p scan copy]。想要更准就用本地高清文件。

    全程本地、不花 token。但引擎要你自己装 —— 插件不带。

    跟 ffmpeg 一个道理:模型有 300MB 上下,还绑一个 Python 环境,塞进 npm 包只会让所有人多下一坨。 插件只负责调用你装的那一份。

    # 1) 建一个独立环境(别装进 ASR 那个 venv,两个模型的依赖会打架)
    python -m venv D:/ocr-local/.venv
    
    # 2) 装引擎:模型包 + 推理后端,两个都要
    D:/ocr-local/.venv/Scripts/python.exe -m pip install rapidocr onnxruntime
    

    别照老教程装 rapidocr-onnxruntime —— 那个包把 requires-python 写死在 <3.13, 在 Python 3.13 上直接 No matching distribution。新版包叫 rapidocr,import 名和返回结构 都变了,插件两个都认(先试新的)。

    # 3) 告诉插件用哪个 Python(桌面版宿主读不到 setx 的环境变量,所以写文件)
    #    ~/.dsh/video.json
    { "ocr": { "python": "D:/ocr-local/.venv/Scripts/python.exe" } }
    

    不配这一行也能用:插件会依次试 DSH_OCR_PYTHON → asr.json 里那个 Python → PATH 上的 python。

    怎么确认配好了:跑一次 detail: high,清单里出现 on-screen subs: spot-checked N lines 就是通了;还是 skipped (no-python-with-rapidocr) 说明没找到。DSH_OCR_DEBUG=1 会打印 它试过哪些解释器、以及每条主句的 OCR 原文和判定理由。

    没装就静默跳过,不影响出图 —— 只是没有字幕校正。

    装完不用重启。 插件对"找不到解释器"的结果只缓存 60 秒,装完最多等一分钟就自动用上 (以前是进程内永久缓存,必须重启整个 DSH 宿主,而且报错完全看不出是这个原因)。

    校正失败时清单里写的是具体原因,不再一律 detect-failed:

    清单里的字样意思
    bridge-missing桥接脚本没找到(安装损坏;正常安装不会出现)
    bridge-timeout单次 OCR 超过 120 秒
    bridge-crashedPython 进程非零退出(栈在 DSH_OCR_DEBUG 的 stderr 里)
    bridge-errorPython 自己报的错,后面跟着原文(多半是没装 rapidocr / onnxruntime)
    no-band检测跑通了,但画面里找不到像字幕的东西

    热词(比换模型更有效)

    热词要按片子传,不要写死在全局。 全局那份是默认值,学科不对时它是负偏置(拿摄影词去识别材料学讲座,只会帮倒忙)。

    video_storyboard({ input: "materials.mp4", hotwords: "位错 滑移 柏氏矢量 晶界 层错 固溶体" })
    

    不传就用 asr.json 里的 hotwords;两者都没有 = 不做偏置(最安全)。

    识别错同音字时,不要先想着换模型 —— 通用模型不知道你片子在讲什么。

    "hotwords": "像差 焦段 光圈 沧野 玄学"
    

    空格分隔,交给 paraformer 解码时做偏置。实测(镜头评测视频):

    加之前:也就是所谓的相差。
    加之后:也就是所谓的像差。
    

    只加了一个词,别的一个没动。专有名词、人名、领域术语都该放这里。

    需要服务端支持:seaco_paraformer 原生支持;自己搭服务时认 hotword 表单字段即可 (不认这个字段的服务会忽略它,不会报错)。

    术语校正

    识别结果里的固定错字可以确定性替换(不进模型):

    "terms": { "绘插": "彗差", "星尘边境": "Stardust Frontier" }
    

    替换次数会报在清单里(termsFixed N),不静默。

    和同类插件比(诚实的版本)

    生态里做这件事的有三种路线,我们只走其中一条:

    插件做法一次 90 秒片子的成本它更强的地方
    dsh-video-see抽帧作为真图片交回模型,每张带时间戳20 帧 ≈ 7,680 token每一帧都是 ~1000px,小字、表情都清楚
    dsh-video-lens关键帧 → 外部 VLM → 返回 evidence JSON帧数 × 384 + VLM 调用费有 video_ask 时间锚定问答;结论结构化
    watch-skill帧 + 转写 + OCR 索引成库,按需查询建库贵,追问几乎免费答案带回执,可被另一个东西复核
    我们一张宫格图 + 文本清单,模型自己看约 1,900 token单位 token 覆盖的时间最长(1 张图 = 90 秒)

    同一部 90 秒片子,各种"按镜头取关键帧"的方案落在 3,900–8,500 token(8 / 16 / 20 帧 + 字幕摘要), 我们是它的 1/2 到 1/4.5。不是 1/8 或 1/16 —— 那种数字是拿"每秒抽一帧"当对手,不诚实。

    我们明确输的地方

    输给谁为什么我们的补救
    dsh-video-see / lens单帧清晰度(每帧 ~1000px vs 我们的格 331×184)video_zoom 再取一次,但那是第二轮成本
    watch-skill多轮追问的边际成本(它索引一次就能反复查)清单里明说"图已在上下文",追问不再重新出图
    watch-skill可验证性(答案能被第三方复核)每张图配一份 .evidence.json,时间码可回看
    lens结构化 evidence、时间锚定问答video_locate 做时间定位,evidence.json 给结构

    适合谁:想先搞清"这部片子讲什么"的人。 不适合谁:逐帧辨字、要复核结论、对同一部片子问二十个问题 —— 那三种情况请用上面三家。

    明确不做的

    • 不烧时间码进画面 —— 占像素、压缩后先糊,时间在文本里有 0.01 秒精度。
    • 不做说话人分离 —— 实测 cam++ 在 1.9 秒短句上把单人素材切成 6 个人。
    • 不打包 ffmpeg / yt-dlp —— 版本和安全都由用户掌握。
    • 不把"结论"写进清单 —— 那是模型的工作。

    已知限制

    • 画面小于约 1% 的改动(PPT 上多一行字)在 64×36 上不可见,再聪明的阈值也救不回来。
    • 连续运镜密集时,cuts=N 会失真(清单会标注)。
    • 极暗素材的取帧锐度判断不稳。
    • 1/8 档的格子是 100×56,只够认出"换没换镜头",认不出字幕和脸。
    • 混合档位的图,行内矮格上方必然有一条缝(行高取本行最高那格)——这是行排布模型的固有损失, 所以填充率通常落在 79–95%。想更高只能靠 tierMap 把档位压得整齐,或者用 thrifty / ultra。
    • 片头长黑场会独占一格(黑场也是一个"镜头",没有切点就切不开)。

    证据与复核

    每次 saveStoryboards 会同时写一份 <名字>.evidence.json:每格的起止时间、取帧时刻、刀数、字数、原文。 清单是给模型读的散文,这份是给人和脚本复核的 —— 想知道"第 5 格真的是 3:29 吗",对着时间码回看原片即可。

    文档

    文档内容
    docs/INSTALL.zh.md从零安装:依赖、自检、排错表
    docs/USAGE.zh.md完整手册:三条安装路径、六个工作流、每个参数
    docs/HISTORY.zh.md从 v0.1 到现在,按"被什么打脸"组织
    docs/原理图解.zh.md用一支 MV 把流程拆成 7 步,每步配图
    CHANGELOG.md逐版本变更
    CREDITS.md第三方致谢、社区贡献、测试素材作者
    README.en.mdEnglish

    赞助

    这个插件是免费的,也不打算收费。如果它帮你省下了 API 费用或剪辑时间, 欢迎请我喝杯奶茶 —— 完全自愿,不影响任何功能。

    仓库右上角的 Sponsor 按钮也指向这里(.github/FUNDING.yml)。

    许可

    见 LICENSE。