自定义素材与配置

September 12, 2026 · View on GitHub

README.md 的补充材料。

放哪里

首次运行时插件会建好 %USERPROFILE%\.dsh\dsh-gal\,里面有 packs\(附一份格式速查)和 ui\, 把文件丢进去即可;用户目录优先,且插件升级不会被冲掉。

内容用户目录插件内置目录
立绘包 / 语音包%USERPROFILE%\.dsh\dsh-gal\packs\<插件目录>\assets\packs\
对话框图片%USERPROFILE%\.dsh\dsh-gal\ui\dialog.png<插件目录>\assets\ui\dialog.png
对话框几何%USERPROFILE%\.dsh\dsh-gal\ui\dialog.json<插件目录>\assets\ui\dialog.json
设定图标%USERPROFILE%\.dsh\dsh-gal\ui\settings.png<插件目录>\assets\ui\settings.png
点击音效%USERPROFILE%\.dsh\dsh-gal\ui\click.wav<插件目录>\assets\ui\click.wav
定价表%USERPROFILE%\.dsh\dsh-gal\pricing.json<插件目录>\assets\pricing.json

包是整体覆盖、不是合并:用户目录里出现叫 neri 的目录,哪怕只放 voices\,也会把内置的 neri 整个换掉。只想补语音就另起一个包名(如 neri-voice),再用设定面板把「立绘包 / 语音包」 分别选成两个包。改完刷新页面生效。

立绘包 / 语音包

<包根目录>\
└── mychar\                 ← 目录名就是包 id,设置面板里显示的就是它
    ├── pack.json           ← 可选:显示名 / 默认立绘 / 手动裁切
    ├── sprites\            ← 立绘:png jpg webp gif avif bmp
    │   ├── 01.png
    │   └── 02.png
    ├── voices\             ← 语音:wav mp3 ogg oga m4a aac flac
    │   ├── my0001.wav
    │   └── my0002.wav
    └── script.csv          ← 可选:语音 → 台词对照表
  • 只放 sprites\ → 立绘包;只放 voices\ → 语音包;两者都放就是一个包两用。
  • 两个文件夹也可以叫 立绘\ / 语音\,或 立ち絵\ / ボイス\ —— 中文、日文目录名都认, 不用为了插件改名(同名的英文目录优先)。台词表放包目录或语音目录里都能找到。
  • 也可以不建子目录,把图片和语音直接丢在包目录里。
  • 立绘的透明边距会自动裁掉,不需要自己切图。
  • voices\ 里有台词的行会被优先随机到。

立绘镜像

有的立绘是朝左画的,挂在右下角时想让它朝右,就整套左右镜像一遍。仓库里带了这个工具 (无损:解码 → 逐行翻转 → 重新编码,写完再逐像素回验,而且镜像两次就等于还原):

node scripts/mirror-sprites.mjs "D:\path\to\pack\立绘"          # 原地镜像
node scripts/mirror-sprites.mjs "D:\path\to\pack\立绘" --out "D:\path\to\pack\立绘-镜像"

只处理 8 位、非隔行的 PNG(灰度 / RGB / RGBA);调色板、16 位、隔行图会直接报错而不是悄悄改坏。 镜像后文件名不变,所以 pack.json 里的 defaultSprite 不用改。

语音顺序与权重

默认洗牌池:把整个语音包洗成一轮,逐条不放回地播;一轮放完自动重洗,新一轮不会拿上一轮 结尾那句开头。所以同一句台词在整轮放完之前不会再次出现——400 次点击里任何一句最多 1 次。 想改回"每次都独立随机",设定面板里把「语音顺序」切成 纯随机 即可(立刻生效)。

要让某几句多放 / 少放 / 不播,在包目录里放 weights.json:

{
  "mas0033": 3,
  "……。": 0.2,
  "mas0044": 0
}
写法含义
键 = 语音文件名(带不带 .wav 都行,如 mas0033)只调这一条
键 = 台词原文(完全匹配,如 ……。)调说这句话的所有语音
1(默认)正常频率
0.25四分之一频率
3三倍频率(一轮里出现 3 次,位置随机且不会连在一起)
0这一条永不播放
  • 只写要调的那几条就行,其余默认 1。
  • 不做任何自动合并:……。 和 …… 是两条不同的键;标点、空格、全半角都不归一化。
  • 文件缺失 / JSON 语法错误 / 键写错 → 那一项按 1 处理,不会报错、不会让某句消失。
  • 设定面板的「语音包」下面会显示 · 权重 n 条,方便确认文件生效了。

默认立绘怎么定

对话框消失后立绘切回默认立绘(常态姿势):

优先级怎么声明
1pack.json 里的 defaultSprite,例如 "defaultSprite": "01.png"
2没写 → 按自然顺序取第一张(2.png 排在 10.png 前)

设置面板的「立绘包」下面会写明当前用的是哪张、以及是 pack.json 指定 还是 自动选定。

替换包里的同名文件(例如把整套立绘换成镜像版)之后,刷新页面就能看到新图:立绘与语音的 URL 带文件版本号(修改时间 + 大小),换了内容就是换了地址,不会读到浏览器缓存里的旧素材。

script.csv 格式

clip,chapter,speaker_jp,japanese,chinese,sec
my0001,1,ねり,旅行、行こうよ!,去旅行嘛!,1.725
my0002,2,ねり,お兄ちゃんいまパンツ見たでしょ!,哥哥刚才是不是在看我的内裤!,2.114
  • clip 必须和 voices\ 里的文件名(不含扩展名)一致:my0001 ↔ my0001.wav。
  • japanese / chinese 两列分别对应设定里的「日文 / 中文」。
  • 字段可以用双引号包住,允许换行;列名认不出来时按 文件名,章节,说话人,日语,中文,时长 的位置兜底。
  • 台词里夹着的音效标记(如 えいっ<dash=2>、ねいっ。)会被自动去掉,不用自己清理; 1 < 2 这种正常的尖括号不受影响。
  • 没有 script.csv 也能用,只是点击立绘改为显示余额页。

