dsh-live2dwaifu 1.2.2 → 1.3.1
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 +17 -1
- package/assets/viewer.js +49 -1
- package/lib/client.js +8 -0
- package/lib/index.js +91 -49
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -231,7 +231,7 @@ python tools/export-dsh-meme.py
|
|
|
231
231
|
| 自动播放动作 | 本机 | 开 | 模型带动作文件时,隔一会儿播一个 |
|
|
232
232
|
| 说话带电子音 | 本机 | 开 | 每个字一声短音 |
|
|
233
233
|
| 复述聊天内容 | 本机 | 开 | 聊天窗口出现新回复时,由她念出来 |
|
|
234
|
-
|
|
|
234
|
+
| 闲置主动说话的间隔 | 服务端 | 30 分钟 | 关闭 / 30 分钟 / 1 小时 / 2 小时 / 4 小时 |
|
|
235
235
|
| 回复附带选项按钮 | 服务端 | 开 | 她觉得需要选择时,气泡上方出现可点按钮 |
|
|
236
236
|
| 记住聊过的内容 | 服务端 | 开 | 关掉就不带记忆、不压缩 |
|
|
237
237
|
| 表情包(四档) | 服务端 | 不使用 | 测试功能,见上面那节 |
|
|
@@ -380,6 +380,22 @@ 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
|
+
### 1.3.1
|
|
384
|
+
|
|
385
|
+
- 修:语料接口整个漏掉了(页面打得开,存不进去,返回 404)。补回来。
|
|
386
|
+
- 修:关掉「记住聊过的内容」之后不写聊天记录了,之前还在偷偷存。
|
|
387
|
+
- 修:语料里的问答样例只在有当前这句话时挑,主动开口不再乱塞无关问答。
|
|
388
|
+
- 修:主动说话间隔滑轴遇到旧配置值(比如 12 分钟)会显示成「关闭」,现在找最接近的档位。
|
|
389
|
+
- 修:语料接口的模型 id 只接受安全字符。
|
|
390
|
+
- 加:按 Esc 关掉覆盖层。
|
|
391
|
+
|
|
392
|
+
### 1.3.0
|
|
393
|
+
|
|
394
|
+
- 主动说话的间隔可以调了:⚙ 里一根滑轴,关闭 / 30 分钟 / 1 小时 / 2 小时 / 4 小时。默认从原来的十几分钟放宽到 30 分钟。
|
|
395
|
+
- 提示词改了顺序和说法:当前这句话和规则挪到最后,语料样例挪到前面并注明「只参考语气,别拿去回答」。之前她容易跟着样例跑,答非所问。
|
|
396
|
+
- 规则里加了硬要求:先接住这句话,别答非所问;接不住就说短的,不要编。
|
|
397
|
+
- 语料取样从「最多 3 组问答 + 4 句台词」收到「2 + 2」,少喂点无关内容。
|
|
398
|
+
|
|
383
399
|
### 1.2.2
|
|
384
400
|
|
|
385
401
|
- 人设卡、音色、语料三个页面的按钮和说明改成通用说法:保存/追加保存/覆盖保存/放弃修改/选择文件/播放/关掉。
|
package/assets/viewer.js
CHANGED
|
@@ -44345,10 +44345,11 @@ void main(void)\r
|
|
|
44345
44345
|
["follow", "\u590D\u8FF0\u804A\u5929\u5185\u5BB9"]
|
|
44346
44346
|
];
|
|
44347
44347
|
var SERVER_OPTS = [
|
|
44348
|
-
["proactive", "\u95F2\u7F6E\u65F6\u4E3B\u52A8\u8BF4\u8BDD"],
|
|
44349
44348
|
["choices", "\u56DE\u590D\u9644\u5E26\u9009\u9879\u6309\u94AE"],
|
|
44350
44349
|
["memory", "\u8BB0\u4F4F\u804A\u8FC7\u7684\u5185\u5BB9"]
|
|
44351
44350
|
];
|
|
44351
|
+
var GAP_LABELS = ["\u5173\u95ED", "30 \u5206\u949F", "1 \u5C0F\u65F6", "2 \u5C0F\u65F6", "4 \u5C0F\u65F6"];
|
|
44352
|
+
var GAP_VALUES = [0, 30, 60, 120, 240];
|
|
44352
44353
|
var LOCAL_HINT = {
|
|
44353
44354
|
gaze: "\u89D2\u8272\u7684\u773C\u775B\u548C\u5934\u4F1A\u8DDF\u7740\u9F20\u6807\u79FB\u52A8",
|
|
44354
44355
|
poke: "\u70B9\u89D2\u8272\u7684\u4E0D\u540C\u90E8\u4F4D\u4F1A\u6709\u4E0D\u540C\u53CD\u5E94",
|
|
@@ -44406,6 +44407,53 @@ void main(void)\r
|
|
|
44406
44407
|
row.appendChild(document.createTextNode(label));
|
|
44407
44408
|
box.appendChild(row);
|
|
44408
44409
|
}
|
|
44410
|
+
const hg = document.createElement("h3");
|
|
44411
|
+
hg.textContent = "\u95F2\u7F6E\u4E3B\u52A8\u8BF4\u8BDD\u7684\u95F4\u9694";
|
|
44412
|
+
box.appendChild(hg);
|
|
44413
|
+
const gapWrap = document.createElement("div");
|
|
44414
|
+
gapWrap.className = "opt-col";
|
|
44415
|
+
const gapSlider = document.createElement("input");
|
|
44416
|
+
gapSlider.type = "range";
|
|
44417
|
+
gapSlider.min = "0";
|
|
44418
|
+
gapSlider.max = String(GAP_VALUES.length - 1);
|
|
44419
|
+
gapSlider.step = "1";
|
|
44420
|
+
const curGap = (() => {
|
|
44421
|
+
const v2 = Number(serverSettings.proactiveInterval ?? 30);
|
|
44422
|
+
let best = 0;
|
|
44423
|
+
let bestDiff = Infinity;
|
|
44424
|
+
GAP_VALUES.forEach((g2, i2) => {
|
|
44425
|
+
const diff = Math.abs(g2 - v2);
|
|
44426
|
+
if (diff < bestDiff) {
|
|
44427
|
+
bestDiff = diff;
|
|
44428
|
+
best = i2;
|
|
44429
|
+
}
|
|
44430
|
+
});
|
|
44431
|
+
return best;
|
|
44432
|
+
})();
|
|
44433
|
+
gapSlider.value = String(curGap);
|
|
44434
|
+
const gapLab = document.createElement("div");
|
|
44435
|
+
gapLab.className = "opt-lab";
|
|
44436
|
+
gapLab.textContent = GAP_LABELS[curGap];
|
|
44437
|
+
gapSlider.oninput = () => {
|
|
44438
|
+
gapLab.textContent = GAP_LABELS[Number(gapSlider.value)];
|
|
44439
|
+
};
|
|
44440
|
+
gapSlider.onchange = () => {
|
|
44441
|
+
const v2 = GAP_VALUES[Number(gapSlider.value)];
|
|
44442
|
+
serverSettings.proactiveInterval = v2;
|
|
44443
|
+
fetch(BASE + "/api/settings", {
|
|
44444
|
+
method: "POST",
|
|
44445
|
+
headers: { "content-type": "application/json" },
|
|
44446
|
+
body: JSON.stringify({ settings: { proactiveInterval: v2 } })
|
|
44447
|
+
}).catch(() => {
|
|
44448
|
+
});
|
|
44449
|
+
};
|
|
44450
|
+
const gapNote = document.createElement("div");
|
|
44451
|
+
gapNote.className = "opt-note";
|
|
44452
|
+
gapNote.textContent = "\u591A\u4E45\u6CA1\u6709\u5BF9\u8BDD\u4E4B\u540E\uFF0C\u89D2\u8272\u4E3B\u52A8\u8BF4\u4E00\u53E5\u3002\u9009\u300C\u5173\u95ED\u300D\u5C31\u4E0D\u4F1A\u81EA\u5DF1\u5F00\u53E3\u3002";
|
|
44453
|
+
gapWrap.appendChild(gapSlider);
|
|
44454
|
+
gapWrap.appendChild(gapLab);
|
|
44455
|
+
gapWrap.appendChild(gapNote);
|
|
44456
|
+
box.appendChild(gapWrap);
|
|
44409
44457
|
const h3 = document.createElement("h3");
|
|
44410
44458
|
h3.textContent = "\u8868\u60C5\u5305\uFF08\u6D4B\u8BD5\u529F\u80FD\uFF09";
|
|
44411
44459
|
box.appendChild(h3);
|
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
|
|
package/lib/index.js
CHANGED
|
@@ -25,6 +25,8 @@ const MEME_TAGS = {
|
|
|
25
25
|
|
|
26
26
|
const DEFAULT_SETTINGS = {
|
|
27
27
|
proactive: true,
|
|
28
|
+
/** 多久没人说话才自己开口,单位分钟;0 = 关闭 */
|
|
29
|
+
proactiveInterval: 30,
|
|
28
30
|
/** off | own | both | external —— 测试功能,默认关 */
|
|
29
31
|
memesMode: 'off',
|
|
30
32
|
choices: true,
|
|
@@ -202,19 +204,22 @@ function writeCorpus(id, corpus) {
|
|
|
202
204
|
}
|
|
203
205
|
|
|
204
206
|
/** 从语料里挑几条最贴当前对话的,拼成样例 */
|
|
205
|
-
function pickCorpusSamples(corpus, text, toneN =
|
|
207
|
+
function pickCorpusSamples(corpus, text, toneN = 2) {
|
|
206
208
|
const out = [];
|
|
207
|
-
const
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
209
|
+
const query = String(text || '').trim();
|
|
210
|
+
if (query) {
|
|
211
|
+
const chars = new Set(query);
|
|
212
|
+
const scored = (corpus.pairs || [])
|
|
213
|
+
.map((p) => {
|
|
214
|
+
let hit = 0;
|
|
215
|
+
for (const ch of String(p.user || '')) if (chars.has(ch)) hit += 1;
|
|
216
|
+
return { p, hit };
|
|
217
|
+
})
|
|
218
|
+
.sort((a, b) => b.hit - a.hit);
|
|
219
|
+
for (const s of scored.slice(0, 2)) {
|
|
220
|
+
if (s.p.user) out.push('他:' + s.p.user + ' —— 你:' + s.p.assistant);
|
|
221
|
+
else out.push('你:' + s.p.assistant);
|
|
222
|
+
}
|
|
218
223
|
}
|
|
219
224
|
const pool = (corpus.lines || []).slice();
|
|
220
225
|
for (let i = 0; i < toneN && pool.length; i += 1) {
|
|
@@ -316,7 +321,7 @@ function pickFacts(facts, text) {
|
|
|
316
321
|
});
|
|
317
322
|
scored.sort((a, b) => b.hit - a.hit);
|
|
318
323
|
const top = scored.filter((s) => s.hit > 0).slice(0, 6).map((s) => s.f);
|
|
319
|
-
return top
|
|
324
|
+
return top;
|
|
320
325
|
}
|
|
321
326
|
|
|
322
327
|
/** 去 AI 味:删套话、砍尾巴、压重复 */
|
|
@@ -330,7 +335,8 @@ const AI_WORDS = [
|
|
|
330
335
|
];
|
|
331
336
|
|
|
332
337
|
function humanize(input) {
|
|
333
|
-
|
|
338
|
+
const original = String(input || '');
|
|
339
|
+
let t = original;
|
|
334
340
|
for (const re of AI_WORDS) t = t.replace(re, '');
|
|
335
341
|
t = t
|
|
336
342
|
.replace(/[ \t]+/g, ' ')
|
|
@@ -344,7 +350,7 @@ function humanize(input) {
|
|
|
344
350
|
const cut = t.match(/^[\s\S]{0,60}?[。!?!?]/);
|
|
345
351
|
t = cut ? cut[0] : t.slice(0, 60);
|
|
346
352
|
}
|
|
347
|
-
return t.trim();
|
|
353
|
+
return t.trim() || original.trim();
|
|
348
354
|
}
|
|
349
355
|
|
|
350
356
|
/* ── 出口队列:谁能说话,都往这儿扔 ───────────────────────── */
|
|
@@ -403,10 +409,13 @@ let lastAutoAt = 0;
|
|
|
403
409
|
|
|
404
410
|
async function maybeTalk(ctx) {
|
|
405
411
|
const cfg = readConfig();
|
|
406
|
-
|
|
412
|
+
const st = settingsOf(cfg);
|
|
413
|
+
const mins = Number(st.proactiveInterval ?? 30);
|
|
414
|
+
if (!(mins > 0)) return;
|
|
415
|
+
const gap = mins * 60 * 1000;
|
|
407
416
|
const now = Date.now();
|
|
408
|
-
if (now - LAST_ACTIVE.at <
|
|
409
|
-
if (now - lastAutoAt <
|
|
417
|
+
if (now - LAST_ACTIVE.at < gap) return;
|
|
418
|
+
if (now - lastAutoAt < gap) return;
|
|
410
419
|
const id = String(cfg.selected || '');
|
|
411
420
|
const meta = id ? readMeta(id) : null;
|
|
412
421
|
if (!meta) return;
|
|
@@ -638,38 +647,37 @@ async function handle(req, res, ctx) {
|
|
|
638
647
|
} catch {}
|
|
639
648
|
}
|
|
640
649
|
const facts = st.memory ? pickFacts(store.facts, text) : [];
|
|
641
|
-
const samples =
|
|
650
|
+
const samples = pickCorpusSamples(readCorpus(id), text);
|
|
642
651
|
const hist = store.messages;
|
|
643
652
|
const lines = hist
|
|
644
653
|
.slice(-8)
|
|
645
654
|
.map((m) => (m.role === 'user' ? '他:' : '你:') + String(m.text ?? ''))
|
|
646
655
|
.join('\n');
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
.join('\n\n');
|
|
656
|
+
const prompt = [
|
|
657
|
+
personaOf(meta),
|
|
658
|
+
meta.isolation === false ? '' : ISOLATION,
|
|
659
|
+
'现在是 ' + nowText() + '。',
|
|
660
|
+
store.summary ? '之前聊过的,你还记得:\n' + store.summary : '',
|
|
661
|
+
facts.length ? '关于他,你记得这些:\n' + facts.map((f) => '- ' + f).join('\n') : '',
|
|
662
|
+
samples.length ? '这个人平时这么说话,只参考语气,别照抄,也别拿这些去回答:\n' + samples.join('\n') : '',
|
|
663
|
+
lines ? '刚才的对话:\n' + lines : '',
|
|
664
|
+
'他现在说:' + text,
|
|
665
|
+
'先接住他这句话:同意、拒绝、反问、解释、或者提一件和他这句有关的事。别答非所问,别自顾自说别的。',
|
|
666
|
+
'一到两句,四十字以内。不总结他,不讲道理,不说“我理解你”“希望帮到你”“还需要什么”;他没问就别建议。接不住就说短的,不要编。',
|
|
667
|
+
'只输出这句话本身:不要引号,不要旁白,不要括号里的动作。',
|
|
668
|
+
'说话密度参考(别照抄内容):\n' + TALK_SAMPLES,
|
|
669
|
+
st.choices ? '需要他挑的话,最后另起一行写 ||选项一|选项二,最多三项。' : '',
|
|
670
|
+
st.memesMode === 'own' || st.memesMode === 'both'
|
|
671
|
+
? '想附一张贴图,就在最后另起一行写 ||图:名字。能挑的:' +
|
|
672
|
+
readOwnMemes()
|
|
673
|
+
.map((it) => it.label)
|
|
674
|
+
.filter(Boolean)
|
|
675
|
+
.join('、') +
|
|
676
|
+
'。不想配就别写。'
|
|
677
|
+
: '',
|
|
678
|
+
]
|
|
679
|
+
.filter(Boolean)
|
|
680
|
+
.join('\n\n');
|
|
673
681
|
|
|
674
682
|
let reply = '';
|
|
675
683
|
try {
|
|
@@ -694,10 +702,17 @@ async function handle(req, res, ctx) {
|
|
|
694
702
|
? pickImage(st.memesMode, moodNow)
|
|
695
703
|
: null;
|
|
696
704
|
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
705
|
+
if (st.memory) {
|
|
706
|
+
writeChat(
|
|
707
|
+
id,
|
|
708
|
+
hist.concat([
|
|
709
|
+
{ role: 'user', text, at: Date.now() },
|
|
710
|
+
{ role: 'assistant', text: line, at: Date.now() },
|
|
711
|
+
]),
|
|
712
|
+
store.summary,
|
|
713
|
+
store.facts,
|
|
714
|
+
);
|
|
715
|
+
}
|
|
701
716
|
return sendJson(res, 200, { ok: true, reply: line, choices, image: image || undefined, id });
|
|
702
717
|
}
|
|
703
718
|
|
|
@@ -738,6 +753,9 @@ async function handle(req, res, ctx) {
|
|
|
738
753
|
for (const k of Object.keys(DEFAULT_SETTINGS)) {
|
|
739
754
|
if (typeof patch?.[k] === 'boolean') next[k] = patch[k];
|
|
740
755
|
}
|
|
756
|
+
if (typeof patch?.proactiveInterval === 'number' && Number.isFinite(patch.proactiveInterval)) {
|
|
757
|
+
next.proactiveInterval = Math.max(0, Math.min(720, Math.round(patch.proactiveInterval)));
|
|
758
|
+
}
|
|
741
759
|
if (typeof patch?.memesMode === 'string' && MEMES_MODES.includes(patch.memesMode)) {
|
|
742
760
|
next.memesMode = patch.memesMode;
|
|
743
761
|
}
|
|
@@ -788,6 +806,30 @@ async function handle(req, res, ctx) {
|
|
|
788
806
|
}
|
|
789
807
|
|
|
790
808
|
// API
|
|
809
|
+
|
|
810
|
+
// 语料
|
|
811
|
+
if (path === '/api/corpus' && method === 'GET') {
|
|
812
|
+
const id = String(url.searchParams.get('id') ?? '');
|
|
813
|
+
if (id && !/^[\w.-]+$/.test(id)) return sendJson(res, 400, { error: 'bad id' });
|
|
814
|
+
const c = id ? readCorpus(id) : { lines: [], pairs: [] };
|
|
815
|
+
return sendJson(res, 200, { id, lines: c.lines, pairs: c.pairs, total: c.lines.length + c.pairs.length });
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
if (path === '/api/corpus' && method === 'POST') {
|
|
819
|
+
const body = JSON.parse((await readBody(req)).toString('utf8') || '{}');
|
|
820
|
+
const id = String(body.id ?? '');
|
|
821
|
+
if (!/^[\w.-]+$/.test(id)) return sendJson(res, 400, { error: 'bad id' });
|
|
822
|
+
const meta = readMeta(id);
|
|
823
|
+
if (!meta) return sendJson(res, 404, { error: 'model not found' });
|
|
824
|
+
const corpus = {
|
|
825
|
+
lines: Array.isArray(body.lines) ? body.lines.filter((x) => typeof x === 'string' && x.trim()) : [],
|
|
826
|
+
pairs: Array.isArray(body.pairs)
|
|
827
|
+
? body.pairs.filter((p) => p && typeof p.assistant === 'string' && p.assistant.trim())
|
|
828
|
+
: [],
|
|
829
|
+
};
|
|
830
|
+
writeCorpus(id, corpus);
|
|
831
|
+
return sendJson(res, 200, { ok: true, total: corpus.lines.length + corpus.pairs.length });
|
|
832
|
+
}
|
|
791
833
|
if (path === '/api/voice' && method === 'GET') {
|
|
792
834
|
const id = String(url.searchParams.get('id') ?? '');
|
|
793
835
|
const meta = id ? readMeta(id) : null;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-live2dwaifu",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.1",
|
|
4
4
|
"description": "给 DSH 网页右下角请一个 Live2D 角色:会眨眼、会呼吸、视线跟着鼠标、点她有反应、说话时嘴动、心情变了眉毛跟着动。模型不用内置——谁装谁放自己的文件夹;支持自定义 .exp3.json 表情和 .motion3.json 动作,卡片能拖能缩,背景可换。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|