dsh-live2dwaifu 0.5.0 → 0.7.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 +11 -1
- package/assets/memes/bunaifan.jpg +0 -0
- package/assets/memes/deyi.jpg +0 -0
- package/assets/memes/diantou.jpg +0 -0
- package/assets/memes/haixiu.jpg +0 -0
- package/assets/memes/jingya.jpg +0 -0
- package/assets/memes/kaixin.jpg +0 -0
- package/assets/memes/kun.jpg +0 -0
- package/assets/memes/manifest.json +29 -0
- package/assets/memes/shengqi.jpg +0 -0
- package/assets/memes/sikao.jpg +0 -0
- package/assets/memes/wuyu.jpg +0 -0
- package/assets/memes/xianqi.jpg +0 -0
- package/assets/memes/xin-cui.jpg +0 -0
- package/assets/memes/xin-daxiao.jpg +0 -0
- package/assets/memes/xin-hei.jpg +0 -0
- package/assets/memes/xin-ing.jpg +0 -0
- package/assets/memes/xin-peng.jpg +0 -0
- package/assets/memes/xin-tiao.jpg +0 -0
- package/assets/memes/xin-xiao.jpg +0 -0
- package/assets/memes/xin-yao.jpg +0 -0
- package/assets/memes/zhaoshou.jpg +0 -0
- package/assets/persona.html +149 -0
- package/assets/viewer.html +4 -0
- package/assets/viewer.js +37 -1
- package/lib/client.js +2 -0
- package/lib/index.js +89 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
|
|
24
24
|
**会出声**:一个字一声很短的电音,标点停一拍,句尾沉一下——不是朗读,是“哔哔”那种。标题栏的 **♪** 打开调音页,放个真模型在旁边,边调边听;每只模型各存一份音色,换人换声。音量拉到 0 就安静,只剩气泡和口型。
|
|
25
25
|
|
|
26
|
-
|
|
26
|
+
**她也会甩图(测试功能)**:⚙ 里有一根滑轴,四档——不使用 / 只使用自带 / 均开启 / 只用 dsh-meme。自带那套是夏铃自己的 Q 版贴纸,二十张,随插件分发,版权归本插件;dsh-meme 那套只是借用你本机已经装的图库。默认关着,功能还在试。
|
|
27
27
|
|
|
28
28
|
**她还会问你要选项**:话说到了需要你挑的地方,气泡下面会冒出两三个按钮,点一个就等于你回了她。
|
|
29
29
|
|
|
@@ -125,6 +125,16 @@ frame.contentWindow.postMessage({ type: 'dsh-live2d/manage' }, '*');
|
|
|
125
125
|
**能放动作吗?**
|
|
126
126
|
暂时不能编排 `.motion3.json`,表情是固定的七种。想要更多情绪,给它发 `mood`,等以后的版本。
|
|
127
127
|
|
|
128
|
+
## 关于别人的插件
|
|
129
|
+
|
|
130
|
+
她的图、她的耳朵,都是从你**已经装好的**插件里读的。这个包里不带任何第三方素材,也一行没改别人的代码:
|
|
131
|
+
|
|
132
|
+
- **表情图**:从 `dsh-meme` 的图库目录里挑一张,只在本机显示,不上传、不分发。没装 `dsh-meme` 就没有图,其它照常。
|
|
133
|
+
- **麦克风**:交给 `dsh-ears`,本插件不碰录音,只是把转写好的字送进对话。
|
|
134
|
+
- **对话**:走 DSH 自己的模型接口(`ctx.llm`),不经过任何人的会话,也不写你的聊天记录。
|
|
135
|
+
|
|
136
|
+
不喜欢这些联动,⚙ 里都能单独关掉。
|
|
137
|
+
|
|
128
138
|
## 已知限制
|
|
129
139
|
|
|
130
140
|
- 只支持 Cubism 3 / 4(表情只认 `.exp3.json`,不认 `.motion3.json` 里的表情轨)
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "xialing-001",
|
|
3
|
+
"name": "夏铃",
|
|
4
|
+
"version": "1.0.0",
|
|
5
|
+
"license": "随本插件以 MIT 分发;图由 LFR 用 Seedream 生成",
|
|
6
|
+
"note": "Q 版三头身,白大褂 + 红高领毛衣 + 黑裤,淡灰虹膜,细黑框眼镜。",
|
|
7
|
+
"items": [
|
|
8
|
+
{ "file": "deyi.jpg", "label": "得意", "moods": ["开心"] },
|
|
9
|
+
{ "file": "shengqi.jpg", "label": "生气", "moods": ["生气"] },
|
|
10
|
+
{ "file": "haixiu.jpg", "label": "害羞", "moods": ["害羞"] },
|
|
11
|
+
{ "file": "kun.jpg", "label": "困", "moods": ["困"] },
|
|
12
|
+
{ "file": "wuyu.jpg", "label": "无语", "moods": ["无语"] },
|
|
13
|
+
{ "file": "jingya.jpg", "label": "惊讶", "moods": ["惊讶"] },
|
|
14
|
+
{ "file": "kaixin.jpg", "label": "开心", "moods": ["开心"] },
|
|
15
|
+
{ "file": "sikao.jpg", "label": "想事情", "moods": ["平静"] },
|
|
16
|
+
{ "file": "xianqi.jpg", "label": "嫌弃", "moods": ["无语", "生气"] },
|
|
17
|
+
{ "file": "diantou.jpg", "label": "点头", "moods": ["平静"] },
|
|
18
|
+
{ "file": "zhaoshou.jpg", "label": "打招呼", "moods": ["开心"] },
|
|
19
|
+
{ "file": "bunaifan.jpg", "label": "别烦我", "moods": ["生气"] },
|
|
20
|
+
{ "file": "xin-ing.jpg", "label": "心动", "moods": ["害羞"] },
|
|
21
|
+
{ "file": "xin-cui.jpg", "label": "手指抵唇", "moods": ["害羞"] },
|
|
22
|
+
{ "file": "xin-peng.jpg", "label": "崩溃", "moods": ["惊讶"] },
|
|
23
|
+
{ "file": "xin-tiao.jpg", "label": "挑衅", "moods": ["生气"] },
|
|
24
|
+
{ "file": "xin-xiao.jpg", "label": "看好戏", "moods": ["开心"] },
|
|
25
|
+
{ "file": "xin-daxiao.jpg", "label": "大笑", "moods": ["开心"] },
|
|
26
|
+
{ "file": "xin-hei.jpg", "label": "暗笑", "moods": ["无语"] },
|
|
27
|
+
{ "file": "xin-yao.jpg", "label": "馋", "moods": ["害羞"] }
|
|
28
|
+
]
|
|
29
|
+
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -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
|
@@ -42,6 +42,10 @@
|
|
|
42
42
|
#settings h3 { font-size: 10px; letter-spacing: .6px; color: #6f7783; font-weight: 600; margin: 8px 0 4px; }
|
|
43
43
|
.opt { display: flex; align-items: center; gap: 8px; padding: 3px 2px; font-size: 12px; color: #c3c9d3; }
|
|
44
44
|
.opt input { accent-color: #6aa9ff; }
|
|
45
|
+
.opt-col { padding: 4px 2px 2px; }
|
|
46
|
+
.opt-col input[type=range] { width: 100%; accent-color: #6aa9ff; }
|
|
47
|
+
.opt-lab { font-size: 12px; color: #dfe4ec; margin-top: 2px; }
|
|
48
|
+
.opt-note { font-size: 10.5px; line-height: 1.6; color: #6f7783; margin-top: 4px; }
|
|
45
49
|
.bub-img { display: block; max-width: 100%; max-height: 120px; border-radius: 8px; margin-bottom: 6px; object-fit: contain; }
|
|
46
50
|
#actions { display: flex; gap: 6px; }
|
|
47
51
|
#actions button { flex: 1; background: #2f343d; color: #cfd4dd; border: 1px solid #3c424c;
|
package/assets/viewer.js
CHANGED
|
@@ -44346,10 +44346,11 @@ void main(void)\r
|
|
|
44346
44346
|
];
|
|
44347
44347
|
var SERVER_OPTS = [
|
|
44348
44348
|
["proactive", "\u6CA1\u4EBA\u7406\u5C31\u81EA\u5DF1\u5F00\u53E3"],
|
|
44349
|
-
["memes", "\u4F1A\u7529\u8868\u60C5\u5305"],
|
|
44350
44349
|
["choices", "\u7ED9\u9009\u9879\u8BA9\u4ED6\u6311"],
|
|
44351
44350
|
["memory", "\u8BB0\u4F4F\u4E4B\u524D\u8BF4\u7684"]
|
|
44352
44351
|
];
|
|
44352
|
+
var MEME_MODES = ["off", "own", "both", "external"];
|
|
44353
|
+
var MEME_LABELS = ["\u4E0D\u4F7F\u7528", "\u53EA\u4F7F\u7528\u81EA\u5E26", "\u5747\u5F00\u542F", "\u53EA\u7528 dsh-meme"];
|
|
44353
44354
|
var serverSettings = {};
|
|
44354
44355
|
function renderSettings() {
|
|
44355
44356
|
const box = $2("settings");
|
|
@@ -44391,6 +44392,41 @@ void main(void)\r
|
|
|
44391
44392
|
row.appendChild(document.createTextNode(label));
|
|
44392
44393
|
box.appendChild(row);
|
|
44393
44394
|
}
|
|
44395
|
+
const h3 = document.createElement("h3");
|
|
44396
|
+
h3.textContent = "\u8868\u60C5\u5305\uFF08\u6D4B\u8BD5\u529F\u80FD\uFF09";
|
|
44397
|
+
box.appendChild(h3);
|
|
44398
|
+
const wrap = document.createElement("div");
|
|
44399
|
+
wrap.className = "opt-col";
|
|
44400
|
+
const slider = document.createElement("input");
|
|
44401
|
+
slider.type = "range";
|
|
44402
|
+
slider.min = "0";
|
|
44403
|
+
slider.max = String(MEME_MODES.length - 1);
|
|
44404
|
+
slider.step = "1";
|
|
44405
|
+
const cur = Math.max(0, MEME_MODES.indexOf(serverSettings.memesMode || "off"));
|
|
44406
|
+
slider.value = String(cur);
|
|
44407
|
+
const lab = document.createElement("div");
|
|
44408
|
+
lab.className = "opt-lab";
|
|
44409
|
+
lab.textContent = MEME_LABELS[cur];
|
|
44410
|
+
slider.oninput = () => {
|
|
44411
|
+
lab.textContent = MEME_LABELS[Number(slider.value)];
|
|
44412
|
+
};
|
|
44413
|
+
slider.onchange = () => {
|
|
44414
|
+
const mode = MEME_MODES[Number(slider.value)];
|
|
44415
|
+
serverSettings.memesMode = mode;
|
|
44416
|
+
fetch(BASE + "/api/settings", {
|
|
44417
|
+
method: "POST",
|
|
44418
|
+
headers: { "content-type": "application/json" },
|
|
44419
|
+
body: JSON.stringify({ settings: { memesMode: mode } })
|
|
44420
|
+
}).catch(() => {
|
|
44421
|
+
});
|
|
44422
|
+
};
|
|
44423
|
+
const note = document.createElement("div");
|
|
44424
|
+
note.className = "opt-note";
|
|
44425
|
+
note.textContent = "\u81EA\u5E26\u90A3\u5957\u662F\u590F\u94C3\u7684 Q \u7248\u8D34\u7EB8\uFF0C\u4E8C\u5341\u5F20\uFF1Bdsh-meme \u90A3\u5957\u501F\u7528\u4F60\u672C\u673A\u5DF2\u7ECF\u88C5\u7684\u56FE\u5E93\u3002\u529F\u80FD\u8FD8\u5728\u8BD5\u9A8C\uFF0C\u53EF\u80FD\u4F1A\u8C03\u3002";
|
|
44426
|
+
wrap.appendChild(slider);
|
|
44427
|
+
wrap.appendChild(lab);
|
|
44428
|
+
wrap.appendChild(note);
|
|
44429
|
+
box.appendChild(wrap);
|
|
44394
44430
|
}
|
|
44395
44431
|
function loadSettings() {
|
|
44396
44432
|
fetch(BASE + "/api/settings").then((r2) => r2.json()).then((d2) => {
|
package/lib/client.js
CHANGED
|
@@ -177,6 +177,7 @@
|
|
|
177
177
|
|
|
178
178
|
const manage = () => ref.current && ref.current.contentWindow.postMessage({ type: 'dsh-live2d/manage' }, '*');
|
|
179
179
|
const openTone = () => window.open(BASE + '/tone', '_blank', 'width=1140,height=780');
|
|
180
|
+
const openPersona = () => window.open(BASE + '/persona', '_blank', 'width=1080,height=740');
|
|
180
181
|
|
|
181
182
|
const send = () => {
|
|
182
183
|
const t = draft.trim();
|
|
@@ -222,6 +223,7 @@
|
|
|
222
223
|
open && h('button', { title: '模型管理', onPointerDown: noDrag, onClick: manage }, '⚙'),
|
|
223
224
|
open && h('button', { title: '背景:' + BG_LABEL[bg], onPointerDown: noDrag, onClick: cycleBg }, '◐'),
|
|
224
225
|
open && h('button', { title: '音色', onPointerDown: noDrag, onClick: openTone }, '♪'),
|
|
226
|
+
open && h('button', { title: '人设卡', onPointerDown: noDrag, onClick: openPersona }, '✎'),
|
|
225
227
|
open && h('button', { title: '刷新', onPointerDown: noDrag, onClick: () => setNonce((n) => n + 1) }, '↻'),
|
|
226
228
|
h('button', { title: open ? '收起' : '展开', onPointerDown: noDrag, onClick: () => setOpen(!open) }, open ? '⌄' : '⌃'),
|
|
227
229
|
),
|
package/lib/index.js
CHANGED
|
@@ -25,7 +25,8 @@ const MEME_TAGS = {
|
|
|
25
25
|
|
|
26
26
|
const DEFAULT_SETTINGS = {
|
|
27
27
|
proactive: true,
|
|
28
|
-
|
|
28
|
+
/** off | own | both | external —— 测试功能,默认关 */
|
|
29
|
+
memesMode: 'off',
|
|
29
30
|
choices: true,
|
|
30
31
|
memory: true,
|
|
31
32
|
follow: true,
|
|
@@ -35,6 +36,45 @@ const DEFAULT_SETTINGS = {
|
|
|
35
36
|
sound: true,
|
|
36
37
|
};
|
|
37
38
|
|
|
39
|
+
const MEMES_MODES = ['off', 'own', 'both', 'external'];
|
|
40
|
+
const OWN_MEME_DIR = join(ASSETS, 'memes');
|
|
41
|
+
|
|
42
|
+
function readOwnMemes() {
|
|
43
|
+
try {
|
|
44
|
+
const m = JSON.parse(readFileSync(join(OWN_MEME_DIR, 'manifest.json'), 'utf8'));
|
|
45
|
+
return Array.isArray(m.items) ? m.items : [];
|
|
46
|
+
} catch {
|
|
47
|
+
return [];
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** 从自带那套里按情绪挑一张,返回文件名 */
|
|
52
|
+
function pickOwnMeme(mood) {
|
|
53
|
+
const items = readOwnMemes();
|
|
54
|
+
if (!items.length) return null;
|
|
55
|
+
const hit = items.filter((it) => Array.isArray(it.moods) && it.moods.includes(mood));
|
|
56
|
+
const pool = hit.length ? hit : items;
|
|
57
|
+
const it = pool[Math.floor(Math.random() * pool.length)];
|
|
58
|
+
return it && it.file ? it.file : null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** 按模式挑一张配图,返回可直接用的 URL */
|
|
62
|
+
function pickImage(mode, mood) {
|
|
63
|
+
const m = MEMES_MODES.includes(mode) ? mode : 'off';
|
|
64
|
+
if (m === 'off') return null;
|
|
65
|
+
const pool = [];
|
|
66
|
+
if (m === 'own' || m === 'both') {
|
|
67
|
+
const f = pickOwnMeme(mood);
|
|
68
|
+
if (f) pool.push('/dsh-live2d/ownmeme/' + encodeURIComponent(f));
|
|
69
|
+
}
|
|
70
|
+
if (m === 'external' || m === 'both') {
|
|
71
|
+
const f = pickMeme(mood);
|
|
72
|
+
if (f) pool.push('/dsh-live2d/media/' + f.split('/').map(encodeURIComponent).join('/'));
|
|
73
|
+
}
|
|
74
|
+
if (!pool.length) return null;
|
|
75
|
+
return pool[Math.floor(Math.random() * pool.length)];
|
|
76
|
+
}
|
|
77
|
+
|
|
38
78
|
function settingsOf(cfg) {
|
|
39
79
|
return { ...DEFAULT_SETTINGS, ...(cfg?.settings || {}) };
|
|
40
80
|
}
|
|
@@ -100,6 +140,23 @@ const DEFAULT_PERSONA = [
|
|
|
100
140
|
'不解释心情,不总结,不升华,不写括号里的动作。',
|
|
101
141
|
].join('\n');
|
|
102
142
|
|
|
143
|
+
/** 没说清要让你挑什么的时候,按情绪给两个短答 */
|
|
144
|
+
const FALLBACK_CHOICES = {
|
|
145
|
+
开心: ['好', '嗯嗯'],
|
|
146
|
+
生气: ['……', '别气'],
|
|
147
|
+
委屈: ['我在', '怎么了'],
|
|
148
|
+
害羞: ['唔', '别看'],
|
|
149
|
+
无语: ['行吧', '你说'],
|
|
150
|
+
困: ['睡吧', '嗯'],
|
|
151
|
+
平静: ['嗯', '在的'],
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
const ISOLATION = [
|
|
155
|
+
'【世界隔离】你不知道什么是程序、界面、插件、模型、参数。',
|
|
156
|
+
'有人问起,你就当他就站在你身边。不要提“系统”“设置”“网页”“token”这些词。',
|
|
157
|
+
'你不知道他那边什么样,得他讲给你听。',
|
|
158
|
+
].join('\n');
|
|
159
|
+
|
|
103
160
|
function personaOf(meta) {
|
|
104
161
|
const p = typeof meta?.persona === 'string' ? meta.persona.trim() : '';
|
|
105
162
|
return p || DEFAULT_PERSONA;
|
|
@@ -366,6 +423,12 @@ async function handle(req, res, ctx) {
|
|
|
366
423
|
return file ? sendFile(res, file) : send(res, 403, 'forbidden');
|
|
367
424
|
}
|
|
368
425
|
|
|
426
|
+
// 自带的 Q 版表情包
|
|
427
|
+
if (path.startsWith('/ownmeme/')) {
|
|
428
|
+
const file = safeJoin(OWN_MEME_DIR, decodeURIComponent(path.slice('/ownmeme/'.length)));
|
|
429
|
+
return file ? sendFile(res, file) : send(res, 403, 'forbidden');
|
|
430
|
+
}
|
|
431
|
+
|
|
369
432
|
// 表情图库
|
|
370
433
|
if (path.startsWith('/media/')) {
|
|
371
434
|
const file = safeJoin(MEMES_ROOT, decodeURIComponent(path.slice('/media/'.length)));
|
|
@@ -403,6 +466,7 @@ async function handle(req, res, ctx) {
|
|
|
403
466
|
.join('\n');
|
|
404
467
|
const prompt = [
|
|
405
468
|
personaOf(meta),
|
|
469
|
+
meta.isolation === false ? '' : ISOLATION,
|
|
406
470
|
'现在是 ' + nowText() + '。',
|
|
407
471
|
lines ? '你们之前说的:\n' + lines : '',
|
|
408
472
|
'他说:' + text,
|
|
@@ -421,10 +485,14 @@ async function handle(req, res, ctx) {
|
|
|
421
485
|
LAST_ACTIVE.at = Date.now();
|
|
422
486
|
const parsed = splitChoices(reply || '');
|
|
423
487
|
let line = parsed.text;
|
|
424
|
-
|
|
488
|
+
let choices = st.choices ? parsed.choices : [];
|
|
425
489
|
if (line.length > 240) line = line.slice(0, 240);
|
|
490
|
+
if (st.choices && !choices.length && line) {
|
|
491
|
+
choices = FALLBACK_CHOICES[guessMood(line)] || FALLBACK_CHOICES['平静'];
|
|
492
|
+
}
|
|
426
493
|
if (!line && !choices.length) return sendJson(res, 200, { ok: true, reply: '', id });
|
|
427
|
-
const
|
|
494
|
+
const moodNow = guessMood(line);
|
|
495
|
+
const image = st.memesMode !== 'off' && Math.random() < 0.35 ? pickImage(st.memesMode, moodNow) : null;
|
|
428
496
|
|
|
429
497
|
writeChat(id, hist.concat([
|
|
430
498
|
{ role: 'user', text, at: Date.now() },
|
|
@@ -437,7 +505,12 @@ async function handle(req, res, ctx) {
|
|
|
437
505
|
if (path === '/api/persona' && method === 'GET') {
|
|
438
506
|
const id = String(url.searchParams.get('id') ?? '');
|
|
439
507
|
const meta = id ? readMeta(id) : null;
|
|
440
|
-
return sendJson(res, 200, {
|
|
508
|
+
return sendJson(res, 200, {
|
|
509
|
+
id,
|
|
510
|
+
persona: personaOf(meta),
|
|
511
|
+
custom: typeof meta?.persona === 'string' && !!meta.persona.trim(),
|
|
512
|
+
isolation: meta?.isolation !== false,
|
|
513
|
+
});
|
|
441
514
|
}
|
|
442
515
|
|
|
443
516
|
if (path === '/api/persona' && method === 'POST') {
|
|
@@ -445,9 +518,10 @@ async function handle(req, res, ctx) {
|
|
|
445
518
|
const id = String(body.id ?? '');
|
|
446
519
|
const meta = id ? readMeta(id) : null;
|
|
447
520
|
if (!meta) return sendJson(res, 404, { error: 'model not found' });
|
|
448
|
-
const persona = typeof body.persona === 'string' ? body.persona :
|
|
449
|
-
|
|
450
|
-
|
|
521
|
+
const persona = typeof body.persona === 'string' ? body.persona : meta.persona;
|
|
522
|
+
const isolation = typeof body.isolation === 'boolean' ? body.isolation : meta.isolation !== false;
|
|
523
|
+
writeFileSync(join(MODELS, id, 'meta.json'), JSON.stringify({ ...meta, persona, isolation }, null, 2), 'utf8');
|
|
524
|
+
return sendJson(res, 200, { ok: true, persona: personaOf({ persona }), isolation });
|
|
451
525
|
}
|
|
452
526
|
|
|
453
527
|
// 开关:哪些功能开着
|
|
@@ -464,6 +538,9 @@ async function handle(req, res, ctx) {
|
|
|
464
538
|
for (const k of Object.keys(DEFAULT_SETTINGS)) {
|
|
465
539
|
if (typeof patch?.[k] === 'boolean') next[k] = patch[k];
|
|
466
540
|
}
|
|
541
|
+
if (typeof patch?.memesMode === 'string' && MEMES_MODES.includes(patch.memesMode)) {
|
|
542
|
+
next.memesMode = patch.memesMode;
|
|
543
|
+
}
|
|
467
544
|
writeConfig({ ...cfg, settings: next });
|
|
468
545
|
return sendJson(res, 200, { ok: true, settings: next });
|
|
469
546
|
}
|
|
@@ -490,6 +567,11 @@ async function handle(req, res, ctx) {
|
|
|
490
567
|
return sendJson(res, 200, { seq: outSeq, items: OUTBOX.filter((m) => m.seq > since) });
|
|
491
568
|
}
|
|
492
569
|
|
|
570
|
+
// 人设卡页
|
|
571
|
+
if (path === '/persona' || path === '/persona.html') {
|
|
572
|
+
return sendFile(res, join(ASSETS, 'persona.html'), MIME['.html']);
|
|
573
|
+
}
|
|
574
|
+
|
|
493
575
|
// 音色调音页
|
|
494
576
|
if (path === '/tone' || path === '/tone.html') {
|
|
495
577
|
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.7.0",
|
|
4
4
|
"description": "给 DSH 网页右下角请一个 Live2D 角色:会眨眼、会呼吸、视线跟着鼠标、点她有反应、说话时嘴动、心情变了眉毛跟着动。模型不用内置——谁装谁放自己的文件夹;支持自定义 .exp3.json 表情和 .motion3.json 动作,卡片能拖能缩,背景可换。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|