dsh-live2dwaifu 1.0.6 → 1.2.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 CHANGED
@@ -145,6 +145,40 @@ dsh plugin --profile desktop add dsh-live2dwaifu
145
145
 
146
146
  ---
147
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
+
148
182
  ## 声音
149
183
 
150
184
  她说话时一个字一声很短的电音,标点停一拍,句尾沉一下。不是朗读,是“哔哔”那种。
@@ -342,6 +376,60 @@ curl -X POST http://127.0.0.1:<端口>/dsh-live2d/api/push \
342
376
 
343
377
  ---
344
378
 
379
+ ## 更新日志
380
+
381
+ 版本号怎么走:动了功能和结构就抬第二位(1.2.0),只补洞和小修就抬第三位(1.2.1)。
382
+
383
+ ### 1.2.0
384
+
385
+ - 语料吃的文件多了:`.md`、`.csv`、`.srt`、`.vtt`、`.ass`。字幕会自动去掉序号、时间轴和样式标签。
386
+ - 拖进图片或视频会告诉你怎么办,不再默默没反应:图片留着当形象参考,视频请先导出字幕,或者把台词复制进来。
387
+ - 语料页的文案改成不预设角色,原来的「他/你」换成「对方/角色」。
388
+
389
+ ### 1.1.0
390
+
391
+ - 加了语料功能:粘贴、选文件、拖拽三种喂法。解析结果实时预览,能看见条数和前十几条。
392
+ - 存的语料会挑几条贴进提示词,优先挑跟当前这句话沾边的问答。
393
+
394
+ ### 1.0.6
395
+
396
+ - 音色和人设卡改成在卡片里盖一层打开。原来点按钮开新窗口,桌面端会拦掉,看起来像没反应。
397
+ - 对话加了说话样例,删掉一句会让她自说自话的提示(“想想你此刻在做什么”)。
398
+ - 回的句子硬卡在六十个字以内。
399
+ - 没写人设时不再套用若妍那套词,换成只带模型名字的通用版。
400
+
401
+ ### 1.0.5
402
+
403
+ - 拖窗口改成用一层全屏遮罩接指针,修「窗口拖不动」。
404
+
405
+ ### 1.0.4
406
+
407
+ - README 加了反馈邮箱。
408
+
409
+ ### 1.0.3
410
+
411
+ - 设置项换成直白说法,每项加了鼠标悬停说明。
412
+
413
+ ### 1.0.2
414
+
415
+ - 缩放手柄挪到左上角,拖的时候不再粘手。
416
+ - 拖动加了边界,拖出屏幕也拉得回来。
417
+
418
+ ### 1.0.1
419
+
420
+ - 去掉通用短答兜底。选项只由角色自己决定给不给。
421
+
422
+ ### 1.0.0
423
+
424
+ - 界面重做一轮。
425
+ - README 重写成说明书。
426
+ - 自带二十张 Q 版贴纸,可以导给 dsh-meme 用。
427
+ - 记忆分两层:最近二十来句原文,加上关于对方的一串事实。
428
+ - 去 AI 味:删套话、卡长度、允许只回两个字。
429
+ - 手机页 `/dsh-live2d/chat`。
430
+ - 人设卡编辑页、音色调色页。
431
+ - 对外接口:`/api/chat`、`/api/push`、`/api/poll`。
432
+
345
433
  ## 反馈
346
434
 
347
435
  遇到问题、想要新功能、觉得哪句话还是像 AI,都可以写信到 **FujiwaraReisen@outlook.com**。
