dsh-live2dwaifu 0.6.0 → 0.7.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
@@ -23,7 +23,7 @@
23
23
 
24
24
  **会出声**:一个字一声很短的电音,标点停一拍,句尾沉一下——不是朗读,是“哔哔”那种。标题栏的 **♪** 打开调音页,放个真模型在旁边,边调边听;每只模型各存一份音色,换人换声。音量拉到 0 就安静,只剩气泡和口型。
25
25
 
26
- **她也会甩图**:图库直接吃 `dsh-meme` 那套,说话时按情绪挑一张,贴在气泡里。
26
+ **她也会甩图(测试功能)**:⚙ 里有一根滑轴,四档——不使用 / 只使用自带 / 均开启 / 只用 dsh-meme。自带那套是夏铃自己的 Q 版贴纸,二十张,随插件分发,版权归本插件;dsh-meme 那套只是借用你本机已经装的图库。默认关着,功能还在试。
27
27
 
28
28
  **她还会问你要选项**:话说到了需要你挑的地方,气泡下面会冒出两三个按钮,点一个就等于你回了她。
29
29
 
@@ -125,6 +125,16 @@ frame.contentWindow.postMessage({ type: 'dsh-live2d/manage' }, '*');
125
125
  **能放动作吗?**
126
126
  暂时不能编排 `.motion3.json`,表情是固定的七种。想要更多情绪,给它发 `mood`,等以后的版本。
127
127
 
128
+ ## 关于别人的插件
129
+
130
+ 她的图、她的耳朵,都是从你**已经装好的**插件里读的。这个包里不带任何第三方素材,也一行没改别人的代码:
131
+
132
+ - **表情图**:从 `dsh-meme` 的图库目录里挑一张,只在本机显示,不上传、不分发。没装 `dsh-meme` 就没有图,其它照常。
133
+ - **麦克风**:交给 `dsh-ears`,本插件不碰录音,只是把转写好的字送进对话。
134
+ - **对话**:走 DSH 自己的模型接口(`ctx.llm`),不经过任何人的会话,也不写你的聊天记录。
135
+
136
+ 不喜欢这些联动,⚙ 里都能单独关掉。
137
+
128
138
  ## 已知限制
129
139
 
130
140
  - 只支持 Cubism 3 / 4(表情只认 `.exp3.json`,不认 `.motion3.json` 里的表情轨)
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
@@ -0,0 +1,29 @@
1
+ {
2
+ "id": "xialing-001",
3
+ "name": "夏铃",
4
+ "version": "1.0.0",
5
+ "license": "随本插件以 MIT 分发;图由 LFR 用 Seedream 生成",
6
+ "note": "Q 版三头身,白大褂 + 红高领毛衣 + 黑裤,淡灰虹膜,细黑框眼镜。",
7
+ "items": [
8
+ { "file": "deyi.jpg", "label": "得意", "moods": ["开心"] },
9
+ { "file": "shengqi.jpg", "label": "生气", "moods": ["生气"] },
10
+ { "file": "haixiu.jpg", "label": "害羞", "moods": ["害羞"] },
11
+ { "file": "kun.jpg", "label": "困", "moods": ["困"] },
12
+ { "file": "wuyu.jpg", "label": "无语", "moods": ["无语"] },
13
+ { "file": "jingya.jpg", "label": "惊讶", "moods": ["惊讶"] },
14
+ { "file": "kaixin.jpg", "label": "开心", "moods": ["开心"] },
15
+ { "file": "sikao.jpg", "label": "想事情", "moods": ["平静"] },
16
+ { "file": "xianqi.jpg", "label": "嫌弃", "moods": ["无语", "生气"] },
17
+ { "file": "diantou.jpg", "label": "点头", "moods": ["平静"] },
18
+ { "file": "zhaoshou.jpg", "label": "打招呼", "moods": ["开心"] },
19
+ { "file": "bunaifan.jpg", "label": "别烦我", "moods": ["生气"] },
20
+ { "file": "xin-ing.jpg", "label": "心动", "moods": ["害羞"] },
21
+ { "file": "xin-cui.jpg", "label": "手指抵唇", "moods": ["害羞"] },
22
+ { "file": "xin-peng.jpg", "label": "崩溃", "moods": ["惊讶"] },
23
+ { "file": "xin-tiao.jpg", "label": "挑衅", "moods": ["生气"] },
24
+ { "file": "xin-xiao.jpg", "label": "看好戏", "moods": ["开心"] },
25
+ { "file": "xin-daxiao.jpg", "label": "大笑", "moods": ["开心"] },
26
+ { "file": "xin-hei.jpg", "label": "暗笑", "moods": ["无语"] },
27
+ { "file": "xin-yao.jpg", "label": "馋", "moods": ["害羞"] }
28
+ ]
29
+ }
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
@@ -42,6 +42,10 @@
42
42
  #settings h3 { font-size: 10px; letter-spacing: .6px; color: #6f7783; font-weight: 600; margin: 8px 0 4px; }
