dsh-live2dwaifu 1.0.5 → 1.1.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 +38 -2
- package/assets/corpus.html +290 -0
- package/assets/viewer.html +1 -0
- package/assets/viewer.js +9 -0
- package/lib/client.js +39 -3
- package/lib/index.js +86 -15
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -47,8 +47,8 @@ dsh plugin --profile desktop add dsh-live2dwaifu
|
|
|
47
47
|
|---|---|
|
|
48
48
|
| **⚙** | 抽屉:模型列表、自定义表情、全部开关 |
|
|
49
49
|
| **◐** | 换背景:透明 / 深色 / 浅色 / 纸质 |
|
|
50
|
-
| **♪** |
|
|
51
|
-
| **✎** |
|
|
50
|
+
| **♪** | 音色调色页(每只模型各存一份音色),在卡片里盖一层打开 |
|
|
51
|
+
| **✎** | 人设卡编辑页,同样盖一层打开 |
|
|
52
52
|
| **↻** | 重载 iframe(模型卡住时点这个) |
|
|
53
53
|
| **⌄ / ⌃** | 收起 / 展开 |
|
|
54
54
|
|
|
@@ -123,6 +123,8 @@ dsh plugin --profile desktop add dsh-live2dwaifu
|
|
|
123
123
|
|
|
124
124
|
提示词里还会明确禁止:不总结、不分点、不讲道理、不解释心情、你没问就不给建议。
|
|
125
125
|
|
|
126
|
+
另外给了几组说话样例(“他:今天吃啥 —— 你:锅里还有,热的。”这种),让她照着那个密度和信息量说,不是照着内容抄。长度硬卡在四十字上下,超了会被截。
|
|
127
|
+
|
|
126
128
|
> 还是觉得哪句像 AI?记下来告诉作者,往规则里加。
|
|
127
129
|
|
|
128
130
|
### 她偶尔给你选项
|
|
@@ -143,6 +145,40 @@ dsh plugin --profile desktop add dsh-live2dwaifu
|
|
|
143
145
|
|
|
144
146
|
---
|
|
145
147
|
|
|
148
|
+
## 喂她语料
|
|
149
|
+
|
|
150
|
+
人设卡告诉她「是谁」,语料告诉她「怎么说话」。⚙ 抽屉里的 **语料** 按钮打开一个页面,有模型选择、大文本框、文件按钮和拖拽区。
|
|
151
|
+
|
|
152
|
+
**四种喂法,怎么方便怎么来:**
|
|
153
|
+
|
|
154
|
+
1. 直接粘贴——打字、贴聊天记录、贴剧本都行
|
|
155
|
+
2. 点「选文件」,`.txt` / `.json` / `.jsonl` / `.csv`,可以多选
|
|
156
|
+
3. 把文件拖到那个虚框里
|
|
157
|
+
4. 一行一句地贴她的台词,什么都不用标
|
|
158
|
+
|
|
159
|
+
**认得的写法**:
|
|
160
|
+
|
|
161
|
+
```
|
|
162
|
+
他:今天吃啥 ← 认得出谁说的
|
|
163
|
+
你:锅里还有,热的。
|
|
164
|
+
|
|
165
|
+
「嗯。我也是。」 ← 只认她是说话人
|
|
166
|
+
|
|
167
|
+
- 先坐下。水在桌上。 ← 一行一句台词
|
|
168
|
+
|
|
169
|
+
[{"user":"累吗","assistant":"还行。"}] ← JSON 也行
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
说话人写 `他 / 我 / 用户` 算你,`你 / 她 / 助手` 算她;认不出来的一律当她说的——**只贴台词最省事**。
|
|
173
|
+
|
|
174
|
+
页面右边会实时显示解析出来的条数,和前面十几条预览。贴错了能看见,改完再存。
|
|
175
|
+
|
|
176
|
+
**存法有两种**:「加进去」是并到原来的里面,「替换成这些」是把之前清掉。第一次喂就用加。
|
|
177
|
+
|
|
178
|
+
以后她说话会从语料里挑几条贴进去当样例——优先挑跟你这句话沾边的问答,另外随机抽几句她的台词找语感。所以**语料越多、越像她本人,她说得就越像**。
|
|
179
|
+
|
|
180
|
+
存的位置:`~/.dsh/dsh-live2d/models/<id>/corpus.json`。
|
|
181
|
+
|
|
146
182
|
## 声音
|
|
147
183
|
|
|
148
184
|
她说话时一个字一声很短的电音,标点停一拍,句尾沉一下。不是朗读,是“哔哔”那种。
|
|
@@ -0,0 +1,290 @@
|
|
|
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:#15181e; --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; padding:22px 24px 28px; }
|
|
11
|
+
h1 { font-size:15px; margin:0 0 4px; font-weight:600; }
|
|
12
|
+
p.sub { margin:0 0 14px; color:var(--dim); font-size:12px; }
|
|
13
|
+
.row { display:flex; gap:12px; align-items:center; margin-bottom:12px; flex-wrap:wrap; }
|
|
14
|
+
select, button { font:inherit; }
|
|
15
|
+
select { padding:8px 10px; border-radius:9px; border:1px solid #3c424c; background:#181c22; color:var(--txt); font-size:13px; }
|
|
16
|
+
.btns { display:flex; gap:8px; flex-wrap:wrap; }
|
|
17
|
+
button.act { padding:8px 14px; border-radius:9px; border:1px solid #3c424c; background:#232830;
|
|
18
|
+
color:#dfe4ec; cursor:pointer; font-size:12px; }
|
|
19
|
+
button.act.primary { background:#27405f; border-color:#3e6b9e; color:#eaf2ff; }
|
|
20
|
+
button.act:hover { filter:brightness(1.14); }
|
|
21
|
+
.cols { display:flex; gap:14px; align-items:flex-start; flex-wrap:wrap; }
|
|
22
|
+
.col { flex:1 1 380px; min-width:320px; }
|
|
23
|
+
.box { border:1px solid var(--line); border-radius:12px; background:var(--card); padding:12px; }
|
|
24
|
+
.box h2 { font-size:11px; letter-spacing:.6px; color:var(--dim); margin:0 0 8px; font-weight:600; }
|
|
25
|
+
textarea { width:100%; height:220px; resize:vertical; padding:10px 12px; border-radius:10px;
|
|
26
|
+
border:1px solid #3c424c; background:#181c22; color:var(--txt);
|
|
27
|
+
font:12.5px/1.75 "Microsoft YaHei", system-ui, sans-serif; outline:none; }
|
|
28
|
+
textarea:focus { border-color:#3f4a5c; }
|
|
29
|
+
#drop { margin-top:10px; border:1px dashed #3c424c; border-radius:10px; padding:14px; text-align:center;
|
|
30
|
+
color:var(--dim); font-size:12px; cursor:pointer; transition:all .15s; }
|
|
31
|
+
#drop.hot { border-color:var(--acc); color:#cfe2ff; background:rgba(106,169,255,.06); }
|
|
32
|
+
.stat { font-size:12px; color:var(--dim); margin-bottom:6px; }
|
|
33
|
+
.stat b { color:#e2e8f1; font-weight:600; }
|
|
34
|
+
#preview { max-height:260px; overflow-y:auto; font-size:12px; line-height:1.75; }
|
|
35
|
+
#preview div { padding:3px 0; border-bottom:1px solid rgba(255,255,255,.04); }
|
|
36
|
+
#preview .who { color:#6f7783; }
|
|
37
|
+
.tips { color:#6f7783; font-size:11.5px; line-height:1.8; margin-top:10px; }
|
|
38
|
+
code { background:rgba(255,255,255,.06); border-radius:4px; padding:1px 5px; font-size:11px; }
|
|
39
|
+
.state { color:var(--dim); font-size:11.5px; margin-left:auto; }
|
|
40
|
+
</style>
|
|
41
|
+
</head>
|
|
42
|
+
<body>
|
|
43
|
+
|
|
44
|
+
<h1>语料</h1>
|
|
45
|
+
<p class="sub">喂她一些她说过的话,她就照着这个调子说。不用排版整齐,能读懂就行。</p>
|
|
46
|
+
|
|
47
|
+
<div class="row">
|
|
48
|
+
<select id="models"></select>
|
|
49
|
+
<div class="btns">
|
|
50
|
+
<button class="act" id="pick">选文件</button>
|
|
51
|
+
<button class="act primary" id="append">加进去</button>
|
|
52
|
+
<button class="act" id="replace">替换成这些</button>
|
|
53
|
+
</div>
|
|
54
|
+
<span class="state" id="state"></span>
|
|
55
|
+
</div>
|
|
56
|
+
|
|
57
|
+
<div class="cols">
|
|
58
|
+
<div class="col box">
|
|
59
|
+
<h2>把话贴进来</h2>
|
|
60
|
+
<textarea id="raw" spellcheck="false" placeholder="随便贴,几种写法都认:
|
|
61
|
+
|
|
62
|
+
他:今天吃啥
|
|
63
|
+
你:锅里还有,热的。
|
|
64
|
+
|
|
65
|
+
或者只贴她的台词,一行一句:
|
|
66
|
+
锅里还有,热的。
|
|
67
|
+
先坐下。水在桌上。
|
|
68
|
+
|
|
69
|
+
也支持她说过的话带引号:「嗯。我也是。」"></textarea>
|
|
70
|
+
<div id="drop">把 .txt / .json / .jsonl 拖到这里,或者点一下选文件(可以多选)</div>
|
|
71
|
+
<input id="files" type="file" multiple accept=".txt,.json,.jsonl,.csv" style="display:none" />
|
|
72
|
+
<div class="tips">
|
|
73
|
+
认得的写法:<code>他:… / 你:…</code>、<code>「…」</code>、<code>- …</code>、一行一句、
|
|
74
|
+
JSON 数组 <code>[{"user":"…","assistant":"…"}]</code>。<br />
|
|
75
|
+
说话人写对才分得清谁说的:他/我/用户 算你,你/她/助手 算她;认不出的一律当她说的。
|
|
76
|
+
</div>
|
|
77
|
+
</div>
|
|
78
|
+
|
|
79
|
+
<div class="col box">
|
|
80
|
+
<h2>解析出来长这样</h2>
|
|
81
|
+
<div class="stat" id="stat">还没有内容</div>
|
|
82
|
+
<div id="preview"></div>
|
|
83
|
+
<div class="tips" id="saved"></div>
|
|
84
|
+
</div>
|
|
85
|
+
</div>
|
|
86
|
+
|
|
87
|
+
<script>
|
|
88
|
+
(function () {
|
|
89
|
+
var $ = function (id) { return document.getElementById(id); };
|
|
90
|
+
var modelId = '';
|
|
91
|
+
var parsed = { lines: [], pairs: [] };
|
|
92
|
+
var stored = { lines: [], pairs: [] };
|
|
93
|
+
var files = [];
|
|
94
|
+
|
|
95
|
+
var USERS = /^(他|我|用户|user|User|USER|问|Q)$/;
|
|
96
|
+
var HERS = /^(你|她|它|助手|bot|Bot|BOT|答|A|B)$/;
|
|
97
|
+
var SPEAK = /^\s*([^\s::]{1,6})\s*[::]\s*(.+)$/;
|
|
98
|
+
|
|
99
|
+
function unquote(s) {
|
|
100
|
+
var m = String(s).trim().match(/^[「“"'']([\s\S]+)[」”"'']$/);
|
|
101
|
+
return m ? m[1].trim() : String(s).trim();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function parse(raw) {
|
|
105
|
+
var lines = [], pairs = [];
|
|
106
|
+
var pending = null;
|
|
107
|
+
var rows = String(raw || '').split(/\r?\n/);
|
|
108
|
+
for (var i = 0; i < rows.length; i += 1) {
|
|
109
|
+
var line = rows[i].trim();
|
|
110
|
+
if (!line) continue;
|
|
111
|
+
|
|
112
|
+
if (line.charAt(0) === '{' || line.charAt(0) === '[') {
|
|
113
|
+
try {
|
|
114
|
+
var o = JSON.parse(line);
|
|
115
|
+
var arr = Array.isArray(o) ? o : [o];
|
|
116
|
+
var used = false;
|
|
117
|
+
for (var k = 0; k < arr.length; k += 1) {
|
|
118
|
+
var it = arr[k] || {};
|
|
119
|
+
var a = it.assistant || it.a || it.answer || it.reply || it.response || it.text || '';
|
|
120
|
+
var u = it.user || it.q || it.question || it.prompt || it.input || '';
|
|
121
|
+
if (a) {
|
|
122
|
+
if (u) pairs.push({ user: String(u), assistant: String(a) });
|
|
123
|
+
else lines.push(String(a));
|
|
124
|
+
used = true;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
if (used) continue;
|
|
128
|
+
} catch (e) { /* 不是 JSON,按普通行处理 */ }
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
line = line.replace(/^[-*•·]\s*/, '');
|
|
132
|
+
var m = line.match(SPEAK);
|
|
133
|
+
if (m) {
|
|
134
|
+
var who = m[1].trim();
|
|
135
|
+
var said = unquote(m[2]);
|
|
136
|
+
if (!said) continue;
|
|
137
|
+
if (USERS.test(who)) { pending = said; continue; }
|
|
138
|
+
if (HERS.test(who) || true) {
|
|
139
|
+
pairs.push({ user: pending || '', assistant: said });
|
|
140
|
+
pending = null;
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
var t = unquote(line);
|
|
145
|
+
if (t) lines.push(t);
|
|
146
|
+
}
|
|
147
|
+
return { lines: lines, pairs: pairs };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function render() {
|
|
151
|
+
var total = parsed.lines.length + parsed.pairs.length;
|
|
152
|
+
$('stat').innerHTML = total
|
|
153
|
+
? '读到 <b>' + parsed.pairs.length + '</b> 组问答、<b>' + parsed.lines.length + '</b> 句台词'
|
|
154
|
+
: '还没有内容';
|
|
155
|
+
var box = $('preview');
|
|
156
|
+
box.innerHTML = '';
|
|
157
|
+
var show = [];
|
|
158
|
+
for (var i = 0; i < parsed.pairs.length && show.length < 8; i += 1) {
|
|
159
|
+
show.push('他:' + parsed.pairs[i].user + '<br>你:' + parsed.pairs[i].assistant);
|
|
160
|
+
}
|
|
161
|
+
for (var j = 0; j < parsed.lines.length && show.length < 14; j += 1) {
|
|
162
|
+
show.push('你:' + parsed.lines[j]);
|
|
163
|
+
}
|
|
164
|
+
show.forEach(function (html) {
|
|
165
|
+
var d = document.createElement('div');
|
|
166
|
+
d.innerHTML = html.replace(/</g, '<').replace(/<br>/g, '<br>');
|
|
167
|
+
box.appendChild(d);
|
|
168
|
+
});
|
|
169
|
+
var kept = stored.lines.length + stored.pairs.length;
|
|
170
|
+
$('saved').textContent = kept ? '已经存着 ' + kept + ' 条(' + stored.pairs.length + ' 组问答、' + stored.lines.length + ' 句台词)' : '';
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function reparse() {
|
|
174
|
+
var all = $('raw').value;
|
|
175
|
+
for (var i = 0; i < files.length; i += 1) all += '\n' + files[i];
|
|
176
|
+
parsed = parse(all);
|
|
177
|
+
render();
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function load() {
|
|
181
|
+
if (!modelId) return;
|
|
182
|
+
$('state').textContent = '读…';
|
|
183
|
+
fetch('/dsh-live2d/api/corpus?id=' + encodeURIComponent(modelId))
|
|
184
|
+
.then(function (r) { return r.json(); })
|
|
185
|
+
.then(function (d) {
|
|
186
|
+
stored = { lines: d.lines || [], pairs: d.pairs || [] };
|
|
187
|
+
$('state').textContent = '读好了';
|
|
188
|
+
render();
|
|
189
|
+
})
|
|
190
|
+
.catch(function (e) { $('state').textContent = '读不了:' + e.message; });
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function save(mode) {
|
|
194
|
+
if (!modelId) return;
|
|
195
|
+
var payload;
|
|
196
|
+
if (mode === 'replace') {
|
|
197
|
+
payload = parsed;
|
|
198
|
+
} else {
|
|
199
|
+
payload = {
|
|
200
|
+
lines: stored.lines.concat(parsed.lines),
|
|
201
|
+
pairs: stored.pairs.concat(parsed.pairs)
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
if (!payload.lines.length && !payload.pairs.length) {
|
|
205
|
+
$('state').textContent = '没有可存的内容';
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
$('state').textContent = '存…';
|
|
209
|
+
fetch('/dsh-live2d/api/corpus', {
|
|
210
|
+
method: 'POST',
|
|
211
|
+
headers: { 'content-type': 'application/json' },
|
|
212
|
+
body: JSON.stringify({ id: modelId, lines: payload.lines, pairs: payload.pairs })
|
|
213
|
+
})
|
|
214
|
+
.then(function (r) { return r.json(); })
|
|
215
|
+
.then(function (d) {
|
|
216
|
+
if (!d.ok) { $('state').textContent = '没存上:' + (d.error || '?'); return; }
|
|
217
|
+
$('state').textContent = '存好了,共 ' + d.total + ' 条';
|
|
218
|
+
$('raw').value = '';
|
|
219
|
+
files = [];
|
|
220
|
+
load();
|
|
221
|
+
})
|
|
222
|
+
.catch(function (e) { $('state').textContent = '没存上:' + e.message; });
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
$('raw').addEventListener('input', reparse);
|
|
226
|
+
$('pick').addEventListener('click', function () { $('files').click(); });
|
|
227
|
+
$('drop').addEventListener('click', function () { $('files').click(); });
|
|
228
|
+
$('append').addEventListener('click', function () { save('append'); });
|
|
229
|
+
$('replace').addEventListener('click', function () { save('replace'); });
|
|
230
|
+
|
|
231
|
+
$('files').addEventListener('change', function (e) {
|
|
232
|
+
var list = Array.prototype.slice.call(e.target.files || []);
|
|
233
|
+
readFiles(list);
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
function readFiles(list) {
|
|
237
|
+
if (!list.length) return;
|
|
238
|
+
$('state').textContent = '读文件…';
|
|
239
|
+
var left = list.length;
|
|
240
|
+
list.forEach(function (f) {
|
|
241
|
+
var reader = new FileReader();
|
|
242
|
+
reader.onload = function () {
|
|
243
|
+
files.push(String(reader.result || ''));
|
|
244
|
+
left -= 1;
|
|
245
|
+
if (left === 0) { $('state').textContent = '读好了 ' + files.length + ' 个文件'; reparse(); }
|
|
246
|
+
};
|
|
247
|
+
reader.onerror = function () { left -= 1; if (left === 0) reparse(); };
|
|
248
|
+
reader.readAsText(f, 'utf-8');
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
var drop = $('drop');
|
|
253
|
+
['dragenter', 'dragover'].forEach(function (ev) {
|
|
254
|
+
drop.addEventListener(ev, function (e) { e.preventDefault(); drop.classList.add('hot'); });
|
|
255
|
+
});
|
|
256
|
+
['dragleave', 'drop'].forEach(function (ev) {
|
|
257
|
+
drop.addEventListener(ev, function (e) { e.preventDefault(); drop.classList.remove('hot'); });
|
|
258
|
+
});
|
|
259
|
+
drop.addEventListener('drop', function (e) {
|
|
260
|
+
var dt = e.dataTransfer;
|
|
261
|
+
if (dt && dt.files && dt.files.length) readFiles(Array.prototype.slice.call(dt.files));
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
fetch('/dsh-live2d/api/models')
|
|
265
|
+
.then(function (r) { return r.json(); })
|
|
266
|
+
.then(function (d) {
|
|
267
|
+
var list = d.models || [];
|
|
268
|
+
var sel = $('models');
|
|
269
|
+
sel.innerHTML = '';
|
|
270
|
+
list.forEach(function (m) {
|
|
271
|
+
var o = document.createElement('option');
|
|
272
|
+
o.value = m.id;
|
|
273
|
+
o.textContent = (m.name || m.id) + (m.id === d.selected ? '(正在显示)' : '');
|
|
274
|
+
sel.appendChild(o);
|
|
275
|
+
});
|
|
276
|
+
modelId = d.selected || (list[0] && list[0].id) || '';
|
|
277
|
+
if (modelId) sel.value = modelId;
|
|
278
|
+
if (!list.length) { $('state').textContent = '还没有模型'; return; }
|
|
279
|
+
load();
|
|
280
|
+
})
|
|
281
|
+
.catch(function (e) { $('state').textContent = '读不了模型表:' + e.message; });
|
|
282
|
+
|
|
283
|
+
$('models').addEventListener('change', function (e) {
|
|
284
|
+
modelId = e.target.value;
|
|
285
|
+
load();
|
|
286
|
+
});
|
|
287
|
+
})();
|
|
288
|
+
</script>
|
|
289
|
+
</body>
|
|
290
|
+
</html>
|
package/assets/viewer.html
CHANGED
package/assets/viewer.js
CHANGED
|
@@ -44693,6 +44693,15 @@ void main(void)\r
|
|
|
44693
44693
|
});
|
|
44694
44694
|
window.__live2d = { say, mood: applyMood, refresh };
|
|
44695
44695
|
$2("import").onclick = () => $2("files").click();
|
|
44696
|
+
var corpusBtn = $2("corpus");
|
|
44697
|
+
if (corpusBtn) {
|
|
44698
|
+
corpusBtn.onclick = () => {
|
|
44699
|
+
try {
|
|
44700
|
+
window.parent.postMessage({ type: "dsh-live2d/open", sheet: "corpus" }, "*");
|
|
44701
|
+
} catch {
|
|
44702
|
+
}
|
|
44703
|
+
};
|
|
44704
|
+
}
|
|
44696
44705
|
$2("close").onclick = () => $2("drawer").classList.remove("show");
|
|
44697
44706
|
$2("files").onchange = (e2) => importFolder(e2.target.files);
|
|
44698
44707
|
function moodValue(id) {
|
package/lib/client.js
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
const BASE = '/dsh-live2d';
|
|
9
9
|
const BGS = ['none', 'dark', 'light', 'paper'];
|
|
10
10
|
const BG_LABEL = { none: '透明', dark: '深色', light: '浅色', paper: '纸质' };
|
|
11
|
+
const SHEET_TITLE = { tone: '音色', persona: '人设卡', corpus: '语料' };
|
|
11
12
|
|
|
12
13
|
const CSS = `
|
|
13
14
|
.dsh-l2d-card{position:fixed;right:18px;bottom:18px;z-index:9000;border-radius:16px;overflow:hidden;
|
|
@@ -47,6 +48,16 @@
|
|
|
47
48
|
transition:opacity .15s ease,background .15s ease;border-radius:16px 0 8px 0;z-index:2;
|
|
48
49
|
background:repeating-linear-gradient(135deg,transparent 0 2px,rgba(255,255,255,.45) 2px 3px) no-repeat left top/10px 10px}
|
|
49
50
|
.dsh-l2d-grip:hover{opacity:1;background-color:rgba(255,255,255,.08)}
|
|
51
|
+
.dsh-l2d-sheet{position:fixed;inset:0;z-index:9500;background:rgba(10,12,16,.74);
|
|
52
|
+
backdrop-filter:blur(4px);display:flex;flex-direction:column;padding:18px 18px 22px}
|
|
53
|
+
.dsh-l2d-sheet-bar{display:flex;align-items:center;gap:10px;padding:0 0 10px;
|
|
54
|
+
color:#cfd6e2;font-size:12px;letter-spacing:.5px}
|
|
55
|
+
.dsh-l2d-sheet-bar span{margin-right:auto}
|
|
56
|
+
.dsh-l2d-sheet-bar button{background:rgba(255,255,255,.08);border:1px solid rgba(255,255,255,.13);
|
|
57
|
+
border-radius:8px;color:#dfe4ec;font:inherit;font-size:11px;padding:5px 12px;cursor:pointer}
|
|
58
|
+
.dsh-l2d-sheet-bar button:hover{background:rgba(255,255,255,.16)}
|
|
59
|
+
.dsh-l2d-sheet-body{flex:1;border:0;border-radius:14px;background:#12141a;
|
|
60
|
+
box-shadow:0 24px 60px rgba(0,0,0,.6)}
|
|
50
61
|
.dsh-l2d-card.resizing{cursor:nwse-resize;box-shadow:0 0 0 1px rgba(106,169,255,.45),0 22px 58px rgba(0,0,0,.6)}
|
|
51
62
|
.dsh-l2d-card.moving{cursor:move;box-shadow:0 0 0 1px rgba(106,169,255,.35),0 22px 58px rgba(0,0,0,.6)}
|
|
52
63
|
`;
|
|
@@ -85,6 +96,15 @@
|
|
|
85
96
|
const [pos, setPos] = react.useState(readPos);
|
|
86
97
|
const [draft, setDraft] = react.useState('');
|
|
87
98
|
const [busy, setBusy] = react.useState(false);
|
|
99
|
+
const [sheet, setSheet] = react.useState(null);
|
|
100
|
+
react.useEffect(() => {
|
|
101
|
+
const onMsg = (e) => {
|
|
102
|
+
const d = e.data || {};
|
|
103
|
+
if (d.type === 'dsh-live2d/open' && d.sheet) setSheet(String(d.sheet));
|
|
104
|
+
};
|
|
105
|
+
window.addEventListener('message', onMsg);
|
|
106
|
+
return () => window.removeEventListener('message', onMsg);
|
|
107
|
+
}, []);
|
|
88
108
|
const [nonce, setNonce] = react.useState(0);
|
|
89
109
|
const ref = react.useRef(null);
|
|
90
110
|
|
|
@@ -204,8 +224,8 @@
|
|
|
204
224
|
const noDrag = (e) => e.stopPropagation();
|
|
205
225
|
|
|
206
226
|
const manage = () => ref.current && ref.current.contentWindow.postMessage({ type: 'dsh-live2d/manage' }, '*');
|
|
207
|
-
const openTone = () =>
|
|
208
|
-
const openPersona = () =>
|
|
227
|
+
const openTone = () => setSheet('tone');
|
|
228
|
+
const openPersona = () => setSheet('persona');
|
|
209
229
|
|
|
210
230
|
const send = () => {
|
|
211
231
|
const t = draft.trim();
|
|
@@ -237,7 +257,21 @@
|
|
|
237
257
|
? { width: size.w + 'px', height: size.h + 'px', transform: 'translate(' + pos.x + 'px,' + pos.y + 'px)' }
|
|
238
258
|
: { transform: 'translate(' + pos.x + 'px,' + pos.y + 'px)' };
|
|
239
259
|
|
|
240
|
-
|
|
260
|
+
const overlay = sheet
|
|
261
|
+
? h(
|
|
262
|
+
'div',
|
|
263
|
+
{ className: 'dsh-l2d-sheet', onPointerDown: noDrag },
|
|
264
|
+
h(
|
|
265
|
+
'div',
|
|
266
|
+
{ className: 'dsh-l2d-sheet-bar' },
|
|
267
|
+
h('span', null, SHEET_TITLE[sheet] || sheet),
|
|
268
|
+
h('button', { onClick: () => setSheet(null) }, '关掉'),
|
|
269
|
+
),
|
|
270
|
+
h('iframe', { className: 'dsh-l2d-sheet-body', src: BASE + '/' + sheet }),
|
|
271
|
+
)
|
|
272
|
+
: null;
|
|
273
|
+
|
|
274
|
+
const card = h(
|
|
241
275
|
'div',
|
|
242
276
|
{ className: 'dsh-l2d-card' + (open ? '' : ' collapsed'), style: cardStyle },
|
|
243
277
|
h(
|
|
@@ -287,6 +321,8 @@
|
|
|
287
321
|
: null,
|
|
288
322
|
open ? h('div', { className: 'dsh-l2d-grip', onPointerDown: startDrag, title: '拖动改大小' }) : null,
|
|
289
323
|
);
|
|
324
|
+
|
|
325
|
+
return h(react.Fragment, null, card, overlay);
|
|
290
326
|
}
|
|
291
327
|
|
|
292
328
|
let frameWin = null;
|
package/lib/index.js
CHANGED
|
@@ -156,22 +156,79 @@ function guessMood(text) {
|
|
|
156
156
|
/** 默认音色:一个字一声短音 */
|
|
157
157
|
const DEFAULT_VOICE = { preset: 'soft', type: 'triangle', base: 400, jit: 10, dur: 60, vol: 32, spd: 62 };
|
|
158
158
|
|
|
159
|
-
/**
|
|
159
|
+
/** 没写人设卡时用的通用版,不绑定具体角色 */
|
|
160
160
|
const DEFAULT_PERSONA = [
|
|
161
|
-
'
|
|
162
|
-
'
|
|
163
|
-
'
|
|
161
|
+
'你叫{name}。',
|
|
162
|
+
'说话短,一句常常不到十五个字;不用感叹号,不写书面语,不解释心情。',
|
|
163
|
+
'情绪落在具体的东西上——桌上有什么、手上在做什么、外面天色怎样。',
|
|
164
|
+
'不总结,不升华,不写括号里的动作。',
|
|
164
165
|
].join('\n');
|
|
165
166
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
'
|
|
169
|
-
'
|
|
167
|
+
/** 参考密度:只给节奏和信息量,不是让模型照抄内容 */
|
|
168
|
+
const TALK_SAMPLES = [
|
|
169
|
+
'他:今天吃啥 —— 你:锅里还有,热的。',
|
|
170
|
+
'他:累死了 —— 你:先坐下。水在桌上。',
|
|
171
|
+
'他:你在干嘛 —— 你:擦桌子。手是湿的。',
|
|
172
|
+
'他:我想你了 —— 你:嗯。我也是。',
|
|
173
|
+
'他:明天要早起 —— 你:那我今晚早点关灯。',
|
|
170
174
|
].join('\n');
|
|
171
175
|
|
|
176
|
+
/** 语料:lines = 她的台词,pairs = 一问一答 */
|
|
177
|
+
function readCorpus(id) {
|
|
178
|
+
try {
|
|
179
|
+
const d = JSON.parse(readFileSync(join(MODELS, id, 'corpus.json'), 'utf8'));
|
|
180
|
+
return {
|
|
181
|
+
lines: Array.isArray(d.lines) ? d.lines.filter((x) => typeof x === 'string' && x.trim()).slice(0, 500) : [],
|
|
182
|
+
pairs: Array.isArray(d.pairs)
|
|
183
|
+
? d.pairs
|
|
184
|
+
.filter((p) => p && typeof p.assistant === 'string' && p.assistant.trim())
|
|
185
|
+
.slice(0, 500)
|
|
186
|
+
.map((p) => ({ user: String(p.user || '').slice(0, 200), assistant: String(p.assistant).slice(0, 400) }))
|
|
187
|
+
: [],
|
|
188
|
+
};
|
|
189
|
+
} catch {
|
|
190
|
+
return { lines: [], pairs: [] };
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function writeCorpus(id, corpus) {
|
|
195
|
+
try {
|
|
196
|
+
writeFileSync(
|
|
197
|
+
join(MODELS, id, 'corpus.json'),
|
|
198
|
+
JSON.stringify({ lines: corpus.lines.slice(0, 500), pairs: corpus.pairs.slice(0, 500) }, null, 2),
|
|
199
|
+
'utf8',
|
|
200
|
+
);
|
|
201
|
+
} catch {}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/** 从语料里挑几条最贴当前对话的,拼成样例 */
|
|
205
|
+
function pickCorpusSamples(corpus, text, toneN = 4) {
|
|
206
|
+
const out = [];
|
|
207
|
+
const chars = new Set(String(text || ''));
|
|
208
|
+
const scored = (corpus.pairs || [])
|
|
209
|
+
.map((p) => {
|
|
210
|
+
let hit = 0;
|
|
211
|
+
for (const ch of String(p.user || '')) if (chars.has(ch)) hit += 1;
|
|
212
|
+
return { p, hit };
|
|
213
|
+
})
|
|
214
|
+
.sort((a, b) => b.hit - a.hit);
|
|
215
|
+
for (const s of scored.slice(0, 3)) {
|
|
216
|
+
if (s.p.user) out.push('他:' + s.p.user + ' —— 你:' + s.p.assistant);
|
|
217
|
+
else out.push('你:' + s.p.assistant);
|
|
218
|
+
}
|
|
219
|
+
const pool = (corpus.lines || []).slice();
|
|
220
|
+
for (let i = 0; i < toneN && pool.length; i += 1) {
|
|
221
|
+
const k = Math.floor(Math.random() * pool.length);
|
|
222
|
+
out.push('你:' + pool.splice(k, 1)[0]);
|
|
223
|
+
}
|
|
224
|
+
return out;
|
|
225
|
+
}
|
|
226
|
+
|
|
172
227
|
function personaOf(meta) {
|
|
173
228
|
const p = typeof meta?.persona === 'string' ? meta.persona.trim() : '';
|
|
174
|
-
|
|
229
|
+
if (p) return p;
|
|
230
|
+
const nm = String(meta?.name || '她').trim() || '她';
|
|
231
|
+
return DEFAULT_PERSONA.replace('{name}', nm);
|
|
175
232
|
}
|
|
176
233
|
|
|
177
234
|
function readChatStore(id) {
|
|
@@ -283,6 +340,10 @@ function humanize(input) {
|
|
|
283
340
|
.trim();
|
|
284
341
|
const parts = t.split(/(?<=[。!?!?~])/).filter((s) => s.trim());
|
|
285
342
|
if (parts.length > 3) t = parts.slice(0, 3).join('');
|
|
343
|
+
if (t.length > 60) {
|
|
344
|
+
const cut = t.match(/^[\s\S]{0,60}?[。!?!?]/);
|
|
345
|
+
t = cut ? cut[0] : t.slice(0, 60);
|
|
346
|
+
}
|
|
286
347
|
return t.trim();
|
|
287
348
|
}
|
|
288
349
|
|
|
@@ -360,8 +421,9 @@ async function maybeTalk(ctx) {
|
|
|
360
421
|
personaOf(meta),
|
|
361
422
|
'现在是 ' + nowText() + '。',
|
|
362
423
|
hist ? '你们之前说的:\n' + hist : '',
|
|
363
|
-
'
|
|
364
|
-
'
|
|
424
|
+
'有一阵子没人说话了。你想开口,扔一句过去——提一件你正在做的事、问一句、或者吐槽一下都行。',
|
|
425
|
+
'一到两句,四十字以内;具体,别总结,别讲道理。只输出这句话本身,不要引号,不要旁白。',
|
|
426
|
+
'密度参考(别照抄):\n' + TALK_SAMPLES,
|
|
365
427
|
]
|
|
366
428
|
.filter(Boolean)
|
|
367
429
|
.join('\n\n');
|
|
@@ -576,6 +638,7 @@ async function handle(req, res, ctx) {
|
|
|
576
638
|
} catch {}
|
|
577
639
|
}
|
|
578
640
|
const facts = st.memory ? pickFacts(store.facts, text) : [];
|
|
641
|
+
const samples = st.memory ? pickCorpusSamples(readCorpus(id), text) : [];
|
|
579
642
|
const hist = store.messages;
|
|
580
643
|
const lines = hist
|
|
581
644
|
.slice(-8)
|
|
@@ -584,14 +647,17 @@ async function handle(req, res, ctx) {
|
|
|
584
647
|
const prompt = [
|
|
585
648
|
personaOf(meta),
|
|
586
649
|
meta.isolation === false ? '' : ISOLATION,
|
|
587
|
-
'现在是 ' + nowText() + '
|
|
650
|
+
'现在是 ' + nowText() + '。',
|
|
588
651
|
store.summary ? '之前聊过的,你还记得:\n' + store.summary : '',
|
|
589
652
|
facts.length ? '关于他,你记得这些:\n' + facts.map((f) => '- ' + f).join('\n') : '',
|
|
590
653
|
lines ? '你们之前说的:\n' + lines : '',
|
|
591
654
|
'他说:' + text,
|
|
592
|
-
'
|
|
593
|
-
|
|
594
|
-
'
|
|
655
|
+
'照上面这个人说话,回他一句。',
|
|
656
|
+
'一到两句,四十字以内。要具体:答应、拒绝、反问、吐槽、提一件正在做的事,都行。' +
|
|
657
|
+
'别总结他的话,别讲道理,别说“我理解你”“希望帮到你”“还需要什么”;他没问就别建议。' +
|
|
658
|
+
'可以不接他的话,扔一句自己的事;允许只回两个字。',
|
|
659
|
+
'只输出这句话本身:不要引号,不要旁白,不要括号里的动作。',
|
|
660
|
+
'往下是这个人的说话密度,只学节奏和信息量,别照抄内容:\n' + TALK_SAMPLES,
|
|
595
661
|
st.choices ? '如果你想让他挑,可以在最后另起一行写 ||选项一|选项二,最多三项。' : '',
|
|
596
662
|
st.memesMode === 'own' || st.memesMode === 'both'
|
|
597
663
|
? '如果你想附一张贴图,就在最后另起一行写 ||图:名字。能挑的名字有:' +
|
|
@@ -706,6 +772,11 @@ async function handle(req, res, ctx) {
|
|
|
706
772
|
return sendFile(res, join(ASSETS, 'mobile.html'), MIME['.html']);
|
|
707
773
|
}
|
|
708
774
|
|
|
775
|
+
// 语料页
|
|
776
|
+
if (path === '/corpus' || path === '/corpus.html') {
|
|
777
|
+
return sendFile(res, join(ASSETS, 'corpus.html'), MIME['.html']);
|
|
778
|
+
}
|
|
779
|
+
|
|
709
780
|
// 人设卡页
|
|
710
781
|
if (path === '/persona' || path === '/persona.html') {
|
|
711
782
|
return sendFile(res, join(ASSETS, 'persona.html'), MIME['.html']);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-live2dwaifu",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "给 DSH 网页右下角请一个 Live2D 角色:会眨眼、会呼吸、视线跟着鼠标、点她有反应、说话时嘴动、心情变了眉毛跟着动。模型不用内置——谁装谁放自己的文件夹;支持自定义 .exp3.json 表情和 .motion3.json 动作,卡片能拖能缩,背景可换。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|