dsh-live2dwaifu 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -0
- package/assets/persona.html +149 -0
- package/assets/viewer.html +12 -0
- package/assets/viewer.js +132 -15
- package/lib/client.js +21 -4
- package/lib/index.js +153 -10
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -23,6 +23,12 @@
|
|
|
23
23
|
|
|
24
24
|
**会出声**:一个字一声很短的电音,标点停一拍,句尾沉一下——不是朗读,是“哔哔”那种。标题栏的 **♪** 打开调音页,放个真模型在旁边,边调边听;每只模型各存一份音色,换人换声。音量拉到 0 就安静,只剩气泡和口型。
|
|
25
25
|
|
|
26
|
+
**她也会甩图**:图库直接吃 `dsh-meme` 那套,说话时按情绪挑一张,贴在气泡里。
|
|
27
|
+
|
|
28
|
+
**她还会问你要选项**:话说到了需要你挑的地方,气泡下面会冒出两三个按钮,点一个就等于你回了她。
|
|
29
|
+
|
|
30
|
+
**开关都在 ⚙ 里**:抽屉底下分两栏——「这台机器上」管眼睛跟随、点她、自己播动作、出声、跟着聊天念;「她那边」管没人理就自己开口、甩表情包、给选项、记性。前几项存本机,后几项存 `~/.dsh/dsh-live2d/config.json`。
|
|
31
|
+
|
|
26
32
|
**跟着聊**:她挂在聊天流上(`conversation.chat.node` 插槽),助手每说一句她就念一句,情绪按话里的词换。
|
|
27
33
|
|
|
28
34
|
**也能直接跟她说**:卡片底下有个小框,打一句回车,她回你。这条线**不进你的聊天记录**——后台直接问模型,往来只存在 `models/<id>/chat.json` 里。她说的话按人设卡现场写,不是复读;人设卡就是 `models/<id>/meta.json` 里的 `persona` 字段,没写就用默认那份(若妍)。
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="zh">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<title>人设卡 — dsh-live2dwaifu</title>
|
|
6
|
+
<style>
|
|
7
|
+
:root { --bg:#12141a; --card:#171a20; --line:#2c313a; --txt:#dfe4ec; --dim:#7d858f; --acc:#6aa9ff; }
|
|
8
|
+
* { box-sizing: border-box; }
|
|
9
|
+
body { margin:0; min-height:100vh; background:radial-gradient(1100px 560px at 50% -10%, #1b1f27, var(--bg));
|
|
10
|
+
color:var(--txt); font:13px/1.6 "Microsoft YaHei", system-ui, sans-serif;
|
|
11
|
+
display:flex; align-items:flex-start; justify-content:center; gap:26px; padding:26px; flex-wrap:wrap; }
|
|
12
|
+
.left { display:flex; flex-direction:column; gap:10px; width:330px; }
|
|
13
|
+
.card { width:330px; height:440px; background:var(--card); border:1px solid var(--line); border-radius:14px;
|
|
14
|
+
overflow:hidden; box-shadow:0 18px 50px rgba(0,0,0,.45); }
|
|
15
|
+
.card iframe { display:block; width:100%; height:100%; border:0; }
|
|
16
|
+
.panel { width:460px; }
|
|
17
|
+
.panel h1 { font-size:15px; margin:0 0 3px; font-weight:600; }
|
|
18
|
+
.panel p.sub { margin:0 0 14px; color:var(--dim); font-size:12px; }
|
|
19
|
+
.group { border:1px solid var(--line); border-radius:12px; padding:12px; margin-bottom:12px; background:#15181e; }
|
|
20
|
+
.group h2 { font-size:11px; letter-spacing:.6px; color:var(--dim); margin:0 0 10px; font-weight:600; }
|
|
21
|
+
select { width:100%; padding:8px 10px; border-radius:9px; border:1px solid #3c424c; background:#181c22;
|
|
22
|
+
color:var(--txt); font:inherit; font-size:13px; }
|
|
23
|
+
textarea { width:100%; height:300px; resize:vertical; padding:10px 12px; border-radius:10px;
|
|
24
|
+
border:1px solid #3c424c; background:#181c22; color:var(--txt);
|
|
25
|
+
font:12px/1.75 "Microsoft YaHei", system-ui, sans-serif; outline:none; }
|
|
26
|
+
textarea:focus { border-color:#3f4a5c; }
|
|
27
|
+
.switch { display:flex; align-items:flex-start; gap:10px; padding:4px 0; cursor:pointer; }
|
|
28
|
+
.switch input { margin-top:3px; accent-color:var(--acc); }
|
|
29
|
+
.switch b { font-weight:600; font-size:12.5px; }
|
|
30
|
+
.switch em { display:block; font-style:normal; color:var(--dim); font-size:11.5px; line-height:1.6; }
|
|
31
|
+
.foot { display:flex; align-items:center; gap:10px; }
|
|
32
|
+
button.act { padding:9px 16px; border-radius:9px; border:1px solid #3c424c; background:#232830;
|
|
33
|
+
color:#dfe4ec; cursor:pointer; font:inherit; font-size:12px; }
|
|
34
|
+
button.act.primary { background:#27405f; border-color:#3e6b9e; color:#eaf2ff; }
|
|
35
|
+
button.act:hover { filter:brightness(1.12); }
|
|
36
|
+
.state { color:var(--dim); font-size:11px; margin-left:auto; }
|
|
37
|
+
.tip { color:#6f7783; font-size:11px; margin-top:10px; line-height:1.7; }
|
|
38
|
+
</style>
|
|
39
|
+
</head>
|
|
40
|
+
<body>
|
|
41
|
+
|
|
42
|
+
<div class="left">
|
|
43
|
+
<div class="card"><iframe id="live" src="/dsh-live2d/viewer?embed=1"></iframe></div>
|
|
44
|
+
<div class="tip">左边就是她本人。改完存一下,她下一句就按新的人设说。</div>
|
|
45
|
+
</div>
|
|
46
|
+
|
|
47
|
+
<div class="panel">
|
|
48
|
+
<h1>人设卡</h1>
|
|
49
|
+
<p class="sub">她是谁、怎么说话,全在这段字里。</p>
|
|
50
|
+
|
|
51
|
+
<div class="group">
|
|
52
|
+
<h2>配给哪个模型</h2>
|
|
53
|
+
<select id="models"></select>
|
|
54
|
+
</div>
|
|
55
|
+
|
|
56
|
+
<div class="group">
|
|
57
|
+
<h2>人设</h2>
|
|
58
|
+
<textarea id="persona" spellcheck="false"></textarea>
|
|
59
|
+
</div>
|
|
60
|
+
|
|
61
|
+
<div class="group">
|
|
62
|
+
<h2>世界隔离</h2>
|
|
63
|
+
<label class="switch">
|
|
64
|
+
<input type="checkbox" id="isolation" checked />
|
|
65
|
+
<span>
|
|
66
|
+
<b>让她待在自己的世界里</b>
|
|
67
|
+
<em>打开后,她不知道什么是程序、界面、插件、模型。问起来就当你在她身边,不提“系统”“设置”“网页”。关掉她就会老老实实承认自己是插件里的角色。</em>
|
|
68
|
+
</span>
|
|
69
|
+
</label>
|
|
70
|
+
</div>
|
|
71
|
+
|
|
72
|
+
<div class="foot">
|
|
73
|
+
<button class="act primary" id="save">存</button>
|
|
74
|
+
<button class="act" id="reload">撤销改动</button>
|
|
75
|
+
<span class="state" id="state"></span>
|
|
76
|
+
</div>
|
|
77
|
+
<div class="tip">存在 <code>~/.dsh/dsh-live2d/models/<id>/meta.json</code> 的 <code>persona</code> 字段里,想手改也行。</div>
|
|
78
|
+
</div>
|
|
79
|
+
|
|
80
|
+
<script>
|
|
81
|
+
(function () {
|
|
82
|
+
var frame = document.getElementById('live');
|
|
83
|
+
var sel = document.getElementById('models');
|
|
84
|
+
var area = document.getElementById('persona');
|
|
85
|
+
var iso = document.getElementById('isolation');
|
|
86
|
+
var state = document.getElementById('state');
|
|
87
|
+
var modelId = '';
|
|
88
|
+
|
|
89
|
+
function $(id) { return document.getElementById(id); }
|
|
90
|
+
|
|
91
|
+
function load() {
|
|
92
|
+
if (!modelId) return;
|
|
93
|
+
state.textContent = '读…';
|
|
94
|
+
fetch('/dsh-live2d/api/persona?id=' + encodeURIComponent(modelId))
|
|
95
|
+
.then(function (r) { return r.json(); })
|
|
96
|
+
.then(function (d) {
|
|
97
|
+
area.value = d.persona || '';
|
|
98
|
+
iso.checked = d.isolation !== false;
|
|
99
|
+
state.textContent = d.custom ? '这份是手写的' : '还是默认那份';
|
|
100
|
+
})
|
|
101
|
+
.catch(function (e) { state.textContent = '读不了:' + e.message; });
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function save() {
|
|
105
|
+
if (!modelId) return;
|
|
106
|
+
state.textContent = '存…';
|
|
107
|
+
fetch('/dsh-live2d/api/persona', {
|
|
108
|
+
method: 'POST',
|
|
109
|
+
headers: { 'content-type': 'application/json' },
|
|
110
|
+
body: JSON.stringify({ id: modelId, persona: area.value, isolation: iso.checked })
|
|
111
|
+
})
|
|
112
|
+
.then(function (r) { return r.json(); })
|
|
113
|
+
.then(function (d) {
|
|
114
|
+
state.textContent = d.ok ? '存好了' : '没存上:' + (d.error || '?');
|
|
115
|
+
if (d.ok) {
|
|
116
|
+
frame.src = '/dsh-live2d/viewer?embed=1&t=' + Date.now();
|
|
117
|
+
}
|
|
118
|
+
})
|
|
119
|
+
.catch(function (e) { state.textContent = '没存上:' + e.message; });
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
$('save').addEventListener('click', save);
|
|
123
|
+
$('reload').addEventListener('click', load);
|
|
124
|
+
sel.addEventListener('change', function () {
|
|
125
|
+
modelId = sel.value;
|
|
126
|
+
load();
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
fetch('/dsh-live2d/api/models')
|
|
130
|
+
.then(function (r) { return r.json(); })
|
|
131
|
+
.then(function (d) {
|
|
132
|
+
var list = d.models || [];
|
|
133
|
+
sel.innerHTML = '';
|
|
134
|
+
list.forEach(function (m) {
|
|
135
|
+
var o = document.createElement('option');
|
|
136
|
+
o.value = m.id;
|
|
137
|
+
o.textContent = (m.name || m.id) + (m.id === d.selected ? '(正在显示)' : '');
|
|
138
|
+
sel.appendChild(o);
|
|
139
|
+
});
|
|
140
|
+
modelId = d.selected || (list[0] && list[0].id) || '';
|
|
141
|
+
if (modelId) sel.value = modelId;
|
|
142
|
+
if (!list.length) { state.textContent = '还没有模型'; return; }
|
|
143
|
+
load();
|
|
144
|
+
})
|
|
145
|
+
.catch(function (e) { state.textContent = '读不了模型表:' + e.message; });
|
|
146
|
+
})();
|
|
147
|
+
</script>
|
|
148
|
+
</body>
|
|
149
|
+
</html>
|
package/assets/viewer.html
CHANGED
|
@@ -33,6 +33,16 @@
|
|
|
33
33
|
border-radius: 6px; padding: 3px 8px; cursor: pointer; font: inherit; }
|
|
34
34
|
#exps button.on { background: #3a5f8a; border-color: #4c7bb0; color: #eaf2ff; }
|
|
35
35
|
#exps .tipx { color: #6f7783; font-size: 11px; align-self: center; }
|
|
36
|
+
#choices { display: none; position: absolute; left: 12px; right: 12px; bottom: 96px; flex-wrap: wrap; gap: 6px; }
|
|
37
|
+
#choices.show { display: flex; }
|
|
38
|
+
#choices button { background: rgba(38,48,62,.96); border: 1px solid #4a6a92; border-radius: 14px;
|
|
39
|
+
color: #dfe9f6; font: inherit; font-size: 12px; padding: 5px 11px; cursor: pointer; }
|
|
40
|
+
#choices button:hover { background: #2f4a68; }
|
|
41
|
+
#settings { max-height: 42%; overflow-y: auto; margin-bottom: 8px; }
|
|
42
|
+
#settings h3 { font-size: 10px; letter-spacing: .6px; color: #6f7783; font-weight: 600; margin: 8px 0 4px; }
|
|
43
|
+
.opt { display: flex; align-items: center; gap: 8px; padding: 3px 2px; font-size: 12px; color: #c3c9d3; }
|
|
44
|
+
.opt input { accent-color: #6aa9ff; }
|
|
45
|
+
.bub-img { display: block; max-width: 100%; max-height: 120px; border-radius: 8px; margin-bottom: 6px; object-fit: contain; }
|
|
36
46
|
#actions { display: flex; gap: 6px; }
|
|
37
47
|
#actions button { flex: 1; background: #2f343d; color: #cfd4dd; border: 1px solid #3c424c;
|
|
38
48
|
border-radius: 8px; padding: 6px; cursor: pointer; font: inherit; }
|
|
@@ -43,11 +53,13 @@
|
|
|
43
53
|
<body>
|
|
44
54
|
<canvas id="stage"></canvas>
|
|
45
55
|
<div id="bubble"></div>
|
|
56
|
+
<div id="choices"></div>
|
|
46
57
|
<div id="hint"><div>还没有模型。<br />点下面的「导入模型」,选中你的 Live2D 文件夹(含 .model3.json 那一层)。</div></div>
|
|
47
58
|
<div id="drawer">
|
|
48
59
|
<h2>模型</h2>
|
|
49
60
|
<div id="list"></div>
|
|
50
61
|
<div id="exps"></div>
|
|
62
|
+
<div id="settings"></div>
|
|
51
63
|
<div id="actions">
|
|
52
64
|
<button id="import">导入模型</button>
|
|
53
65
|
<button id="close">关掉</button>
|
package/assets/viewer.js
CHANGED
|
@@ -700,8 +700,8 @@
|
|
|
700
700
|
}
|
|
701
701
|
return base2;
|
|
702
702
|
}
|
|
703
|
-
function digitToBasic(digit,
|
|
704
|
-
return digit + 22 + 75 * (digit < 26) - ((
|
|
703
|
+
function digitToBasic(digit, flag2) {
|
|
704
|
+
return digit + 22 + 75 * (digit < 26) - ((flag2 != 0) << 5);
|
|
705
705
|
}
|
|
706
706
|
function adapt(delta, numPoints, firstTime) {
|
|
707
707
|
var k2 = 0;
|
|
@@ -9999,16 +9999,16 @@ void main(void)
|
|
|
9999
9999
|
if (!this.bitmap && (this.process(), !this.bitmap))
|
|
10000
10000
|
return false;
|
|
10001
10001
|
if (super.upload(renderer, baseTexture, glTexture, this.bitmap), !this.preserveBitmap) {
|
|
10002
|
-
let
|
|
10002
|
+
let flag2 = true;
|
|
10003
10003
|
const glTextures = baseTexture._glTextures;
|
|
10004
10004
|
for (const key in glTextures) {
|
|
10005
10005
|
const otherTex = glTextures[key];
|
|
10006
10006
|
if (otherTex !== glTexture && otherTex.dirtyId !== baseTexture.dirtyId) {
|
|
10007
|
-
|
|
10007
|
+
flag2 = false;
|
|
10008
10008
|
break;
|
|
10009
10009
|
}
|
|
10010
10010
|
}
|
|
10011
|
-
|
|
10011
|
+
flag2 && (this.bitmap.close && this.bitmap.close(), this.bitmap = null);
|
|
10012
10012
|
}
|
|
10013
10013
|
return true;
|
|
10014
10014
|
}
|
|
@@ -31603,16 +31603,16 @@ void main(void)\r
|
|
|
31603
31603
|
}
|
|
31604
31604
|
_super.prototype.upload.call(this, renderer, baseTexture, glTexture, this.bitmap);
|
|
31605
31605
|
if (!this.preserveBitmap) {
|
|
31606
|
-
var
|
|
31606
|
+
var flag2 = true;
|
|
31607
31607
|
var glTextures = baseTexture._glTextures;
|
|
31608
31608
|
for (var key in glTextures) {
|
|
31609
31609
|
var otherTex = glTextures[key];
|
|
31610
31610
|
if (otherTex !== glTexture && otherTex.dirtyId !== baseTexture.dirtyId) {
|
|
31611
|
-
|
|
31611
|
+
flag2 = false;
|
|
31612
31612
|
break;
|
|
31613
31613
|
}
|
|
31614
31614
|
}
|
|
31615
|
-
if (
|
|
31615
|
+
if (flag2) {
|
|
31616
31616
|
if (this.bitmap.close) {
|
|
31617
31617
|
this.bitmap.close();
|
|
31618
31618
|
}
|
|
@@ -44321,6 +44321,110 @@ void main(void)\r
|
|
|
44321
44321
|
var POKE_HEAD = ["\u5514\u3002", "\u522B\u6478\u3002", "\u5934\u53D1\u4E71\u4E86\u3002", "\u55EF\u2026\u2026", "\u4F60\u624B\u51C9\u3002"];
|
|
44322
44322
|
var POKE_BODY = ["\u770B\u4EC0\u4E48\u770B\u3002", "\u6233\u4EC0\u4E48\u3002", "\u54FC\u3002", "\u522B\u95F9\u3002"];
|
|
44323
44323
|
var POKE_LOW = ["\u5728\u5462\u3002", "\u6709\u4E8B\uFF1F", "\u600E\u4E48\u4E86\u3002"];
|
|
44324
|
+
var flags = {};
|
|
44325
|
+
try {
|
|
44326
|
+
flags = JSON.parse(localStorage.getItem("dsh-l2d-flags") || "{}");
|
|
44327
|
+
} catch {
|
|
44328
|
+
flags = {};
|
|
44329
|
+
}
|
|
44330
|
+
function flag(k2) {
|
|
44331
|
+
return flags[k2] === void 0 ? true : !!flags[k2];
|
|
44332
|
+
}
|
|
44333
|
+
function setFlag(k2, v2) {
|
|
44334
|
+
flags[k2] = !!v2;
|
|
44335
|
+
try {
|
|
44336
|
+
localStorage.setItem("dsh-l2d-flags", JSON.stringify(flags));
|
|
44337
|
+
} catch {
|
|
44338
|
+
}
|
|
44339
|
+
}
|
|
44340
|
+
var LOCAL_OPTS = [
|
|
44341
|
+
["gaze", "\u773C\u775B\u8DDF\u7740\u9F20\u6807"],
|
|
44342
|
+
["poke", "\u70B9\u5979\u6709\u53CD\u5E94"],
|
|
44343
|
+
["motion", "\u81EA\u5DF1\u64AD\u52A8\u4F5C"],
|
|
44344
|
+
["sound", "\u51FA\u58F0"],
|
|
44345
|
+
["follow", "\u8DDF\u7740\u804A\u5929\u5FF5"]
|
|
44346
|
+
];
|
|
44347
|
+
var SERVER_OPTS = [
|
|
44348
|
+
["proactive", "\u6CA1\u4EBA\u7406\u5C31\u81EA\u5DF1\u5F00\u53E3"],
|
|
44349
|
+
["memes", "\u4F1A\u7529\u8868\u60C5\u5305"],
|
|
44350
|
+
["choices", "\u7ED9\u9009\u9879\u8BA9\u4ED6\u6311"],
|
|
44351
|
+
["memory", "\u8BB0\u4F4F\u4E4B\u524D\u8BF4\u7684"]
|
|
44352
|
+
];
|
|
44353
|
+
var serverSettings = {};
|
|
44354
|
+
function renderSettings() {
|
|
44355
|
+
const box = $2("settings");
|
|
44356
|
+
if (!box) return;
|
|
44357
|
+
box.innerHTML = "";
|
|
44358
|
+
const h1 = document.createElement("h3");
|
|
44359
|
+
h1.textContent = "\u8FD9\u53F0\u673A\u5668\u4E0A";
|
|
44360
|
+
box.appendChild(h1);
|
|
44361
|
+
for (const [k2, label] of LOCAL_OPTS) {
|
|
44362
|
+
const row = document.createElement("label");
|
|
44363
|
+
row.className = "opt";
|
|
44364
|
+
const cb = document.createElement("input");
|
|
44365
|
+
cb.type = "checkbox";
|
|
44366
|
+
cb.checked = flag(k2);
|
|
44367
|
+
cb.onchange = () => setFlag(k2, cb.checked);
|
|
44368
|
+
row.appendChild(cb);
|
|
44369
|
+
row.appendChild(document.createTextNode(label));
|
|
44370
|
+
box.appendChild(row);
|
|
44371
|
+
}
|
|
44372
|
+
const h2 = document.createElement("h3");
|
|
44373
|
+
h2.textContent = "\u5979\u90A3\u8FB9";
|
|
44374
|
+
box.appendChild(h2);
|
|
44375
|
+
for (const [k2, label] of SERVER_OPTS) {
|
|
44376
|
+
const row = document.createElement("label");
|
|
44377
|
+
row.className = "opt";
|
|
44378
|
+
const cb = document.createElement("input");
|
|
44379
|
+
cb.type = "checkbox";
|
|
44380
|
+
cb.checked = serverSettings[k2] !== false;
|
|
44381
|
+
cb.onchange = () => {
|
|
44382
|
+
serverSettings[k2] = cb.checked;
|
|
44383
|
+
fetch(BASE + "/api/settings", {
|
|
44384
|
+
method: "POST",
|
|
44385
|
+
headers: { "content-type": "application/json" },
|
|
44386
|
+
body: JSON.stringify({ settings: { [k2]: cb.checked } })
|
|
44387
|
+
}).catch(() => {
|
|
44388
|
+
});
|
|
44389
|
+
};
|
|
44390
|
+
row.appendChild(cb);
|
|
44391
|
+
row.appendChild(document.createTextNode(label));
|
|
44392
|
+
box.appendChild(row);
|
|
44393
|
+
}
|
|
44394
|
+
}
|
|
44395
|
+
function loadSettings() {
|
|
44396
|
+
fetch(BASE + "/api/settings").then((r2) => r2.json()).then((d2) => {
|
|
44397
|
+
serverSettings = d2.settings || {};
|
|
44398
|
+
renderSettings();
|
|
44399
|
+
}).catch(() => renderSettings());
|
|
44400
|
+
}
|
|
44401
|
+
function showChoices(list) {
|
|
44402
|
+
const box = $2("choices");
|
|
44403
|
+
if (!box) return;
|
|
44404
|
+
box.innerHTML = "";
|
|
44405
|
+
if (!list || !list.length) {
|
|
44406
|
+
box.classList.remove("show");
|
|
44407
|
+
return;
|
|
44408
|
+
}
|
|
44409
|
+
for (const c2 of list) {
|
|
44410
|
+
const b2 = document.createElement("button");
|
|
44411
|
+
b2.textContent = c2;
|
|
44412
|
+
b2.onclick = () => {
|
|
44413
|
+
box.classList.remove("show");
|
|
44414
|
+
fetch(BASE + "/api/chat", {
|
|
44415
|
+
method: "POST",
|
|
44416
|
+
headers: { "content-type": "application/json" },
|
|
44417
|
+
body: JSON.stringify({ text: c2 })
|
|
44418
|
+
}).then((r2) => r2.json()).then((d2) => {
|
|
44419
|
+
if (d2.reply || d2.image) say(d2.reply || "", d2.mood, d2.image);
|
|
44420
|
+
showChoices(d2.choices || []);
|
|
44421
|
+
}).catch(() => {
|
|
44422
|
+
});
|
|
44423
|
+
};
|
|
44424
|
+
box.appendChild(b2);
|
|
44425
|
+
}
|
|
44426
|
+
box.classList.add("show");
|
|
44427
|
+
}
|
|
44324
44428
|
var tone = { type: "triangle", base: 400, jit: 10, dur: 60, vol: 32, spd: 62 };
|
|
44325
44429
|
var ac = null;
|
|
44326
44430
|
function audioCtx() {
|
|
@@ -44337,7 +44441,7 @@ void main(void)\r
|
|
|
44337
44441
|
}
|
|
44338
44442
|
function blip(freq, durMs, type) {
|
|
44339
44443
|
const a2 = audioCtx();
|
|
44340
|
-
if (!a2 || !(tone.vol > 0)) return;
|
|
44444
|
+
if (!a2 || !flag("sound") || !(tone.vol > 0)) return;
|
|
44341
44445
|
const t0 = a2.currentTime;
|
|
44342
44446
|
const osc = a2.createOscillator();
|
|
44343
44447
|
const g2 = a2.createGain();
|
|
@@ -44485,7 +44589,7 @@ void main(void)\r
|
|
|
44485
44589
|
await refresh(true);
|
|
44486
44590
|
$2("drawer").classList.remove("show");
|
|
44487
44591
|
}
|
|
44488
|
-
function say(text, m2) {
|
|
44592
|
+
function say(text, m2, image) {
|
|
44489
44593
|
if (m2) applyMood(m2);
|
|
44490
44594
|
const bubble = $2("bubble");
|
|
44491
44595
|
let s2 = String(text == null ? "" : text).replace(/\s+/g, " ").trim();
|
|
@@ -44493,6 +44597,14 @@ void main(void)\r
|
|
|
44493
44597
|
bubble.textContent = "";
|
|
44494
44598
|
bubble.style.opacity = "1";
|
|
44495
44599
|
clearTimeout(sayFade);
|
|
44600
|
+
if (image) {
|
|
44601
|
+
const img = document.createElement("img");
|
|
44602
|
+
img.className = "bub-img";
|
|
44603
|
+
img.src = String(image);
|
|
44604
|
+
bubble.appendChild(img);
|
|
44605
|
+
}
|
|
44606
|
+
const holder = document.createElement("div");
|
|
44607
|
+
bubble.appendChild(holder);
|
|
44496
44608
|
const chars = Array.from(s2);
|
|
44497
44609
|
let i2 = 0;
|
|
44498
44610
|
const step = Math.max(20, Number(tone.spd) || 62);
|
|
@@ -44506,7 +44618,7 @@ void main(void)\r
|
|
|
44506
44618
|
return;
|
|
44507
44619
|
}
|
|
44508
44620
|
const ch = chars[i2++];
|
|
44509
|
-
|
|
44621
|
+
holder.textContent += ch;
|
|
44510
44622
|
mouth = 0.14;
|
|
44511
44623
|
if (ch.trim() && PAUSE_CH.indexOf(ch) < 0) {
|
|
44512
44624
|
const f2 = tone.base * (1 + (Math.random() - 0.5) * 2 * (Number(tone.jit) / 100));
|
|
@@ -44522,7 +44634,10 @@ void main(void)\r
|
|
|
44522
44634
|
window.addEventListener("message", (e2) => {
|
|
44523
44635
|
const d2 = e2.data || {};
|
|
44524
44636
|
if (d2.type === "dsh-live2d/manage") $2("drawer").classList.toggle("show");
|
|
44525
|
-
if (d2.type === "dsh-live2d/say")
|
|
44637
|
+
if (d2.type === "dsh-live2d/say") {
|
|
44638
|
+
say(String(d2.text || ""), d2.mood, d2.image);
|
|
44639
|
+
if (Array.isArray(d2.choices)) showChoices(d2.choices);
|
|
44640
|
+
}
|
|
44526
44641
|
if (d2.type === "dsh-live2d/mood") applyMood(String(d2.mood || "\u5E73\u9759"));
|
|
44527
44642
|
if (d2.type === "dsh-live2d/tone" && d2.voice && typeof d2.voice === "object") Object.assign(tone, d2.voice);
|
|
44528
44643
|
});
|
|
@@ -44550,8 +44665,8 @@ void main(void)\r
|
|
|
44550
44665
|
}
|
|
44551
44666
|
}
|
|
44552
44667
|
setP("ParamBreath", (Math.sin(t2 * 1.1) + 1) / 2);
|
|
44553
|
-
const gx = Math.max(-1, Math.min(1, gazeX));
|
|
44554
|
-
const gy = Math.max(-1, Math.min(1, gazeY));
|
|
44668
|
+
const gx = flag("gaze") ? Math.max(-1, Math.min(1, gazeX)) : 0;
|
|
44669
|
+
const gy = flag("gaze") ? Math.max(-1, Math.min(1, gazeY)) : 0;
|
|
44555
44670
|
setP("ParamEyeBallX", gx * 0.9);
|
|
44556
44671
|
setP("ParamEyeBallY", -gy * 0.65);
|
|
44557
44672
|
if (!window.__holdHead) {
|
|
@@ -44567,7 +44682,7 @@ void main(void)\r
|
|
|
44567
44682
|
core.update();
|
|
44568
44683
|
});
|
|
44569
44684
|
function maybeMotion() {
|
|
44570
|
-
if (!model || !motionGroups.length) return;
|
|
44685
|
+
if (!model || !motionGroups.length || !flag("motion")) return;
|
|
44571
44686
|
const now = performance.now();
|
|
44572
44687
|
if (now < nextMotionAt) return;
|
|
44573
44688
|
nextMotionAt = now + 12e3 + Math.random() * 16e3;
|
|
@@ -44578,6 +44693,7 @@ void main(void)\r
|
|
|
44578
44693
|
}
|
|
44579
44694
|
}
|
|
44580
44695
|
window.addEventListener("resize", () => setTimeout(fit, 60));
|
|
44696
|
+
loadSettings();
|
|
44581
44697
|
refresh(true).then(() => {
|
|
44582
44698
|
if (state.models.length) setTimeout(idleTick, 2500);
|
|
44583
44699
|
setInterval(() => {
|
|
@@ -44658,6 +44774,7 @@ void main(void)\r
|
|
|
44658
44774
|
$2("stage").addEventListener("pointerdown", (e2) => {
|
|
44659
44775
|
if (!core) return;
|
|
44660
44776
|
audioCtx();
|
|
44777
|
+
if (!flag("poke")) return;
|
|
44661
44778
|
const now = Date.now();
|
|
44662
44779
|
if (now - pokeAt < 800) return;
|
|
44663
44780
|
pokeAt = now;
|
package/lib/client.js
CHANGED
|
@@ -99,7 +99,13 @@
|
|
|
99
99
|
if (!frameWin) break;
|
|
100
100
|
try {
|
|
101
101
|
frameWin.postMessage(
|
|
102
|
-
{
|
|
102
|
+
{
|
|
103
|
+
type: 'dsh-live2d/say',
|
|
104
|
+
text: items[i].text || '',
|
|
105
|
+
mood: items[i].mood,
|
|
106
|
+
image: items[i].image,
|
|
107
|
+
choices: items[i].choices,
|
|
108
|
+
},
|
|
103
109
|
'*',
|
|
104
110
|
);
|
|
105
111
|
} catch {}
|
|
@@ -171,6 +177,7 @@
|
|
|
171
177
|
|
|
172
178
|
const manage = () => ref.current && ref.current.contentWindow.postMessage({ type: 'dsh-live2d/manage' }, '*');
|
|
173
179
|
const openTone = () => window.open(BASE + '/tone', '_blank', 'width=1140,height=780');
|
|
180
|
+
const openPersona = () => window.open(BASE + '/persona', '_blank', 'width=1080,height=740');
|
|
174
181
|
|
|
175
182
|
const send = () => {
|
|
176
183
|
const t = draft.trim();
|
|
@@ -184,9 +191,12 @@
|
|
|
184
191
|
})
|
|
185
192
|
.then((r) => r.json())
|
|
186
193
|
.then((d) => {
|
|
187
|
-
if (d.reply && frameWin) {
|
|
194
|
+
if ((d.reply || d.image) && frameWin) {
|
|
188
195
|
try {
|
|
189
|
-
frameWin.postMessage(
|
|
196
|
+
frameWin.postMessage(
|
|
197
|
+
{ type: 'dsh-live2d/say', text: d.reply || '', image: d.image, choices: d.choices },
|
|
198
|
+
'*',
|
|
199
|
+
);
|
|
190
200
|
} catch {}
|
|
191
201
|
}
|
|
192
202
|
})
|
|
@@ -213,6 +223,7 @@
|
|
|
213
223
|
open && h('button', { title: '模型管理', onPointerDown: noDrag, onClick: manage }, '⚙'),
|
|
214
224
|
open && h('button', { title: '背景:' + BG_LABEL[bg], onPointerDown: noDrag, onClick: cycleBg }, '◐'),
|
|
215
225
|
open && h('button', { title: '音色', onPointerDown: noDrag, onClick: openTone }, '♪'),
|
|
226
|
+
open && h('button', { title: '人设卡', onPointerDown: noDrag, onClick: openPersona }, '✎'),
|
|
216
227
|
open && h('button', { title: '刷新', onPointerDown: noDrag, onClick: () => setNonce((n) => n + 1) }, '↻'),
|
|
217
228
|
h('button', { title: open ? '收起' : '展开', onPointerDown: noDrag, onClick: () => setOpen(!open) }, open ? '⌄' : '⌃'),
|
|
218
229
|
),
|
|
@@ -291,7 +302,13 @@
|
|
|
291
302
|
(props && props.node && (props.node.role || props.node.kind)) ||
|
|
292
303
|
(props && props.message && props.message.role) ||
|
|
293
304
|
'';
|
|
294
|
-
|
|
305
|
+
let followOn = true;
|
|
306
|
+
try {
|
|
307
|
+
followOn = JSON.parse(localStorage.getItem('dsh-l2d-flags') || '{}').follow !== false;
|
|
308
|
+
} catch {
|
|
309
|
+
followOn = true;
|
|
310
|
+
}
|
|
311
|
+
const skip = !followOn || /user|human|prompt/i.test(String(role));
|
|
295
312
|
react.useEffect(() => {
|
|
296
313
|
if (skip) return;
|
|
297
314
|
const el = anchor.current;
|
package/lib/index.js
CHANGED
|
@@ -9,6 +9,86 @@ const ROOT = join(homedir(), '.dsh', 'dsh-live2d');
|
|
|
9
9
|
const MODELS = join(ROOT, 'models');
|
|
10
10
|
const CONFIG = join(ROOT, 'config.json');
|
|
11
11
|
const PREFIX = '/dsh-live2d';
|
|
12
|
+
const MEMES_ROOT = join(homedir(), '.dsh', 'profiles', 'desktop', 'node_modules', 'dsh-meme', 'memes');
|
|
13
|
+
|
|
14
|
+
/** 情绪 → 图库分类 */
|
|
15
|
+
const MEME_TAGS = {
|
|
16
|
+
开心: ['happy', 'like'],
|
|
17
|
+
生气: ['angry', 'baka'],
|
|
18
|
+
委屈: ['sad', 'fool'],
|
|
19
|
+
无语: ['sigh', 'cpu', 'color'],
|
|
20
|
+
害羞: ['shy', 'see'],
|
|
21
|
+
困: ['sleep'],
|
|
22
|
+
惊讶: ['surprised'],
|
|
23
|
+
平静: ['daily', 'morning', 'meow'],
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const DEFAULT_SETTINGS = {
|
|
27
|
+
proactive: true,
|
|
28
|
+
memes: true,
|
|
29
|
+
choices: true,
|
|
30
|
+
memory: true,
|
|
31
|
+
follow: true,
|
|
32
|
+
gaze: true,
|
|
33
|
+
poke: true,
|
|
34
|
+
motion: true,
|
|
35
|
+
sound: true,
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
function settingsOf(cfg) {
|
|
39
|
+
return { ...DEFAULT_SETTINGS, ...(cfg?.settings || {}) };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** 从表情图库按情绪挑一张,返回相对路径 */
|
|
43
|
+
function pickMeme(mood) {
|
|
44
|
+
try {
|
|
45
|
+
const tags = MEME_TAGS[mood] || MEME_TAGS['平静'];
|
|
46
|
+
const galleries = readdirSync(MEMES_ROOT, { withFileTypes: true }).filter((e) => e.isDirectory());
|
|
47
|
+
const pool = [];
|
|
48
|
+
for (const g of galleries) {
|
|
49
|
+
for (const tag of tags) {
|
|
50
|
+
const dir = join(MEMES_ROOT, g.name, 'memes', tag);
|
|
51
|
+
let files = [];
|
|
52
|
+
try {
|
|
53
|
+
files = readdirSync(dir);
|
|
54
|
+
} catch {
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
for (const f of files) {
|
|
58
|
+
if (/\.(png|jpe?g|webp|gif)$/i.test(f)) pool.push(g.name + '/memes/' + tag + '/' + f);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
if (!pool.length) return null;
|
|
63
|
+
return pool[Math.floor(Math.random() * pool.length)];
|
|
64
|
+
} catch {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** 从回复里摘出「||选项1|选项2」这种尾巴 */
|
|
70
|
+
function splitChoices(raw) {
|
|
71
|
+
const m = /\|\|\s*([^|]+(?:\|[^|]+)*)\s*$/.exec(raw);
|
|
72
|
+
if (!m) return { text: raw.trim(), choices: [] };
|
|
73
|
+
const choices = m[1]
|
|
74
|
+
.split('|')
|
|
75
|
+
.map((s) => s.trim())
|
|
76
|
+
.filter(Boolean)
|
|
77
|
+
.slice(0, 4);
|
|
78
|
+
return { text: raw.slice(0, m.index).trim(), choices };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** 粗判情绪,配图用 */
|
|
82
|
+
function guessMood(text) {
|
|
83
|
+
const s = String(text || '');
|
|
84
|
+
if (/[气烦讨厌]/.test(s)) return '生气';
|
|
85
|
+
if (/[委屈难过哭]/.test(s)) return '委屈';
|
|
86
|
+
if (/[困睡熬夜]/.test(s)) return '困';
|
|
87
|
+
if (/[唔害羞]/.test(s)) return '害羞';
|
|
88
|
+
if (/[无语算了]/.test(s)) return '无语';
|
|
89
|
+
if (/[好谢喜欢]/.test(s)) return '开心';
|
|
90
|
+
return '平静';
|
|
91
|
+
}
|
|
12
92
|
|
|
13
93
|
/** 默认音色:一个字一声短音 */
|
|
14
94
|
const DEFAULT_VOICE = { preset: 'soft', type: 'triangle', base: 400, jit: 10, dur: 60, vol: 32, spd: 62 };
|
|
@@ -20,6 +100,23 @@ const DEFAULT_PERSONA = [
|
|
|
20
100
|
'不解释心情,不总结,不升华,不写括号里的动作。',
|
|
21
101
|
].join('\n');
|
|
22
102
|
|
|
103
|
+
/** 没说清要让你挑什么的时候,按情绪给两个短答 */
|
|
104
|
+
const FALLBACK_CHOICES = {
|
|
105
|
+
开心: ['好', '嗯嗯'],
|
|
106
|
+
生气: ['……', '别气'],
|
|
107
|
+
委屈: ['我在', '怎么了'],
|
|
108
|
+
害羞: ['唔', '别看'],
|
|
109
|
+
无语: ['行吧', '你说'],
|
|
110
|
+
困: ['睡吧', '嗯'],
|
|
111
|
+
平静: ['嗯', '在的'],
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const ISOLATION = [
|
|
115
|
+
'【世界隔离】你不知道什么是程序、界面、插件、模型、参数。',
|
|
116
|
+
'有人问起,你就当他就站在你身边。不要提“系统”“设置”“网页”“token”这些词。',
|
|
117
|
+
'你不知道他那边什么样,得他讲给你听。',
|
|
118
|
+
].join('\n');
|
|
119
|
+
|
|
23
120
|
function personaOf(meta) {
|
|
24
121
|
const p = typeof meta?.persona === 'string' ? meta.persona.trim() : '';
|
|
25
122
|
return p || DEFAULT_PERSONA;
|
|
@@ -96,7 +193,7 @@ let lastAutoAt = 0;
|
|
|
96
193
|
|
|
97
194
|
async function maybeTalk(ctx) {
|
|
98
195
|
const cfg = readConfig();
|
|
99
|
-
if (cfg.proactive
|
|
196
|
+
if (!settingsOf(cfg).proactive) return;
|
|
100
197
|
const now = Date.now();
|
|
101
198
|
if (now - LAST_ACTIVE.at < IDLE_MS) return;
|
|
102
199
|
if (now - lastAutoAt < IDLE_MS) return;
|
|
@@ -139,6 +236,7 @@ const MIME = {
|
|
|
139
236
|
'.jpg': 'image/jpeg',
|
|
140
237
|
'.jpeg': 'image/jpeg',
|
|
141
238
|
'.webp': 'image/webp',
|
|
239
|
+
'.gif': 'image/gif',
|
|
142
240
|
'.wasm': 'application/wasm',
|
|
143
241
|
'.moc3': 'application/octet-stream',
|
|
144
242
|
};
|
|
@@ -285,6 +383,12 @@ async function handle(req, res, ctx) {
|
|
|
285
383
|
return file ? sendFile(res, file) : send(res, 403, 'forbidden');
|
|
286
384
|
}
|
|
287
385
|
|
|
386
|
+
// 表情图库
|
|
387
|
+
if (path.startsWith('/media/')) {
|
|
388
|
+
const file = safeJoin(MEMES_ROOT, decodeURIComponent(path.slice('/media/'.length)));
|
|
389
|
+
return file ? sendFile(res, file) : send(res, 403, 'forbidden');
|
|
390
|
+
}
|
|
391
|
+
|
|
288
392
|
// 模型文件
|
|
289
393
|
if (path.startsWith('/model/')) {
|
|
290
394
|
const rest = path.slice('/model/'.length);
|
|
@@ -308,17 +412,20 @@ async function handle(req, res, ctx) {
|
|
|
308
412
|
const slot = modelSlot(ctx);
|
|
309
413
|
if (!slot) return sendJson(res, 503, { error: '模型接口不可用' });
|
|
310
414
|
|
|
311
|
-
const
|
|
415
|
+
const st = settingsOf(readConfig());
|
|
416
|
+
const hist = st.memory ? readChat(id) : [];
|
|
312
417
|
const lines = hist
|
|
313
418
|
.slice(-8)
|
|
314
419
|
.map((m) => (m.role === 'user' ? '他:' : '你:') + String(m.text ?? ''))
|
|
315
420
|
.join('\n');
|
|
316
421
|
const prompt = [
|
|
317
422
|
personaOf(meta),
|
|
423
|
+
meta.isolation === false ? '' : ISOLATION,
|
|
318
424
|
'现在是 ' + nowText() + '。',
|
|
319
425
|
lines ? '你们之前说的:\n' + lines : '',
|
|
320
426
|
'他说:' + text,
|
|
321
427
|
'回他一句。只输出这句话本身:一到三句,短,口语,像发消息。不要解释,不要加引号,不要旁白,不要括号里的动作。',
|
|
428
|
+
st.choices ? '如果你想让他挑,可以在最后另起一行写 ||选项一|选项二,最多三项。' : '',
|
|
322
429
|
]
|
|
323
430
|
.filter(Boolean)
|
|
324
431
|
.join('\n\n');
|
|
@@ -330,21 +437,33 @@ async function handle(req, res, ctx) {
|
|
|
330
437
|
return sendJson(res, 502, { error: '生成失败:' + (e instanceof Error ? e.message : String(e)) });
|
|
331
438
|
}
|
|
332
439
|
LAST_ACTIVE.at = Date.now();
|
|
333
|
-
|
|
334
|
-
|
|
440
|
+
const parsed = splitChoices(reply || '');
|
|
441
|
+
let line = parsed.text;
|
|
442
|
+
let choices = st.choices ? parsed.choices : [];
|
|
443
|
+
if (line.length > 240) line = line.slice(0, 240);
|
|
444
|
+
if (st.choices && !choices.length && line) {
|
|
445
|
+
choices = FALLBACK_CHOICES[guessMood(line)] || FALLBACK_CHOICES['平静'];
|
|
446
|
+
}
|
|
447
|
+
if (!line && !choices.length) return sendJson(res, 200, { ok: true, reply: '', id });
|
|
448
|
+
const image = st.memes && guessMood(line) && Math.random() < 0.35 ? pickMeme(guessMood(line)) : null;
|
|
335
449
|
|
|
336
450
|
writeChat(id, hist.concat([
|
|
337
451
|
{ role: 'user', text, at: Date.now() },
|
|
338
|
-
{ role: 'assistant', text:
|
|
452
|
+
{ role: 'assistant', text: line, at: Date.now() },
|
|
339
453
|
]));
|
|
340
|
-
return sendJson(res, 200, { ok: true, reply, id });
|
|
454
|
+
return sendJson(res, 200, { ok: true, reply: line, choices, image: image || undefined, id });
|
|
341
455
|
}
|
|
342
456
|
|
|
343
457
|
// 人设卡
|
|
344
458
|
if (path === '/api/persona' && method === 'GET') {
|
|
345
459
|
const id = String(url.searchParams.get('id') ?? '');
|
|
346
460
|
const meta = id ? readMeta(id) : null;
|
|
347
|
-
return sendJson(res, 200, {
|
|
461
|
+
return sendJson(res, 200, {
|
|
462
|
+
id,
|
|
463
|
+
persona: personaOf(meta),
|
|
464
|
+
custom: typeof meta?.persona === 'string' && !!meta.persona.trim(),
|
|
465
|
+
isolation: meta?.isolation !== false,
|
|
466
|
+
});
|
|
348
467
|
}
|
|
349
468
|
|
|
350
469
|
if (path === '/api/persona' && method === 'POST') {
|
|
@@ -352,9 +471,28 @@ async function handle(req, res, ctx) {
|
|
|
352
471
|
const id = String(body.id ?? '');
|
|
353
472
|
const meta = id ? readMeta(id) : null;
|
|
354
473
|
if (!meta) return sendJson(res, 404, { error: 'model not found' });
|
|
355
|
-
const persona = typeof body.persona === 'string' ? body.persona :
|
|
356
|
-
|
|
357
|
-
|
|
474
|
+
const persona = typeof body.persona === 'string' ? body.persona : meta.persona;
|
|
475
|
+
const isolation = typeof body.isolation === 'boolean' ? body.isolation : meta.isolation !== false;
|
|
476
|
+
writeFileSync(join(MODELS, id, 'meta.json'), JSON.stringify({ ...meta, persona, isolation }, null, 2), 'utf8');
|
|
477
|
+
return sendJson(res, 200, { ok: true, persona: personaOf({ persona }), isolation });
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// 开关:哪些功能开着
|
|
481
|
+
if (path === '/api/settings' && method === 'GET') {
|
|
482
|
+
return sendJson(res, 200, { settings: settingsOf(readConfig()) });
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
if (path === '/api/settings' && method === 'POST') {
|
|
486
|
+
const body = JSON.parse((await readBody(req)).toString('utf8') || '{}');
|
|
487
|
+
const cfg = readConfig();
|
|
488
|
+
const cur = settingsOf(cfg);
|
|
489
|
+
const next = { ...cur };
|
|
490
|
+
const patch = body && typeof body.settings === 'object' ? body.settings : body;
|
|
491
|
+
for (const k of Object.keys(DEFAULT_SETTINGS)) {
|
|
492
|
+
if (typeof patch?.[k] === 'boolean') next[k] = patch[k];
|
|
493
|
+
}
|
|
494
|
+
writeConfig({ ...cfg, settings: next });
|
|
495
|
+
return sendJson(res, 200, { ok: true, settings: next });
|
|
358
496
|
}
|
|
359
497
|
|
|
360
498
|
// 出口:别的插件、im 机器人、脚本都能把她要说的话推进来
|
|
@@ -379,6 +517,11 @@ async function handle(req, res, ctx) {
|
|
|
379
517
|
return sendJson(res, 200, { seq: outSeq, items: OUTBOX.filter((m) => m.seq > since) });
|
|
380
518
|
}
|
|
381
519
|
|
|
520
|
+
// 人设卡页
|
|
521
|
+
if (path === '/persona' || path === '/persona.html') {
|
|
522
|
+
return sendFile(res, join(ASSETS, 'persona.html'), MIME['.html']);
|
|
523
|
+
}
|
|
524
|
+
|
|
382
525
|
// 音色调音页
|
|
383
526
|
if (path === '/tone' || path === '/tone.html') {
|
|
384
527
|
return sendFile(res, join(ASSETS, 'tone.html'), MIME['.html']);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-live2dwaifu",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "给 DSH 网页右下角请一个 Live2D 角色:会眨眼、会呼吸、视线跟着鼠标、点她有反应、说话时嘴动、心情变了眉毛跟着动。模型不用内置——谁装谁放自己的文件夹;支持自定义 .exp3.json 表情和 .motion3.json 动作,卡片能拖能缩,背景可换。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|