dsh-tool-jizura
调用 JIZURA 在线应用,将歌词文本自动生成为文字 PV(MP4 视频或 PNG 序列)的 DSH 工具插件。
- Stars
- 1
- Language
- TypeScript
- Created
- Oct 7, 2026
- Updated
- Oct 7, 2026
Introduction
dsh-tool-jizura
给 DSH 用的工具插件:把一段歌词(可附音乐)交给 JIZURA 在线应用,自动产出文字 PV(MP4 视频或連番 PNG)。

上图是插件实际驱动 JIZURA 之后的界面(无头 Chromium 跑真实流程截取):
曲名 / 艺术家已填入;音频显示为 test-beat-120.wav (00:12.00 ・ 約120BPM)——BPM 是 JIZURA 检测出来的;
各行起始时间 0.48 / 2.99 / 4.89 / 7.85 全部咬在 0.5 秒的拍网格上,即踩点生效;
右侧「いまの案」显示 スタイル=ノワール・クロマ、雰囲気=エモーショナル,
正是 stylePreset: "dark" 展开成的样子;構成为 10 カット / レイアウト 9 種。
功能
- 自动踩点:提供背景音乐后,插件会等待 JIZURA 完成异步 BPM 检测,让画面切点咬在节拍上,
并把检测到的
bpm/ 拍数 / 音频时长回传,方便确认踩点真的生效(而不是以为生效)。 - 情绪 + 强度:可指定 8 种情绪(
J.MOODS)与 0~1 的强度,强度会在该情绪自带的区间内线性插值, 直接驱动 JIZURA 的 13 个效果强度参数(motion / glitch / chroma / texture / density …)。 - 自动段落对比:按音乐能量包络自动判定每一行的响度,让安静的段落收敛、激烈的段落切得更碎。
- 画质 / 帧率 / 分辨率 / 画幅:
quality决定视频码率(standard/high/max),fps可选 24 / 30 / 60,resolution可选 720 / 1080 / 1440 / 2160。 - 主题 / 曲名 / 艺术家 / 绿幕背景 / 随机种子,以及可复现的确定性输出。
- 遵守 DSH 的取消信号:调用被中止时立即关闭浏览器进程。
安装
dsh plugin --profile web add github:Flooding-Rain/dsh-tool-jizura
前置依赖
插件依赖 Node.js 侧的 Playwright,首次使用前请确保浏览器已就绪:
npx playwright install chromium
使用示例
只要歌词:
{ "lyrics": "夜明けの色を/覚えてる\n*透明*なままの街" }
带音乐、定向情绪、竖屏:
{
"lyrics": "夜明けの色を/覚えてる\n*透明*なままの街\n君の名前を呼んだ",
"audioPath": "D:\\music\\song.mp3",
"mood": "emotional",
"theme": "ballad",
"intensity": 0.7,
"aspectRatio": "9:16",
"resolution": 1080,
"outputDir": "D:\\output\\pv",
"title": "夜明け",
"seed": 20261007
}
一次完整的手工验证(dsh 会话里):
用 generate_jizura_pv 把下面这段歌词做成 16:9 的 MP4,情绪用 emotional,强度 0.7, 配上 D:\music\song.mp3,输出到 D:\output\pv
夜明けの色を/覚えてる *透明*なままの街
参数
| 参数 | 类型 | 必填 | 默认 | 说明 |
|---|---|---|---|---|
lyrics | string | ✅ | — | 歌词文本,支持多行。JIZURA 记法(/ 切分、*强调*、[間奏 8]、LRC 时间戳)原样可用。 |
audioPath | string | — | 背景音乐绝对路径。提供后才会自动踩点。 | |
title | string | — | 曲名,显示在标题卡 / HUD;也会用作输出文件名。 | |
artist | string | — | 艺术家名。 | |
stylePreset | string | auto | auto | light | dark | neon,展开为 style + mood + intensity 三项。 | |
styleKey | string | — | **直传 JIZURA 的样式 key,优先于 stylePreset。**共 27 个,见下节。 | |
mood | string | 随预设 | glitch | calm | pop | graphic | editorial | emotional | horror | chaos | |
theme | string | — | lyricpv | kinetic | wa | horror | pop | ballad,限定随机取材方向。 | |
intensity | number | 随预设 | 0~1,在该情绪的强度区间内插值。 | |
paletteLock | boolean | false | 锁定样式的首套配色,整片不换色。 | |
autoDynamics | boolean | true | 按音乐能量做段落对比。仅有音频时有效。 | |
hideNo | boolean | false | 隐藏装饰编号(No.08 之类)。 | |
hideTime | boolean | false | 隐藏装饰时间码(LINE 08 · 00:36.52 之类)。 | |
aspectRatio | string | 16:9 | 16:9 | 9:16 | 1:1 | |
resolution | integer | 1080 | 720 | 1080 | 1440 | 2160 | |
fps | integer | 24 | 24 | 30 | 60。 | |
quality | string | high | standard | high | max,决定视频码率。 | |
outputFormat | string | mp4 | mp4 | png_sequence | |
outputDir | string | ./jizura-pv-output | 输出目录绝对路径,不存在会自动创建。 | |
keyBg | string | off | off | green | black,合成用纯色背景。 | |
seed | integer | — | 随机种子。给定后同样的输入得到可复现的结果。 |
预设展开表
stylePreset | JIZURA 样式包 | 情绪 | 强度 |
|---|---|---|---|
light | paper(ペーパー・インク,#ECE9E3 亮纸底) | calm | 0.35 |
dark | noir(ノワール・クロマ,#060607 黑底白字) | emotional | 0.65 |
neon | magenta(ポップ・マゼンタ,#FF0A8C 震撼粉) | pop | 0.85 |
auto | 不覆盖 | 不覆盖 | 不覆盖 |
mood / intensity 显式给出时会覆盖上表的推断值。
全部 27 个样式(styleKey)
stylePreset 只展开 3 个常用样式;想用别的就直接传 styleKey。基础 12 个:
noir crimson caution magenta paper hud mint specimen transit blueprint rouge mono
扩展 15 个:
hrRuin hrNightRec hrCurse sakura ocean sunset forest vapor newsprint synth80 kraft candy acid sumi gold
扩展样式受「追加分 / 和風」开关影响、平时不会被随机选中,但直传不受限制。
styleKey 无效时忽略并沿用 stylePreset;实际生效值可从返回值的 style 读到。
intensity 到底调了什么
它在当前情绪的强度区间内线性插值,一次驱动 13 个参数。以 calm 为例,
intensity: 0.22 会得到:
{ "motion": 0.35, "glitch": 0.09, "chroma": 0.27, "decor": 0.27, "density": 0.29,
"texture": 0.58, "bgSwitch": 0.14 }
即 下限 + (上限 − 下限) × intensity。同一个 intensity 在不同 mood 下观感不同。
paletteLock 与 autoDynamics 做了什么
paletteLock:每个样式含 2~4 套配色,planner 会按fx.bgSwitch的概率逐行换色 (calm的区间是[0.1, 0.3],几十行累积下来仍会跳好几次)。打开后把fx.bgSwitch压到0,整片维持首套配色。实测:不锁时 90 个 cut 分属 4 套配色,锁后 90/90 全在第 0 套。autoDynamics:算每行平均能量与四分位,以 planner 自己算出的 cut 数为基准缩放 —— 高于 Q3 的行 ×1.5(至少 +1,切得更碎),低于 Q1 的行 ×0.5(收敛),中间档不动。 能量没有起伏(Q1 == Q3)时整体跳过。
早期实现把高低两档写死成
cuts = 3/cuts = 1,而 planner 依fx.density与行时长算出的 切分常在 4~6 刀 —— 那等于把激烈的段落改慢了,与「切得更碎」的说法相反。现已改为按基准缩放。
返回值
execute 返回一个结构化对象(而不是只返回路径),让模型能判断踩点等关键步骤是否真的生效:
{
"path": "D:\\output\\pv\\夜明け.mp4",
"bpm": 120, // JIZURA 检测到的 BPM;0 表示没有音频
"beatCount": 25, // 检测到的拍数
"audioDuration": 12, // 音频时长(秒)
"style": "noir", // 实际生效的样式
"mood": "emotional", // 实际生效的情绪
"intensity": 0.7, // 实际生效的强度;-1 表示由随机决定
"seed": 20261007, // 复现键(回显调用方传入的 seed)
"aspect": "16:9",
"resolution": 1080,
"fps": 24,
"quality": "high", // 导出画质档位
"bytes": 7111804, // 产物字节数
"dynamicsApplied": true // 能量驱动的段落对比是否生效
}
界面卡片上会显示成:
PV 已生成: D:\output\pv\夜明け.mp4
画面 16:9 / 1080p / 24fps · 画质 high · 6.8 MB
样式 noir · 情绪 emotional · 强度 0.70
踩点 BPM 120 · 25 拍 · 音频 12.0s
已按音乐能量做段落对比
非理想结果
| 情况 | 行为 |
|---|---|
| 歌词为空 / 纯空白 | 不启动浏览器,返回各字段为零值(path: ''),卡片提示「PV 未生成:歌词为空」。 |
没有 audioPath | 正常出片,但 bpm: 0、dynamicsApplied: false,卡片显示「未提供音频,本次没有踩点」。 |
| 浏览器启动失败 / 导航超时 / 音频分析超时 / 找不到关键元素 | 抛异常,工具以错误卡片呈现。 |
关于 outputFormat
mp4:点 JIZURA 的「MP4 を書き出す」,得到单个.mp4。png_sequence:点「連番PNG(ZIP)」。注意 JIZURA 原生只会打包成一个 ZIP, 因此产物是.zip而非一堆裸 PNG;需要逐帧图片时自行解压。
实现原理:调 API,而不是点 UI
这是本插件与"用 Playwright 点按钮"最本质的区别。JIZURA 把内部状态与算法都挂在 window.J 上,
src/12_ui.js 结尾处还专门为嵌入式宿主导出了 J.uiApi:
J.ui = S; // 完整状态对象,S.project 即全部设置
J.uiApi = { toast, replan, syncUI, pause, seek, flushSave, loadAudioFile, restartPreview, exportRange, exportRangeLines };
插件用到的接口:
| 接口 | 用途 |
|---|---|
J.ui.project | 31 个可读写字段:style mood seed aspect res fps title artist keyBg themeId fx timing overrides enabled … |
J.uiApi.replan() / syncUI() | 改完状态后重排并同步界面 |
J.omakase(project, rnd, themeId) | 「おまかせ」本体;传入自定义 rnd 即可用 seed 复现 |
J.MOODS[mood].fx | 每种情绪的效果强度区间,用于把 intensity 插值成 13 个参数 |
J.THEMES | 主题 → 情绪池的映射 |
J.analyzeAudio(file) | 异步解码 + 能量包络 + onset + 自相关求 BPM 与相位 |
J.rng(seed) | mulberry32 随机流,供可复现的 omakase 使用 |
只有三件事必须走 DOM:填歌词(走真实输入路径,让 JIZURA 自己的处理器刷新行列表)、
塞音频文件(文件不适合经 evaluate 传递)、点导出按钮(导出是 UI 驱动的下载)。
上游 JIZURA 的行为(决定怎么调参)
以下结论来自对 JIZURA v0.10.1 读源码 + 实测(一次 3 分 41 秒 FLAC / 1080p 的真实出片),直接影响调用方式。
导出长度 = 音频长度,不是歌词长度
J.computeTiming() 末段:提供了音频时,duration 取 max(音频时长, 最后一行结束 + 0.2)。
所以只写了 57 秒歌词、却给了 221 秒音轨时,导出就是 221 秒,后面全是没有 cut 的空白。
对策:歌词没铺满音轨时用 [間奏 N] 把空隙填掉,否则会出现几十秒空画面。
想知道导出多长,看音频时长而不是歌词行数。
| 注记(note)在多数行不会显示 —— 双语歌词要写成独立时间戳行
歌詞|注釈 是官方记法,但 11p_layoutsA.js:altCopy() 只在 cut.text === cut.lineText 时才用
note;而一行通常会被切成多个 cut,每个 cut 只拿到行内一段文本,于是 note 被跳过。
想让译文稳定出现,就得把译文写成独立行并给它显式 LRC 时间戳。
实测可用的写法(原文 / 译文按 ≈55% / 45% 分配同一句的时长):
[00:08.90]五月雨は/緑色
[00:13.00]绿色的/五月雨
[01:19.90][間奏 11.7]
顺带:J.detectLang() 里 kana >= 2 先于汉字判定,所以日文原文 + 中文译文混排仍判为 ja;
简体字会回退到系统字体渲染(实测无豆腐块,观感可接受)。
[間奏 N] 支持小数,也可与 LRC 时间戳同时使用
[01:19.90][間奏 11.7] 解析正常(先剥时间戳、再匹配间奏)。间奏 ≥ 6 秒时会顺带显示曲名 / 艺术家小字。
输出文件名 = project.title
12_ui.js:baseName() 用 title(非法字符替换为 _、截断 60 字),keyBg 不为 off 时追加
_greenback / _blackback。插件侧再过一层 uniquePath(),重名会追加 -1、-2。
渲染耗时最大的杠杆是 fx.koma,不是码率
fx.koma 是「每秒画几次」:0 = 每个输出帧都重绘(最慢),12 = 每秒 12 次(on twos)。
ballad 主题的候选表是 [0, 0, 12],抽到 0 的概率 2/3。同样 5309 帧,实测耗时在
335s ~ 454s 之间浮动,主要就来自 mood / 主题决定的 density 与 koma。
JIZURA 界面/接口维护说明
上游一旦改动,插件需要跟着改。所有 DOM 依赖集中在 src/impl.ts 的 SELECTORS,
所有内部 API 依赖集中在 configureProject() / applyDynamics() 两个 page.evaluate 里。
当前依赖的 DOM 选择器
| 用途 | 候选选择器(按优先级) | 来源 |
|---|---|---|
| 引导浮层(兜底) | #tour | <div id="tour" role="dialog" aria-modal="true"> |
| 引导层「スキップ」 | #tour .tour-skip → .tour-skip | 引导层导航里的跳过按钮 |
| 歌词输入 | #lyrics → textarea → [contenteditable="true"] → [aria-label*="歌詞"] → [aria-label*="歌词"] | <textarea id="lyrics"> |
| 音频文件 | #audioFile → input[type="file"][accept*="audio"] | <input id="audioFile" type="file"> |
| MP4 导出 | #eMP4 → #btnMP4 | <button id="eMP4">MP4 を書き出す</button> |
| PNG 序列导出 | #ePNG → #btnPNG | <button id="ePNG">連番PNG(ZIP)</button> |
| 「書き出し」标签页 | button[role="tab"][data-tab="out"] | 詳細模式的兜底路径 |
四个必须知道的坑
-
静态模板与实际运行时状态相反。
app/body.html里#easyPanel带hidden、#modePro带aria-pressed="true", 看起来默认是「詳細」模式;但实测页面加载完成后 JS 会把默认模式设为**「かんたん」** (#modeEasy[aria-pressed="true"]、#easyPanel不含hidden), 此时#outAspect/#btnMP4/#btnPNG与各标签页反而是隐藏的。 不要照抄body.html推断初始可见性。 本插件因此优先用#eMP4/#ePNG,只在找不到时才回退到詳細模式 + 切标签页。 -
首次访问有全屏引导浮层,会吞掉所有点击。
#tour是role="dialog" aria-modal="true"的遮罩,每次新建浏览器上下文都算首次访问, 所以每次调用都会遇到。不处理它,第一次点击就会失败并报... intercepts pointer events。 本插件用context.addInitScript()在页面脚本执行前写入localStorage['jizura.tourDone'] = '1'(12_ui.js启动段据此判断),从源头规避;点「スキップ」与addStyleTag只作为兜底。 -
风格必须在「おまかせ」之后应用。
おまかせ会重掷style、mood、fx、enabled(见src/08b_omakase.js)。 若先设样式再omakase,样式会被随机覆盖。因此configureProject()的顺序恒为 「输出设置 →omakase打底 → 显式 style/mood 覆盖 → intensity 插值 → replan」。 画幅 / 分辨率 / 帧率属于输出设置,不受おまかせ影响,可以先设。 -
overrides[line].cutQuiet是死数据,不要用它做按行控制。 它看起来正是「让这一行安静下来」的开关,但全仓库只有12_ui.js读它来渲染一个forced标记,08_planner.js与渲染流程从不读取,写进去不会有任何效果。 真正生效的按行控制是overrides[line].cuts(08_planner.js里const fixedN = ov.cuts > 0 ? …) 与overrides[line].single/seed/lock。段落对比因此基于cuts实现。
页面/接口变更后的修法
- 打开 https://852wa.github.io/JIZURA/,用开发者工具确认新元素或新 API。
- DOM 变了 → 更新
SELECTORS(尽量追加候选而非替换,保留兜底)。 - API 变了 → 改
configureProject()/applyDynamics(),注意J.ui.project的字段名。 - 若是样式包改名,同步更新
STYLE_PRESETS。当前#styleGrid渲染 27 张卡片: 基础 12 个(noircrimsoncautionmagentapaperhudmintspecimentransitblueprintrougemono) 加扩展 15 个(hrRuinhrNightRechrCursesakuraoceansunsetforestvapornewsprintsynth80kraftcandyacidsumigold)。 本插件用到的三个都在基础 12 个内,不受「追加分」开关影响。 - 跑
npm test(用例里的假 DOM 是手写的,不受真实页面影响,仍应全绿)。 - 用真实页面做一次端到端验证(见下)。
端到端自检
node --input-type=module -e "
import { generateJizuraPv } from './lib/impl.js';
const r = await generateJizuraPv({
lyrics: '夜明けの色を/覚えてる\n*透明*なままの街',
audioPath: 'D:/path/to/song.mp3',
stylePreset: 'dark', mood: 'emotional', intensity: 0.7, autoDynamics: true,
aspectRatio: '16:9', resolution: 720, fps: 24, outputFormat: 'mp4',
outputDir: './tmp-verify', keyBg: 'off', seed: 20261007,
});
console.log(r);
if (r.bpm === 0 && r.audioDuration === 0) throw new Error('踩点没有生效,检查音频格式');
"
开发
npm install # 安装开发依赖
npm run build # tsc -p tsconfig.json → lib/
npm test # vitest run tests
构建产物布局与 exports 严格对应:
lib/index.js ← main / exports["."].default
lib/types/index.d.ts ← types / exports["."].types
lib/impl.js
lib/types/impl.d.ts
测试分两层:
tests/register.spec.ts—— 屏蔽@deepseek-ai/dsh-tools,断言name、inject与工具定义形状(21 个参数、枚举与默认值、output.schema的 13 个全 required 字段、render的四种分支、timeoutMs)符合 dsh 契约。tests/impl.spec.ts—— 屏蔽playwright,用假 DOM 覆盖:引导浮层的addInitScript注入、 等待音频异步分析、预设展开为 style+mood+intensity、显式参数覆盖(含styleKey/paletteLock/hideNo/hideTime)、段落对比的开关与缩放系数、结构化返回、 取消信号清理、空歌词短路、音频超时、导航失败,以及路径辅助函数。
共 33 个用例。
实测记录
(一)无音频基线 —— Windows + Chrome Headless Shell 153:
SUCCESS (94.1s)
path: <outputDir>/jizura.mp4
size: 20423484 bytes # ftyp isom / isomavc1mp41(H.264)
(二)带音频 + 情绪/强度/段落对比 —— 用合成的 120 BPM 音频(12 秒):
SUCCESS (28.7s)
{
"bpm": 120, // 合成源是 120 BPM,检测完全准确
"beatCount": 25,
"audioDuration": 12,
"style": "noir", // stylePreset: dark → noir
"mood": "emotional",
"intensity": 0.7,
"aspect": "16:9", "resolution": 720, "fps": 24,
"bytes": 7111804,
"dynamicsApplied": true
}
即「载入音频 → 等 BPM 检测 → 注入样式/情绪/强度 → 按能量做段落对比 → 导出」全程无人工干预。
(三)画质与帧率的实际影响 —— 同一段素材、同一个 seed,只改 quality 与 fps:
| 配置 | 产物大小 | 编码耗时 |
|---|---|---|
720p / 24fps / standard | 4,523,934 B | 23.1s |
720p / 24fps / max | 10,094,872 B(2.23×) | 23.8s(1.03×) |
720p / 60fps / max | 19,931,162 B(1.97×) | 45.0s(1.89×) |
结论:quality 确实改变码率(max 约为 standard 的 2.2 倍,接近系数比 0.42/0.16),
且对编码耗时几乎无影响;fps 翻倍会让体积与耗时都接近翻倍。
quality并不在J.defaultProject()里,界面读的是S.project.quality || 'high'。 它之所以能用,是因为导出路径把它作为码率档位传给了J.videoBitrate()。上面这组实测就是 为了确认"写了真的生效",而不是只写进了一个没人读的字段。
(四)全长曲子的真实量级 —— 3 分 41 秒(音轨 221 秒)、720p / 24fps、无头软件编码:
| quality | 目标码率 | 实测码率 | 产物 |
|---|---|---|---|
standard | 3.54 Mbps | 3.65 Mbps | 100.8 MB |
high | 6.19 Mbps | 6.16 Mbps | 170.0 MB |
同一首曲子 5309 帧的墙钟耗时:335s ~ 454s(≈12~16 帧/秒),差异主要来自 mood / 主题决定的
density 与 koma。由此推算其他配置:
| 配置 | 目标码率 | 221 秒成品 |
|---|---|---|
1080p / 30fps / high | 17.4 Mbps | ≈ 480 MB |
1080p / 30fps / standard | 9.95 Mbps | ≈ 275 MB |
1080p / 60fps / high | 34.8 Mbps | ≈ 960 MB |
1080p / 60fps / max | 40 Mbps(被 cap) | ≈ 1.1 GB |
一部长 221 秒的歌在 1080p / 60fps 下是 13 260 帧、像素量再乘 2.25,按同一速度推算要 40 分钟以上(本次 60fps 跑到 30 分钟时中止改 30fps,所以这一行是推算值)。
这正是把导出等待预算改成按帧数动态估算的原因:固定 5 分钟连 720p / 24fps 的全长 (≈7 分钟)都盖不住。