@@ -0,0 +1,314 @@
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">拖文件进来,或者点一下选(可以多选)<br /><span style="font-size:11px">文本、字幕、表格都认;图片和视频见下面</span></div>
71
+ <input id="files" type="file" multiple accept=".txt,.json,.jsonl,.csv,.md,.srt,.vtt,.ass,image/*,video/*" style="display:none" />
72
+ <div class="tips">
73
+ 认得的写法:<code>对方:… / 角色:…</code>、<code>「…」</code>、<code>- …</code>、一行一句、
74
+ JSON 数组、字幕文件 <code>.srt / .vtt / .ass</code>(会自动去掉时间轴)。<br />
75
+ 说话人对得上才分得清是谁:他/我/用户/对方 算对方,你/她/角色/助手 算角色;认不出来的一律算角色说的。<br />
76
+ 图片和视频:暂时不进语料。图片可以留着当形象参考,视频建议先导出字幕文件,或者把台词复制进来。
77
+ </div>
78
+ </div>
79
+
80
+ <div class="col box">
81
+ <h2>解析出来长这样</h2>
82
+ <div class="stat" id="stat">还没有内容</div>
83
+ <div id="preview"></div>
84
+ <div class="tips" id="saved"></div>
85
+ </div>
86
+ </div>
87
+
88
+ <script>
89
+ (function () {
90
+ var $ = function (id) { return document.getElementById(id); };
91
+ var modelId = '';
92
+ var parsed = { lines: [], pairs: [] };
93
+ var stored = { lines: [], pairs: [] };
94
+ var files = [];
95
+
96
+ var USERS = /^(他|我|用户|user|User|USER|问|Q)$/;
97
+ var HERS = /^(你|她|它|助手|bot|Bot|BOT|答|A|B)$/;
98
+ var SPEAK = /^\s*([^\s::]{1,6})\s*[::]\s*(.+)$/;
99
+
100
+ function unquote(s) {
101
+ var m = String(s).trim().match(/^[「“"'']([\s\S]+)[」”"'']$/);
102
+ return m ? m[1].trim() : String(s).trim();
103
+ }
104
+
105
+ function parse(raw) {
106
+ var lines = [], pairs = [];
107
+ var pending = null;
108
+ var rows = String(raw || '').split(/\r?\n/);
109
+ for (var i = 0; i < rows.length; i += 1) {
110
+ var line = rows[i].trim();
111
+ if (!line) continue;
112
+
113
+ if (line.charAt(0) === '{' || line.charAt(0) === '[') {
114
+ try {
115
+ var o = JSON.parse(line);
116
+ var arr = Array.isArray(o) ? o : [o];
117
+ var used = false;
118
+ for (var k = 0; k < arr.length; k += 1) {
119
+ var it = arr[k] || {};
120
+ var a = it.assistant || it.a || it.answer || it.reply || it.response || it.text || '';
121
+ var u = it.user || it.q || it.question || it.prompt || it.input || '';
122
+ if (a) {
123
+ if (u) pairs.push({ user: String(u), assistant: String(a) });
124
+ else lines.push(String(a));
125
+ used = true;
126
+ }
127
+ }
128
+ if (used) continue;
129
+ } catch (e) { /* 不是 JSON,按普通行处理 */ }
130
+ }
131
+
132
+ // 字幕文件:跳过序号、时间轴、文件头
133
+ if (/^\d+$/.test(line)) continue;
134
+ if (/-->/.test(line)) continue;
135
+ if (/^(WEBVTT|NOTE|STYLE|REGION)/i.test(line)) continue;
136
+ line = line.replace(/^[-*•·]\s*/, '');
137
+ line = line.replace(/<[^>]+>/g, '');
138
+ var m = line.match(SPEAK);
139
+ if (m) {
140
+ var who = m[1].trim();
141
+ var said = unquote(m[2]);
142
+ if (!said) continue;
143
+ if (USERS.test(who)) { pending = said; continue; }
144
+ if (HERS.test(who) || true) {
145
+ pairs.push({ user: pending || '', assistant: said });
146
+ pending = null;
147
+ continue;
148
+ }
149
+ }
150
+ var t = unquote(line);
151
+ if (t) lines.push(t);
152
+ }
153
+ return { lines: lines, pairs: pairs };
154
+ }
155
+
156
+ function render() {
157
+ var total = parsed.lines.length + parsed.pairs.length;
158
+ $('stat').innerHTML = total
159
+ ? '读到 <b>' + parsed.pairs.length + '</b> 组问答、<b>' + parsed.lines.length + '</b> 句台词'
160
+ : '还没有内容';
161
+ var box = $('preview');
162
+ box.innerHTML = '';
163
+ var show = [];
164
+ for (var i = 0; i < parsed.pairs.length && show.length < 8; i += 1) {
165
+ show.push('他:' + parsed.pairs[i].user + '<br>你:' + parsed.pairs[i].assistant);
166
+ }
167
+ for (var j = 0; j < parsed.lines.length && show.length < 14; j += 1) {
168
+ show.push('你:' + parsed.lines[j]);
169
+ }
170
+ show.forEach(function (html) {
171
+ var d = document.createElement('div');
172
+ d.innerHTML = html.replace(/</g, '&lt;').replace(/&lt;br&gt;/g, '<br>');
173
+ box.appendChild(d);
174
+ });
175
+ var kept = stored.lines.length + stored.pairs.length;
176
+ $('saved').textContent = kept ? '已经存着 ' + kept + ' 条(' + stored.pairs.length + ' 组问答、' + stored.lines.length + ' 句台词)' : '';
177
+ }
178
+
179
+ function reparse() {
180
+ var all = $('raw').value;
181
+ for (var i = 0; i < files.length; i += 1) all += '\n' + files[i];
182
+ parsed = parse(all);
183
+ render();
184
+ }
185
+
186
+ function load() {
187
+ if (!modelId) return;
188
+ $('state').textContent = '读…';
189
+ fetch('/dsh-live2d/api/corpus?id=' + encodeURIComponent(modelId))
190
+ .then(function (r) { return r.json(); })
191
+ .then(function (d) {
192
+ stored = { lines: d.lines || [], pairs: d.pairs || [] };
193
+ $('state').textContent = '读好了';
194
+ render();
195
+ })
196
+ .catch(function (e) { $('state').textContent = '读不了:' + e.message; });
197
+ }
198
+
199
+ function save(mode) {
200
+ if (!modelId) return;
201
+ var payload;
202
+ if (mode === 'replace') {
203
+ payload = parsed;
204
+ } else {
205
+ payload = {
206
+ lines: stored.lines.concat(parsed.lines),
207
+ pairs: stored.pairs.concat(parsed.pairs)
208
+ };
209
+ }
210
+ if (!payload.lines.length && !payload.pairs.length) {
211
+ $('state').textContent = '没有可存的内容';
212
+ return;
213
+ }
214
+ $('state').textContent = '存…';
215
+ fetch('/dsh-live2d/api/corpus', {
216
+ method: 'POST',
217
+ headers: { 'content-type': 'application/json' },
218
+ body: JSON.stringify({ id: modelId, lines: payload.lines, pairs: payload.pairs })
219
+ })
220
+ .then(function (r) { return r.json(); })
221
+ .then(function (d) {
222
+ if (!d.ok) { $('state').textContent = '没存上:' + (d.error || '?'); return; }
223
+ $('state').textContent = '存好了,共 ' + d.total + ' 条';
224
+ $('raw').value = '';
225
+ files = [];
226
+ load();
227
+ })
228
+ .catch(function (e) { $('state').textContent = '没存上:' + e.message; });
229
+ }
230
+
231
+ $('raw').addEventListener('input', reparse);
232
+ $('pick').addEventListener('click', function () { $('files').click(); });
233
+ $('drop').addEventListener('click', function () { $('files').click(); });
234
+ $('append').addEventListener('click', function () { save('append'); });
235
+ $('replace').addEventListener('click', function () { save('replace'); });
236
+
237
+ $('files').addEventListener('change', function (e) {
238
+ var list = Array.prototype.slice.call(e.target.files || []);
239
+ readFiles(list);
240
+ });
241
+
242
+ function readFiles(list) {
243
+ if (!list.length) return;
244
+ var texts = [];
245
+ var skipped = [];
246
+ list.forEach(function (f) {
247
+ var name = String(f.name || '').toLowerCase();
248
+ if (f.type.indexOf('image/') === 0 || /\.(png|jpe?g|webp|gif|bmp)$/.test(name)) {
249
+ skipped.push(f.name + '(图片)');
250
+ return;
251
+ }
252
+ if (f.type.indexOf('video/') === 0 || /\.(mp4|mkv|mov|avi|webm|flv)$/.test(name)) {
253
+ skipped.push(f.name + '(视频)');
254
+ return;
255
+ }
256
+ texts.push(f);
257
+ });
258
+ if (skipped.length) {
259
+ $('saved').textContent = '这些没读:' + skipped.join('、') + ' —— 图片先留着当形象参考,视频请导出字幕(.srt)或直接贴台词。';
260
+ }
261
+ if (!texts.length) { $('state').textContent = '没有可读的文本'; return; }
262
+ $('state').textContent = '读文件…';
263
+ var left = texts.length;
264
+ texts.forEach(function (f) {
265
+ var reader = new FileReader();
266
+ reader.onload = function () {
267
+ files.push(String(reader.result || ''));
268
+ left -= 1;
269
+ if (left === 0) { $('state').textContent = '读好了 ' + texts.length + ' 个文件'; reparse(); }
270
+ };
271
+ reader.onerror = function () { left -= 1; if (left === 0) reparse(); };
272
+ reader.readAsText(f, 'utf-8');
273
+ });
274
+ }
275
+
276
+ var drop = $('drop');
277
+ ['dragenter', 'dragover'].forEach(function (ev) {
278
+ drop.addEventListener(ev, function (e) { e.preventDefault(); drop.classList.add('hot'); });
279
+ });
280
+ ['dragleave', 'drop'].forEach(function (ev) {
281
+ drop.addEventListener(ev, function (e) { e.preventDefault(); drop.classList.remove('hot'); });
282
+ });
283
+ drop.addEventListener('drop', function (e) {
284
+ var dt = e.dataTransfer;
285
+ if (dt && dt.files && dt.files.length) readFiles(Array.prototype.slice.call(dt.files));
286
+ });
287
+
288
+ fetch('/dsh-live2d/api/models')
289
+ .then(function (r) { return r.json(); })
290
+ .then(function (d) {
291
+ var list = d.models || [];
292
+ var sel = $('models');
293
+ sel.innerHTML = '';
294
+ list.forEach(function (m) {
295
+ var o = document.createElement('option');
296
+ o.value = m.id;
297
+ o.textContent = (m.name || m.id) + (m.id === d.selected ? '(正在显示)' : '');
298
+ sel.appendChild(o);
299
+ });
300
+ modelId = d.selected || (list[0] && list[0].id) || '';
301
+ if (modelId) sel.value = modelId;
302
+ if (!list.length) { $('state').textContent = '还没有模型'; return; }
303
+ load();
304
+ })
305
+ .catch(function (e) { $('state').textContent = '读不了模型表:' + e.message; });
306
+
307
+ $('models').addEventListener('change', function (e) {
308
+ modelId = e.target.value;
309
+ load();
310
+ });
311
+ })();
312
+ </script>
313
+ </body>
314
+ </html>
@@ -78,6 +78,7 @@
78
78
  <div id="settings"></div>
