dsh-live2dwaifu 1.3.0 → 2.0.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 +25 -0
- package/assets/corpus.html +4 -5
- package/assets/mobile.html +1 -0
- package/assets/tone.html +19 -12
- package/assets/viewer.js +13 -5
- package/lib/client.js +19 -1
- package/lib/index.js +66 -21
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -380,6 +380,29 @@ curl -X POST http://127.0.0.1:<端口>/dsh-live2d/api/push \
|
|
|
380
380
|
|
|
381
381
|
版本号怎么走:动了功能和结构就抬第二位(1.2.0),只补洞和小修就抬第三位(1.2.1)。
|
|
382
382
|
|
|
383
|
+
### 2.0.0
|
|
384
|
+
|
|
385
|
+
- 修:`ISOLATION` 这个变量在 1.3.0 被误删,`/api/chat` 全部返回 500——那之后她其实一句都说不出来。这是这版最主要的问题。
|
|
386
|
+
- 修:补回 1.3.1 里漏掉的语料接口(之前保存语料会 404)。
|
|
387
|
+
- 修:调音页的下拉框改成只试听,不再顺手把正在显示的模型换掉;要换模型另点「切到这只模型」。
|
|
388
|
+
- 修:卡片位置存在本机,窗口缩小后可能跑到屏幕外,现在重开时自动收回可见范围。
|
|
389
|
+
- 修:页面切到后台时暂停轮询,不再空跑请求。
|
|
390
|
+
- 修:所有接受模型 id 的接口加了字符校验,避免拼出奇怪路径。
|
|
391
|
+
- 改:对话要求补一条——不要每句都反问,不要用「哈哈」「嗯嗯」凑字数。
|
|
392
|
+
- 清:删掉几个没用到的常量、开关和恒真判断。
|
|
393
|
+
|
|
394
|
+
<details>
|
|
395
|
+
<summary>2.0.0 之前的记录(黑历史,留着自嘲)</summary>
|
|
396
|
+
|
|
397
|
+
### 1.3.1
|
|
398
|
+
|
|
399
|
+
- 修:语料接口整个漏掉了(页面打得开,存不进去,返回 404)。补回来。
|
|
400
|
+
- 修:关掉「记住聊过的内容」之后不写聊天记录了,之前还在偷偷存。
|
|
401
|
+
- 修:语料里的问答样例只在有当前这句话时挑,主动开口不再乱塞无关问答。
|
|
402
|
+
- 修:主动说话间隔滑轴遇到旧配置值(比如 12 分钟)会显示成「关闭」,现在找最接近的档位。
|
|
403
|
+
- 修:语料接口的模型 id 只接受安全字符。
|
|
404
|
+
- 加:按 Esc 关掉覆盖层。
|
|
405
|
+
|
|
383
406
|
### 1.3.0
|
|
384
407
|
|
|
385
408
|
- 主动说话的间隔可以调了:⚙ 里一根滑轴,关闭 / 30 分钟 / 1 小时 / 2 小时 / 4 小时。默认从原来的十几分钟放宽到 30 分钟。
|
|
@@ -448,6 +471,8 @@ curl -X POST http://127.0.0.1:<端口>/dsh-live2d/api/push \
|
|
|
448
471
|
- 人设卡编辑页、音色调色页。
|
|
449
472
|
- 对外接口:`/api/chat`、`/api/push`、`/api/poll`。
|
|
450
473
|
|
|
474
|
+
</details>
|
|
475
|
+
|
|
451
476
|
## 反馈
|
|
452
477
|
|
|
453
478
|
遇到问题、想要新功能、觉得哪句话还是像 AI,都可以写信到 **FujiwaraReisen@outlook.com**。
|
package/assets/corpus.html
CHANGED
|
@@ -147,11 +147,10 @@
|
|
|
147
147
|
var said = unquote(m[2]);
|
|
148
148
|
if (!said) continue;
|
|
149
149
|
if (USERS.test(who)) { pending = said; continue; }
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
}
|
|
150
|
+
// 不是「他/我/用户」的说话人,一律当角色
|
|
151
|
+
pairs.push({ user: pending || '', assistant: said });
|
|
152
|
+
pending = null;
|
|
153
|
+
continue;
|
|
155
154
|
}
|
|
156
155
|
var t = unquote(line);
|
|
157
156
|
if (t) lines.push(t);
|
package/assets/mobile.html
CHANGED
package/assets/tone.html
CHANGED
|
@@ -91,6 +91,7 @@
|
|
|
91
91
|
|
|
92
92
|
<div class="foot">
|
|
93
93
|
<button class="act primary" id="save">保存</button>
|
|
94
|
+
<button class="act" id="useModel">切到这只模型</button>
|
|
94
95
|
<button class="act" id="reset">恢复默认</button>
|
|
95
96
|
<span class="state" id="state"></span>
|
|
96
97
|
</div>
|
|
@@ -205,21 +206,27 @@
|
|
|
205
206
|
|
|
206
207
|
sel.addEventListener('change', function () {
|
|
207
208
|
modelId = sel.value;
|
|
208
|
-
loadVoice(
|
|
209
|
+
loadVoice();
|
|
209
210
|
});
|
|
210
211
|
|
|
211
|
-
function
|
|
212
|
+
$('useModel').addEventListener('click', function () {
|
|
212
213
|
if (!modelId) return;
|
|
213
|
-
state.textContent = '
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
214
|
+
state.textContent = '切换中';
|
|
215
|
+
fetch('/dsh-live2d/api/select', {
|
|
216
|
+
method: 'POST',
|
|
217
|
+
headers: { 'content-type': 'application/json' },
|
|
218
|
+
body: JSON.stringify({ id: modelId })
|
|
219
|
+
})
|
|
220
|
+
.then(function () {
|
|
220
221
|
frame.src = '/dsh-live2d/viewer?embed=1&t=' + Date.now();
|
|
221
|
-
|
|
222
|
-
|
|
222
|
+
state.textContent = '已切换';
|
|
223
|
+
})
|
|
224
|
+
.catch(function (e) { state.textContent = '切换失败:' + e.message; });
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
function loadVoice() {
|
|
228
|
+
if (!modelId) return;
|
|
229
|
+
state.textContent = '读取中';
|
|
223
230
|
fetch('/dsh-live2d/api/voice?id=' + encodeURIComponent(modelId))
|
|
224
231
|
.then(function (r) { return r.json(); })
|
|
225
232
|
.then(function (d) {
|
|
@@ -249,7 +256,7 @@
|
|
|
249
256
|
state.textContent = '尚未导入模型';
|
|
250
257
|
return;
|
|
251
258
|
}
|
|
252
|
-
loadVoice(
|
|
259
|
+
loadVoice();
|
|
253
260
|
})
|
|
254
261
|
.catch(function (e) { state.textContent = '读取模型列表失败:' + e.message; });
|
|
255
262
|
|
package/assets/viewer.js
CHANGED
|
@@ -44358,7 +44358,6 @@ void main(void)\r
|
|
|
44358
44358
|
follow: "\u804A\u5929\u7A97\u53E3\u51FA\u73B0\u65B0\u56DE\u590D\u65F6\uFF0C\u7531\u89D2\u8272\u5FF5\u51FA\u6765"
|
|
44359
44359
|
};
|
|
44360
44360
|
var SERVER_HINT = {
|
|
44361
|
-
proactive: "\u4E00\u6BB5\u65F6\u95F4\u6CA1\u4EBA\u8BF4\u8BDD\uFF0C\u89D2\u8272\u81EA\u5DF1\u5F00\u4E00\u53E5",
|
|
44362
44361
|
choices: "\u89D2\u8272\u89C9\u5F97\u9700\u8981\u9009\u62E9\u65F6\uFF0C\u6C14\u6CE1\u4E0A\u65B9\u51FA\u73B0\u53EF\u70B9\u7684\u6309\u94AE",
|
|
44363
44362
|
memory: "\u4FDD\u7559\u804A\u8FC7\u7684\u5185\u5BB9\uFF0C\u4E0B\u6B21\u63A5\u7740\u804A\uFF08\u5360\u66F4\u591A\u6A21\u578B\u989D\u5EA6\uFF09"
|
|
44364
44363
|
};
|
|
@@ -44417,10 +44416,19 @@ void main(void)\r
|
|
|
44417
44416
|
gapSlider.min = "0";
|
|
44418
44417
|
gapSlider.max = String(GAP_VALUES.length - 1);
|
|
44419
44418
|
gapSlider.step = "1";
|
|
44420
|
-
const curGap =
|
|
44421
|
-
|
|
44422
|
-
|
|
44423
|
-
|
|
44419
|
+
const curGap = (() => {
|
|
44420
|
+
const v2 = Number(serverSettings.proactiveInterval ?? 30);
|
|
44421
|
+
let best = 0;
|
|
44422
|
+
let bestDiff = Infinity;
|
|
44423
|
+
GAP_VALUES.forEach((g2, i2) => {
|
|
44424
|
+
const diff = Math.abs(g2 - v2);
|
|
44425
|
+
if (diff < bestDiff) {
|
|
44426
|
+
bestDiff = diff;
|
|
44427
|
+
best = i2;
|
|
44428
|
+
}
|
|
44429
|
+
});
|
|
44430
|
+
return best;
|
|
44431
|
+
})();
|
|
44424
44432
|
gapSlider.value = String(curGap);
|
|
44425
44433
|
const gapLab = document.createElement("div");
|
|
44426
44434
|
gapLab.className = "opt-lab";
|
package/lib/client.js
CHANGED
|
@@ -106,6 +106,14 @@
|
|
|
106
106
|
window.addEventListener('message', onMsg);
|
|
107
107
|
return () => window.removeEventListener('message', onMsg);
|
|
108
108
|
}, []);
|
|
109
|
+
react.useEffect(() => {
|
|
110
|
+
if (!sheet) return undefined;
|
|
111
|
+
const onKey = (ev) => {
|
|
112
|
+
if (ev.key === 'Escape') setSheet(null);
|
|
113
|
+
};
|
|
114
|
+
window.addEventListener('keydown', onKey);
|
|
115
|
+
return () => window.removeEventListener('keydown', onKey);
|
|
116
|
+
}, [sheet]);
|
|
109
117
|
const [nonce, setNonce] = react.useState(0);
|
|
110
118
|
const ref = react.useRef(null);
|
|
111
119
|
|
|
@@ -113,6 +121,16 @@
|
|
|
113
121
|
react.useEffect(() => localStorage.setItem('dsh-l2d-bg', bg), [bg]);
|
|
114
122
|
react.useEffect(() => localStorage.setItem('dsh-l2d-size', JSON.stringify(size)), [size]);
|
|
115
123
|
react.useEffect(() => localStorage.setItem('dsh-l2d-pos', JSON.stringify(pos)), [pos]);
|
|
124
|
+
react.useEffect(() => {
|
|
125
|
+
setPos((p) => {
|
|
126
|
+
const w = size.w;
|
|
127
|
+
const h = open ? size.h : 38;
|
|
128
|
+
const x = Math.max(-w + 90, Math.min(window.innerWidth - 90, p.x));
|
|
129
|
+
const y = Math.max(-h + 50, Math.min(window.innerHeight - 50, p.y));
|
|
130
|
+
return x === p.x && y === p.y ? p : { x, y };
|
|
131
|
+
});
|
|
132
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
133
|
+
}, []);
|
|
116
134
|
react.useEffect(() => {
|
|
117
135
|
frameWin = (ref.current && ref.current.contentWindow) || null;
|
|
118
136
|
}, [nonce, open]);
|
|
@@ -120,7 +138,7 @@
|
|
|
120
138
|
let seq = null;
|
|
121
139
|
let stop = false;
|
|
122
140
|
const tick = () => {
|
|
123
|
-
if (stop) return;
|
|
141
|
+
if (stop || document.hidden) return;
|
|
124
142
|
fetch(BASE + '/api/poll?since=' + (seq === null ? 0 : seq))
|
|
125
143
|
.then((r) => r.json())
|
|
126
144
|
.then((d) => {
|
package/lib/index.js
CHANGED
|
@@ -24,7 +24,6 @@ const MEME_TAGS = {
|
|
|
24
24
|
};
|
|
25
25
|
|
|
26
26
|
const DEFAULT_SETTINGS = {
|
|
27
|
-
proactive: true,
|
|
28
27
|
/** 多久没人说话才自己开口,单位分钟;0 = 关闭 */
|
|
29
28
|
proactiveInterval: 30,
|
|
30
29
|
/** off | own | both | external —— 测试功能,默认关 */
|
|
@@ -175,6 +174,13 @@ const TALK_SAMPLES = [
|
|
|
175
174
|
'他:明天要早起 —— 你:那我今晚早点关灯。',
|
|
176
175
|
].join('\n');
|
|
177
176
|
|
|
177
|
+
/** 世界隔离:不说破自己在哪儿 */
|
|
178
|
+
const ISOLATION = [
|
|
179
|
+
'【世界隔离】你不知道什么是程序、界面、插件、模型、参数。',
|
|
180
|
+
'有人问起,你就当他就站在你身边。不要提“系统”“设置”“网页”“token”这些词。',
|
|
181
|
+
'你不知道他那边什么样,得他讲给你听。',
|
|
182
|
+
].join('\n');
|
|
183
|
+
|
|
178
184
|
/** 语料:lines = 她的台词,pairs = 一问一答 */
|
|
179
185
|
function readCorpus(id) {
|
|
180
186
|
try {
|
|
@@ -206,17 +212,20 @@ function writeCorpus(id, corpus) {
|
|
|
206
212
|
/** 从语料里挑几条最贴当前对话的,拼成样例 */
|
|
207
213
|
function pickCorpusSamples(corpus, text, toneN = 2) {
|
|
208
214
|
const out = [];
|
|
209
|
-
const
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
215
|
+
const query = String(text || '').trim();
|
|
216
|
+
if (query) {
|
|
217
|
+
const chars = new Set(query);
|
|
218
|
+
const scored = (corpus.pairs || [])
|
|
219
|
+
.map((p) => {
|
|
220
|
+
let hit = 0;
|
|
221
|
+
for (const ch of String(p.user || '')) if (chars.has(ch)) hit += 1;
|
|
222
|
+
return { p, hit };
|
|
223
|
+
})
|
|
224
|
+
.sort((a, b) => b.hit - a.hit);
|
|
225
|
+
for (const s of scored.slice(0, 2)) {
|
|
226
|
+
if (s.p.user) out.push('他:' + s.p.user + ' —— 你:' + s.p.assistant);
|
|
227
|
+
else out.push('你:' + s.p.assistant);
|
|
228
|
+
}
|
|
220
229
|
}
|
|
221
230
|
const pool = (corpus.lines || []).slice();
|
|
222
231
|
for (let i = 0; i < toneN && pool.length; i += 1) {
|
|
@@ -318,7 +327,7 @@ function pickFacts(facts, text) {
|
|
|
318
327
|
});
|
|
319
328
|
scored.sort((a, b) => b.hit - a.hit);
|
|
320
329
|
const top = scored.filter((s) => s.hit > 0).slice(0, 6).map((s) => s.f);
|
|
321
|
-
return top
|
|
330
|
+
return top;
|
|
322
331
|
}
|
|
323
332
|
|
|
324
333
|
/** 去 AI 味:删套话、砍尾巴、压重复 */
|
|
@@ -332,7 +341,8 @@ const AI_WORDS = [
|
|
|
332
341
|
];
|
|
333
342
|
|
|
334
343
|
function humanize(input) {
|
|
335
|
-
|
|
344
|
+
const original = String(input || '');
|
|
345
|
+
let t = original;
|
|
336
346
|
for (const re of AI_WORDS) t = t.replace(re, '');
|
|
337
347
|
t = t
|
|
338
348
|
.replace(/[ \t]+/g, ' ')
|
|
@@ -346,7 +356,7 @@ function humanize(input) {
|
|
|
346
356
|
const cut = t.match(/^[\s\S]{0,60}?[。!?!?]/);
|
|
347
357
|
t = cut ? cut[0] : t.slice(0, 60);
|
|
348
358
|
}
|
|
349
|
-
return t.trim();
|
|
359
|
+
return t.trim() || original.trim();
|
|
350
360
|
}
|
|
351
361
|
|
|
352
362
|
/* ── 出口队列:谁能说话,都往这儿扔 ───────────────────────── */
|
|
@@ -400,7 +410,6 @@ async function generate(slot, prompt, limit = 900) {
|
|
|
400
410
|
}
|
|
401
411
|
|
|
402
412
|
/* ── 主动开口:没人说话,她自己开一句 ─────────────────────── */
|
|
403
|
-
const IDLE_MS = 12 * 60 * 1000;
|
|
404
413
|
let lastAutoAt = 0;
|
|
405
414
|
|
|
406
415
|
async function maybeTalk(ctx) {
|
|
@@ -519,7 +528,12 @@ function findModel3(dir) {
|
|
|
519
528
|
return out[0];
|
|
520
529
|
}
|
|
521
530
|
|
|
531
|
+
function validId(id) {
|
|
532
|
+
return /^[\w.-]+$/.test(String(id || ''));
|
|
533
|
+
}
|
|
534
|
+
|
|
522
535
|
function readMeta(id) {
|
|
536
|
+
if (!validId(id)) return null;
|
|
523
537
|
try {
|
|
524
538
|
return JSON.parse(readFileSync(join(MODELS, id, 'meta.json'), 'utf8'));
|
|
525
539
|
} catch {
|
|
@@ -643,7 +657,7 @@ async function handle(req, res, ctx) {
|
|
|
643
657
|
} catch {}
|
|
644
658
|
}
|
|
645
659
|
const facts = st.memory ? pickFacts(store.facts, text) : [];
|
|
646
|
-
const samples =
|
|
660
|
+
const samples = pickCorpusSamples(readCorpus(id), text);
|
|
647
661
|
const hist = store.messages;
|
|
648
662
|
const lines = hist
|
|
649
663
|
.slice(-8)
|
|
@@ -698,10 +712,17 @@ async function handle(req, res, ctx) {
|
|
|
698
712
|
? pickImage(st.memesMode, moodNow)
|
|
699
713
|
: null;
|
|
700
714
|
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
715
|
+
if (st.memory) {
|
|
716
|
+
writeChat(
|
|
717
|
+
id,
|
|
718
|
+
hist.concat([
|
|
719
|
+
{ role: 'user', text, at: Date.now() },
|
|
720
|
+
{ role: 'assistant', text: line, at: Date.now() },
|
|
721
|
+
]),
|
|
722
|
+
store.summary,
|
|
723
|
+
store.facts,
|
|
724
|
+
);
|
|
725
|
+
}
|
|
705
726
|
return sendJson(res, 200, { ok: true, reply: line, choices, image: image || undefined, id });
|
|
706
727
|
}
|
|
707
728
|
|
|
@@ -795,6 +816,30 @@ async function handle(req, res, ctx) {
|
|
|
795
816
|
}
|
|
796
817
|
|
|
797
818
|
// API
|
|
819
|
+
|
|
820
|
+
// 语料
|
|
821
|
+
if (path === '/api/corpus' && method === 'GET') {
|
|
822
|
+
const id = String(url.searchParams.get('id') ?? '');
|
|
823
|
+
if (id && !/^[\w.-]+$/.test(id)) return sendJson(res, 400, { error: 'bad id' });
|
|
824
|
+
const c = id ? readCorpus(id) : { lines: [], pairs: [] };
|
|
825
|
+
return sendJson(res, 200, { id, lines: c.lines, pairs: c.pairs, total: c.lines.length + c.pairs.length });
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
if (path === '/api/corpus' && method === 'POST') {
|
|
829
|
+
const body = JSON.parse((await readBody(req)).toString('utf8') || '{}');
|
|
830
|
+
const id = String(body.id ?? '');
|
|
831
|
+
if (!/^[\w.-]+$/.test(id)) return sendJson(res, 400, { error: 'bad id' });
|
|
832
|
+
const meta = readMeta(id);
|
|
833
|
+
if (!meta) return sendJson(res, 404, { error: 'model not found' });
|
|
834
|
+
const corpus = {
|
|
835
|
+
lines: Array.isArray(body.lines) ? body.lines.filter((x) => typeof x === 'string' && x.trim()) : [],
|
|
836
|
+
pairs: Array.isArray(body.pairs)
|
|
837
|
+
? body.pairs.filter((p) => p && typeof p.assistant === 'string' && p.assistant.trim())
|
|
838
|
+
: [],
|
|
839
|
+
};
|
|
840
|
+
writeCorpus(id, corpus);
|
|
841
|
+
return sendJson(res, 200, { ok: true, total: corpus.lines.length + corpus.pairs.length });
|
|
842
|
+
}
|
|
798
843
|
if (path === '/api/voice' && method === 'GET') {
|
|
799
844
|
const id = String(url.searchParams.get('id') ?? '');
|
|
800
845
|
const meta = id ? readMeta(id) : null;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-live2dwaifu",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "给 DSH 网页右下角请一个 Live2D 角色:会眨眼、会呼吸、视线跟着鼠标、点她有反应、说话时嘴动、心情变了眉毛跟着动。模型不用内置——谁装谁放自己的文件夹;支持自定义 .exp3.json 表情和 .motion3.json 动作,卡片能拖能缩,背景可换。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|