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 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 = 4) {
207
+ function pickCorpusSamples(corpus, text, toneN = 2) {
206
208
  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);
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.length ? top : facts.slice(-6);
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
- let t = String(input || '');
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
- if (!settingsOf(cfg).proactive) return;
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 < IDLE_MS) return;
409
- if (now - lastAutoAt < IDLE_MS) return;
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 = st.memory ? pickCorpusSamples(readCorpus(id), text) : [];
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
- const prompt = [
648
- personaOf(meta),
649
- meta.isolation === false ? '' : ISOLATION,
650
- '现在是 ' + nowText() + '。',
651
- store.summary ? '之前聊过的,你还记得:\n' + store.summary : '',
652
- facts.length ? '关于他,你记得这些:\n' + facts.map((f) => '- ' + f).join('\n') : '',
653
- lines ? '你们之前说的:\n' + lines : '',
654
- '他说:' + text,
655
- '照上面这个人说话,回他一句。',
656
- '一到两句,四十字以内。要具体:答应、拒绝、反问、吐槽、提一件正在做的事,都行。' +
657
- '别总结他的话,别讲道理,别说“我理解你”“希望帮到你”“还需要什么”;他没问就别建议。' +
658
- '可以不接他的话,扔一句自己的事;允许只回两个字。',
659
- '只输出这句话本身:不要引号,不要旁白,不要括号里的动作。',
660
- '往下是这个人的说话密度,只学节奏和信息量,别照抄内容:\n' + TALK_SAMPLES,
661
- st.choices ? '如果你想让他挑,可以在最后另起一行写 ||选项一|选项二,最多三项。' : '',
662
- st.memesMode === 'own' || st.memesMode === 'both'
663
- ? '如果你想附一张贴图,就在最后另起一行写 ||图:名字。能挑的名字有:' +
664
- readOwnMemes()
665
- .map((it) => it.label)
666
- .filter(Boolean)
667
- .join('、') +
668
- '。不想配就别写。'
669
- : '',
670
- ]
671
- .filter(Boolean)
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
- writeChat(id, hist.concat([
698
- { role: 'user', text, at: Date.now() },
699
- { role: 'assistant', text: line, at: Date.now() },
700
- ]), store.summary, store.facts);
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.2.2",
3
+ "version": "1.3.1",
4
4
  "description": "给 DSH 网页右下角请一个 Live2D 角色:会眨眼、会呼吸、视线跟着鼠标、点她有反应、说话时嘴动、心情变了眉毛跟着动。模型不用内置——谁装谁放自己的文件夹;支持自定义 .exp3.json 表情和 .motion3.json 动作,卡片能拖能缩,背景可换。",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",