pack.json(可选)

{
  "displayName": "我的角色",
  "defaultSprite": "01.png",
  "crop": { "x": 0.164, "y": 0.168, "w": 0.679, "h": 0.832 }
}
字段说明
displayName设置面板下拉里显示的名字,缺省用目录名
description包的说明文字,可省略
defaultSprite默认立绘文件名;写的文件不存在时回退到自动选定
crop手动指定裁切范围(0~1)。通常不用写:会自动扫描 PNG 透明通道;只有 JPG 或自动结果不满意时才需要

包 id 取目录名,对照表在包目录和 voices\ 里自动找 .csv(script 开头的优先)。

换掉对话框底图:dialog.json

首选在设定面板里换:「对话框」一组里的 更换图片… 选一张自己的图、空白底图 换成自带的 纯白面板、恢复默认 还原——三者都立即生效,插件会自动为新图写好几何。 手改文件也行(用户目录优先,改完刷新页面即可):

%USERPROFILE%\.dsh\dsh-gal\ui\dialog.png     ← 你的底图
%USERPROFILE%\.dsh\dsh-gal\ui\dialog.json    ← 描述它的几何

换成自己的对话框图片时,同目录放一个 dialog.json 描述它的几何:

{
  "image": { "width": 1280, "height": 720 },
  "crop":  { "x": 0, "y": 0.42, "w": 1, "h": 0.58 },
  "inset": { "left": 0.05, "right": 0.05, "top": 0.07, "bottom": 0.04 },
  "footer": { "heightRatio": 0.42, "maxWidthRatio": 0.66 },
  "radius": 10,
  "logoShare": 0.72
}
字段说明
crop显示原图的哪一块(0~1)。默认只取下方 58%,把上方空白裁掉
inset内容区相对裁切后画面的内边距
footer.heightRatio底栏(余额 / 花费)高度最多占文字区的比例,0.15–0.7
footer.maxWidthRatio底栏最多横向占多宽,0.3–1
radius圆角像素
logoShare底栏最多用掉文字区高度的几分之几。自带底图右下角有 logo,所以是 0.72;自己的底图没有 logo 就写 1,余额/花费页会用满整块

自带的空白底图是 assets\ui\dialog-blank.png + dialog-blank.json$(1200 \times 700、$crop 全图、 logoShare: 1),用 node scripts/make-blank-plate.mjs 重新生成。

定价表

pricing.json(用户目录优先)—— 改价不用改代码:

{
  "updatedAt": "2026-08-17",
  "peakHours": [[9, 12], [14, 18]],
  "weekendOffPeak": true,
  "models": {
    "deepseek-v4-flash": { "hit": [0.05, 0.1], "miss": [1.5, 3.0], "out": [4.5, 9.0] },
    "deepseek-v4-pro":   { "hit": [0.15, 0.3], "miss": [4.5, 9.0], "out": [13.5, 27.0] },
    "_default":          { "hit": [0.05, 0.1], "miss": [1.5, 3.0], "out": [4.5, 9.0] }
  }
}

每条是 [空闲时段价, 高峰时段价],单位人民币 / 百万 token。高峰为北京时间周一至周五 09:00–12:00 与 14:00–18:00,周末全天按空闲价;推理 token 按输出价计费。

配置文件

%USERPROFILE%\.dsh\.dsh-gal.json:

{
  "version": 3,
  "spritePack": "neri",
  "voicePack": "neri",
  "spriteFile": "large_neri_07face.png",
  "scale": 5,
  "volume": 0.5,
  "lang": "ja",
  "autoPlay": false,
  "autoPlayMinutes": 1,
  "dialogScale": 5,
  "dialogEnabled": true,
  "dialogSide": "above",
  "dialogOpacity": 0,
  "dialogHoldSeconds": 3,
  "spriteRevertOnHide": true,
  "spriteVisible": true,
  "pos": { "hx": "right", "hd": 24, "vy": "bottom", "vd": 24 },
  "extraPackRoots": []
}
键说明
scale立绘档位 1–10,默认 5
dialogScale对话框档位 1–10,默认 5(单位与立绘不同)
volume0–1 的小数
langja(日文)或 zh(中文)
voiceOrder语音抽取方式:shuffle(洗牌池,默认)或 random(纯随机)
autoPlay / autoPlayMinutes自动播放开关与间隔(分钟,0.5–240)
dialogEnabled对话框总开关
dialogSideabove(立绘上方)或 below
dialogOpacity百分比 0–95。0 = 原图完全不透明,越大越透明
dialogHoldSeconds语音播完后停留几秒再淡出,默认 3,范围 0.5–120
spriteRevertOnHide对话框消失后是否切回默认立绘,默认 true
spriteVisible设为 false 只隐藏立绘,保留对话框
pos位置锚点:hx 为 left/right、hd 是离该边的像素距离;vy 为 top/bottom、vd 同理
extraPackRoots额外的包目录数组,每个目录的直接子目录会被当成包
pricingFile自定义定价表的绝对路径

设定面板里改(缩放 / 位置 / 不透明度 / 选包 / 换底图)立即生效。 手改这个文件需要重启 DSH Desktop——配置是插件启动时读入一次,之后走内存; ui\ 下的素材与 packs\ 都是每次请求实时读的,刷新页面即可。