79
79
  <div id="actions">
80
80
  <button id="import">导入模型</button>
81
+ <button id="corpus">语料</button>
81
82
  <button id="close">关掉</button>
82
83
  </div>
83
84
  <div id="tip">选整个文件夹,浏览器会自己把里面所有文件读进来。</div>
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;
@@ -96,6 +97,14 @@
96
97
  const [draft, setDraft] = react.useState('');
97
98
  const [busy, setBusy] = react.useState(false);
98
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
+ }, []);
99
108
  const [nonce, setNonce] = react.useState(0);
100
109
  const ref = react.useRef(null);
101
110
 
@@ -255,7 +264,7 @@
255
264
  h(
256
265
  'div',
257
266
  { className: 'dsh-l2d-sheet-bar' },
258
- h('span', null, sheet === 'tone' ? '音色' : '人设卡'),
267
+ h('span', null, SHEET_TITLE[sheet] || sheet),
259
268
  h('button', { onClick: () => setSheet(null) }, '关掉'),
260
269
  ),
261
270
  h('iframe', { className: 'dsh-l2d-sheet-body', src: BASE + '/' + sheet }),
package/lib/index.js CHANGED
@@ -173,6 +173,57 @@ const TALK_SAMPLES = [
173
173
  '他:明天要早起 —— 你:那我今晚早点关灯。',
174
174
  ].join('\n');