43
43
  .opt { display: flex; align-items: center; gap: 8px; padding: 3px 2px; font-size: 12px; color: #c3c9d3; }
44
44
  .opt input { accent-color: #6aa9ff; }
45
+ .opt-col { padding: 4px 2px 2px; }
46
+ .opt-col input[type=range] { width: 100%; accent-color: #6aa9ff; }
47
+ .opt-lab { font-size: 12px; color: #dfe4ec; margin-top: 2px; }
48
+ .opt-note { font-size: 10.5px; line-height: 1.6; color: #6f7783; margin-top: 4px; }
45
49
  .bub-img { display: block; max-width: 100%; max-height: 120px; border-radius: 8px; margin-bottom: 6px; object-fit: contain; }
46
50
  #actions { display: flex; gap: 6px; }
47
51
  #actions button { flex: 1; background: #2f343d; color: #cfd4dd; border: 1px solid #3c424c;
package/assets/viewer.js CHANGED
@@ -44346,10 +44346,11 @@ void main(void)\r
44346
44346
  ];
44347
44347
  var SERVER_OPTS = [
44348
44348
  ["proactive", "\u6CA1\u4EBA\u7406\u5C31\u81EA\u5DF1\u5F00\u53E3"],
44349
- ["memes", "\u4F1A\u7529\u8868\u60C5\u5305"],
44350
44349
  ["choices", "\u7ED9\u9009\u9879\u8BA9\u4ED6\u6311"],
44351
44350
  ["memory", "\u8BB0\u4F4F\u4E4B\u524D\u8BF4\u7684"]
44352
44351
  ];
44352
+ var MEME_MODES = ["off", "own", "both", "external"];
44353
+ var MEME_LABELS = ["\u4E0D\u4F7F\u7528", "\u53EA\u4F7F\u7528\u81EA\u5E26", "\u5747\u5F00\u542F", "\u53EA\u7528 dsh-meme"];
44353
44354
  var serverSettings = {};
44354
44355
  function renderSettings() {
44355
44356
  const box = $2("settings");
@@ -44391,6 +44392,41 @@ void main(void)\r
44391
44392
  row.appendChild(document.createTextNode(label));
44392
44393
  box.appendChild(row);
44393
44394
  }
44395
+ const h3 = document.createElement("h3");
44396
+ h3.textContent = "\u8868\u60C5\u5305\uFF08\u6D4B\u8BD5\u529F\u80FD\uFF09";
44397
+ box.appendChild(h3);
44398
+ const wrap = document.createElement("div");
44399
+ wrap.className = "opt-col";
44400
+ const slider = document.createElement("input");
44401
+ slider.type = "range";
44402
+ slider.min = "0";
44403
+ slider.max = String(MEME_MODES.length - 1);
44404
+ slider.step = "1";
44405
+ const cur = Math.max(0, MEME_MODES.indexOf(serverSettings.memesMode || "off"));
44406
+ slider.value = String(cur);
44407
+ const lab = document.createElement("div");
44408
+ lab.className = "opt-lab";
44409
+ lab.textContent = MEME_LABELS[cur];
44410
+ slider.oninput = () => {
44411
+ lab.textContent = MEME_LABELS[Number(slider.value)];
44412
+ };
44413
+ slider.onchange = () => {
44414
+ const mode = MEME_MODES[Number(slider.value)];
44415
+ serverSettings.memesMode = mode;
44416
+ fetch(BASE + "/api/settings", {
44417
+ method: "POST",
44418
+ headers: { "content-type": "application/json" },
44419
+ body: JSON.stringify({ settings: { memesMode: mode } })
44420
+ }).catch(() => {
44421
+ });
44422
+ };
44423
+ const note = document.createElement("div");
44424
+ note.className = "opt-note";
44425
+ note.textContent = "\u81EA\u5E26\u90A3\u5957\u662F\u590F\u94C3\u7684 Q \u7248\u8D34\u7EB8\uFF0C\u4E8C\u5341\u5F20\uFF1Bdsh-meme \u90A3\u5957\u501F\u7528\u4F60\u672C\u673A\u5DF2\u7ECF\u88C5\u7684\u56FE\u5E93\u3002\u529F\u80FD\u8FD8\u5728\u8BD5\u9A8C\uFF0C\u53EF\u80FD\u4F1A\u8C03\u3002";
44426
+ wrap.appendChild(slider);
44427
+ wrap.appendChild(lab);
44428
+ wrap.appendChild(note);
44429
+ box.appendChild(wrap);
44394
44430
  }
44395
44431
  function loadSettings() {
44396
44432
  fetch(BASE + "/api/settings").then((r2) => r2.json()).then((d2) => {
package/lib/index.js CHANGED
@@ -25,7 +25,8 @@ const MEME_TAGS = {
25
25
 
26
26
  const DEFAULT_SETTINGS = {
27
27
  proactive: true,
28
- memes: true,
28
+ /** off | own | both | external —— 测试功能,默认关 */
29
+ memesMode: 'off',
29
30
  choices: true,
30
31
  memory: true,
31
32
  follow: true,
@@ -35,6 +36,45 @@ const DEFAULT_SETTINGS = {
35
36
  sound: true,
36
37
  };
37
38
 
39
+ const MEMES_MODES = ['off', 'own', 'both', 'external'];
40
+ const OWN_MEME_DIR = join(ASSETS, 'memes');
41
+
42
+ function readOwnMemes() {
43
+ try {
44
+ const m = JSON.parse(readFileSync(join(OWN_MEME_DIR, 'manifest.json'), 'utf8'));
45
+ return Array.isArray(m.items) ? m.items : [];
46
+ } catch {
47
+ return [];
48
+ }
49
+ }
50
+
51
+ /** 从自带那套里按情绪挑一张,返回文件名 */
52
+ function pickOwnMeme(mood) {
53
+ const items = readOwnMemes();
54
+ if (!items.length) return null;
55
+ const hit = items.filter((it) => Array.isArray(it.moods) && it.moods.includes(mood));
56
+ const pool = hit.length ? hit : items;
57
+ const it = pool[Math.floor(Math.random() * pool.length)];
58
+ return it && it.file ? it.file : null;
59
+ }
60
+
61
+ /** 按模式挑一张配图,返回可直接用的 URL */
62
+ function pickImage(mode, mood, label) {
63
+ const m = MEMES_MODES.includes(mode) ? mode : 'off';
64
+ if (m === 'off') return null;
65
+ const pool = [];
66
+ if (m === 'own' || m === 'both') {
67
+ const f = (label && pickOwnMemeByLabel(label)) || pickOwnMeme(mood);
68
+ if (f) pool.push('/dsh-live2d/ownmeme/' + encodeURIComponent(f));
69
+ }
70
+ if (m === 'external' || m === 'both') {
71
+ const f = pickMeme(mood);
72
+ if (f) pool.push('/dsh-live2d/media/' + f.split('/').map(encodeURIComponent).join('/'));
73
+ }
74
+ if (!pool.length) return null;
75
+ return pool[Math.floor(Math.random() * pool.length)];
76
+ }
77
+
38
78
  function settingsOf(cfg) {
39
79
  return { ...DEFAULT_SETTINGS, ...(cfg?.settings || {}) };
40
80
  }
@@ -78,7 +118,30 @@ function splitChoices(raw) {
78
118
  return { text: raw.slice(0, m.index).trim(), choices };
79
119
  }
80
120
 
81
- /** 粗判情绪,配图用 */
121
+ /** 先摘「||图:名字」,再摘「||选项1|选项2」 */
122
+ function splitMeta(raw) {
123
+ let text = String(raw || '');
124
+ let imageLabel = '';
125
+ const imgMatch = /\|\|\s*图\s*[::]\s*([^\n|]+)\s*$/m.exec(text);
126
+ if (imgMatch) {
127
+ imageLabel = imgMatch[1].trim().replace(/^["“「『']+|["”」』']+$/g, '');
128
+ text = (text.slice(0, imgMatch.index) + text.slice(imgMatch.index + imgMatch[0].length)).trim();
129
+ }
130
+ const parsed = splitChoices(text);
131
+ return { text: parsed.text, choices: parsed.choices, imageLabel };
132
+ }
133
+
134
+ /** 按名字取自带那张 */
135
+ function pickOwnMemeByLabel(label) {
136
+ const items = readOwnMemes();
137
+ if (!items.length || !label) return null;
138
+ const key = String(label).trim();
139
+ const hit =
140
+ items.find((it) => it.label === key) ||
141
+ items.find((it) => String(it.file).replace(/\.\w+$/, '') === key) ||
142
+ items.find((it) => key && String(it.label).includes(key));
143
+ return hit ? hit.file : null;
144
+ }
82
145
  function guessMood(text) {
83
146
  const s = String(text || '');
84
147
  if (/[气烦讨厌]/.test(s)) return '生气';
@@ -383,6 +446,12 @@ async function handle(req, res, ctx) {
383
446
  return file ? sendFile(res, file) : send(res, 403, 'forbidden');
384
447
  }
385
448
 
449
+ // 自带的 Q 版表情包
450
+ if (path.startsWith('/ownmeme/')) {
451
+ const file = safeJoin(OWN_MEME_DIR, decodeURIComponent(path.slice('/ownmeme/'.length)));
452
+ return file ? sendFile(res, file) : send(res, 403, 'forbidden');
453
+ }
454
+
386
455
  // 表情图库
387
456
  if (path.startsWith('/media/')) {
388
457
  const file = safeJoin(MEMES_ROOT, decodeURIComponent(path.slice('/media/'.length)));
@@ -426,6 +495,14 @@ async function handle(req, res, ctx) {
426
495
  '他说:' + text,
427
496
  '回他一句。只输出这句话本身:一到三句,短,口语,像发消息。不要解释,不要加引号,不要旁白,不要括号里的动作。',
428
497
  st.choices ? '如果你想让他挑,可以在最后另起一行写 ||选项一|选项二,最多三项。' : '',
498
+ st.memesMode === 'own' || st.memesMode === 'both'
499
+ ? '如果你想附一张贴图,就在最后另起一行写 ||图:名字。能挑的名字有:' +
500
+ readOwnMemes()
501
+ .map((it) => it.label)
502
+ .filter(Boolean)
503
+ .join('、') +
504
+ '。不想配就别写。'
505
+ : '',
429
506
  ]
430
507
  .filter(Boolean)
431
508
  .join('\n\n');
@@ -437,7 +514,7 @@ async function handle(req, res, ctx) {
437
514
  return sendJson(res, 502, { error: '生成失败:' + (e instanceof Error ? e.message : String(e)) });
438
515
  }
439
516
  LAST_ACTIVE.at = Date.now();
440
- const parsed = splitChoices(reply || '');
517
+ const parsed = splitMeta(reply || '');
441
518
  let line = parsed.text;
442
519
  let choices = st.choices ? parsed.choices : [];
443
520
  if (line.length > 240) line = line.slice(0, 240);
@@ -445,7 +522,15 @@ async function handle(req, res, ctx) {
445
522
  choices = FALLBACK_CHOICES[guessMood(line)] || FALLBACK_CHOICES['平静'];
446
523
  }
447
524
  if (!line && !choices.length) return sendJson(res, 200, { ok: true, reply: '', id });
448
- const image = st.memes && guessMood(line) && Math.random() < 0.35 ? pickMeme(guessMood(line)) : null;
525
+ const moodNow = guessMood(line);
526
+ const image =
527
+ st.memesMode === 'off'
528
+ ? null
529
+ : parsed.imageLabel
530
+ ? pickImage(st.memesMode, moodNow, parsed.imageLabel)
531
+ : Math.random() < 0.35
532
+ ? pickImage(st.memesMode, moodNow)
533
+ : null;
449
534
 
450
535
  writeChat(id, hist.concat([
451
536
  { role: 'user', text, at: Date.now() },
@@ -491,6 +576,9 @@ async function handle(req, res, ctx) {
491
576
  for (const k of Object.keys(DEFAULT_SETTINGS)) {
492
577
  if (typeof patch?.[k] === 'boolean') next[k] = patch[k];
493
578
  }
579
+ if (typeof patch?.memesMode === 'string' && MEMES_MODES.includes(patch.memesMode)) {
580
+ next.memesMode = patch.memesMode;
581
+ }
494
582
  writeConfig({ ...cfg, settings: next });
495
583
  return sendJson(res, 200, { ok: true, settings: next });
496
584
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-live2dwaifu",
3
- "version": "0.6.0",
3
+ "version": "0.7.1",
4
4
  "description": "给 DSH 网页右下角请一个 Live2D 角色:会眨眼、会呼吸、视线跟着鼠标、点她有反应、说话时嘴动、心情变了眉毛跟着动。模型不用内置——谁装谁放自己的文件夹;支持自定义 .exp3.json 表情和 .motion3.json 动作,卡片能拖能缩,背景可换。",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",