175
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
+
176
227
  function personaOf(meta) {
177
228
  const p = typeof meta?.persona === 'string' ? meta.persona.trim() : '';
178
229
  if (p) return p;
@@ -587,6 +638,7 @@ async function handle(req, res, ctx) {
587
638
  } catch {}
588
639
  }
589
640
  const facts = st.memory ? pickFacts(store.facts, text) : [];
641
+ const samples = st.memory ? pickCorpusSamples(readCorpus(id), text) : [];
590
642
  const hist = store.messages;
591
643
  const lines = hist
592
644
  .slice(-8)
@@ -720,6 +772,11 @@ async function handle(req, res, ctx) {
720
772
  return sendFile(res, join(ASSETS, 'mobile.html'), MIME['.html']);
721
773
  }
722
774
 
775
+ // 语料页
776
+ if (path === '/corpus' || path === '/corpus.html') {
777
+ return sendFile(res, join(ASSETS, 'corpus.html'), MIME['.html']);
778
+ }
779
+
723
780
  // 人设卡页
724
781
  if (path === '/persona' || path === '/persona.html') {
725
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.6",
3
+ "version": "1.2.0",
4
4
  "description": "给 DSH 网页右下角请一个 Live2D 角色:会眨眼、会呼吸、视线跟着鼠标、点她有反应、说话时嘴动、心情变了眉毛跟着动。模型不用内置——谁装谁放自己的文件夹;支持自定义 .exp3.json 表情和 .motion3.json 动作,卡片能拖能缩,背景可换。",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",