dsh-live2dwaifu 0.7.1 → 0.8.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 +24 -0
- package/assets/mobile.html +86 -0
- package/lib/index.js +51 -8
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -33,6 +33,12 @@
|
|
|
33
33
|
|
|
34
34
|
**也能直接跟她说**:卡片底下有个小框,打一句回车,她回你。这条线**不进你的聊天记录**——后台直接问模型,往来只存在 `models/<id>/chat.json` 里。她说的话按人设卡现场写,不是复读;人设卡就是 `models/<id>/meta.json` 里的 `persona` 字段,没写就用默认那份(若妍)。
|
|
35
35
|
|
|
36
|
+
**她记得住**:聊够三十句,前面二十句会被压成一小段小结挂在记忆里,不会一过二十句就把你说过的话全忘掉。嫌费 token 就在 ⚙ 里把「记住之前说的」关掉。
|
|
37
|
+
|
|
38
|
+
**她知道几点、也知道自己此刻在干什么**:提示词里会让她按人设推断这个时间点的处境——早上在实验室、半夜还没睡——话里自然带着那个味道,不用你提醒。
|
|
39
|
+
|
|
40
|
+
**手机上一页就够**:手机连同一个地址,打开 `/dsh-live2d/chat`,就是她的脸加一个输入框,不用看整个 DSH。
|
|
41
|
+
|
|
36
42
|
**她会自己开口**:十几分钟没人理她,她就找个话头说一句。嫌吵就把 `~/.dsh/dsh-live2d/config.json` 里的 `proactive` 设成 `false`。
|
|
37
43
|
|
|
38
44
|
**给别人留的口子**:im 机器人、别的插件、随手写的脚本,都能让她说话。
|
|
@@ -102,6 +108,24 @@ frame.contentWindow.postMessage({ type: 'dsh-live2d/manage' }, '*');
|
|
|
102
108
|
| POST | `/dsh-live2d/api/commit` | `{id,name}` 扫一遍 model3 并选中 |
|
|
103
109
|
| POST | `/dsh-live2d/api/delete` | `{id}` 删掉 |
|
|
104
110
|
|
|
111
|
+
## 写人设卡,请写细一点
|
|
112
|
+
|
|
113
|
+
她回得像不像人,全看那段字。别只写“一个可爱的女孩”——那样只会得到一段没味道的话。
|
|
114
|
+
|
|
115
|
+
值得写进去的:她是谁、几岁、做什么的、说话什么调子(短句还是长句、用不用标点)、有没有口头禅、怕什么、喜欢什么、跟你是什么关系、平时怎么称呼你、这个时间点通常在干嘛。
|
|
116
|
+
|
|
117
|
+
`models/<id>/meta.json` 里的 `persona` 想写多长写多长,写不写都行,不写就是默认那份。也可以点标题栏的 **✎** 在网页里改。
|
|
118
|
+
|
|
119
|
+
## 把她的表情包给 dsh-meme 用
|
|
120
|
+
|
|
121
|
+
自带那二十张是标准图库格式,导一次就能丢给 `dsh-meme`:
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
python tools/export-dsh-meme.py
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
产物在 `dsh-live2d/dsh-meme-pack/xialing-001/`(含 `manifest.json`、`memes/<tag>/`、`index.db`)。整个目录丢进 `~/.dsh/profiles/desktop/node_modules/dsh-meme/memes/` 下面就认了。
|
|
128
|
+
|
|
105
129
|
## 东西都放哪了
|
|
106
130
|
|
|
107
131
|
```
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="zh">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover" />
|
|
6
|
+
<meta name="theme-color" content="#12141a" />
|
|
7
|
+
<title>夏铃</title>
|
|
8
|
+
<style>
|
|
9
|
+
:root { --bg:#12141a; --line:#2c313a; --txt:#dfe4ec; --dim:#7d858f; --acc:#6aa9ff; }
|
|
10
|
+
* { box-sizing: border-box; -webkit-tap-highlight-color: transparent; }
|
|
11
|
+
html, body { margin:0; height:100%; overflow:hidden; background:var(--bg); color:var(--txt);
|
|
12
|
+
font:14px/1.5 "Microsoft YaHei", system-ui, sans-serif; }
|
|
13
|
+
#app { display:flex; flex-direction:column; height:100%; height:100dvh; }
|
|
14
|
+
#stage { flex:1; border:0; width:100%; background:transparent; }
|
|
15
|
+
#bar { flex:none; display:flex; gap:8px; padding:8px 10px calc(8px + env(safe-area-inset-bottom));
|
|
16
|
+
border-top:1px solid var(--line); background:#15181e; }
|
|
17
|
+
#bar input { flex:1; min-width:0; padding:11px 13px; border-radius:12px; border:1px solid #333842;
|
|
18
|
+
background:#1b1f27; color:var(--txt); font:inherit; outline:none; }
|
|
19
|
+
#bar input:focus { border-color:#3f4a5c; }
|
|
20
|
+
#bar button { flex:none; padding:0 16px; border-radius:12px; border:1px solid #3e6b9e;
|
|
21
|
+
background:#27405f; color:#eaf2ff; font:inherit; cursor:pointer; }
|
|
22
|
+
#bar button:active { filter:brightness(1.15); }
|
|
23
|
+
#hint { flex:none; text-align:center; font-size:11px; color:var(--dim); padding:0 0 6px; background:#15181e; }
|
|
24
|
+
</style>
|
|
25
|
+
</head>
|
|
26
|
+
<body>
|
|
27
|
+
<div id="app">
|
|
28
|
+
<iframe id="stage" src="/dsh-live2d/viewer?embed=1&bg=dark"></iframe>
|
|
29
|
+
<div id="hint">说一句,她回你。不进聊天记录。</div>
|
|
30
|
+
<div id="bar">
|
|
31
|
+
<input id="text" placeholder="跟她说一句" autocomplete="off" enterkeyhint="send" />
|
|
32
|
+
<button id="send">说</button>
|
|
33
|
+
</div>
|
|
34
|
+
</div>
|
|
35
|
+
<script>
|
|
36
|
+
(function () {
|
|
37
|
+
var frame = document.getElementById('stage');
|
|
38
|
+
var box = document.getElementById('text');
|
|
39
|
+
var btn = document.getElementById('send');
|
|
40
|
+
var busy = false;
|
|
41
|
+
|
|
42
|
+
function say(text, image, choices) {
|
|
43
|
+
try {
|
|
44
|
+
frame.contentWindow.postMessage({ type: 'dsh-live2d/say', text: text || '', image: image, choices: choices }, '*');
|
|
45
|
+
} catch (e) {}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function send() {
|
|
49
|
+
var t = box.value.trim();
|
|
50
|
+
if (!t || busy) return;
|
|
51
|
+
busy = true;
|
|
52
|
+
box.value = '';
|
|
53
|
+
fetch('/dsh-live2d/api/chat', {
|
|
54
|
+
method: 'POST',
|
|
55
|
+
headers: { 'content-type': 'application/json' },
|
|
56
|
+
body: JSON.stringify({ text: t })
|
|
57
|
+
})
|
|
58
|
+
.then(function (r) { return r.json(); })
|
|
59
|
+
.then(function (d) {
|
|
60
|
+
if (d.reply || d.image) say(d.reply, d.image, d.choices);
|
|
61
|
+
})
|
|
62
|
+
.catch(function () {})
|
|
63
|
+
.finally(function () { busy = false; });
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
btn.addEventListener('click', send);
|
|
67
|
+
box.addEventListener('keydown', function (e) {
|
|
68
|
+
if (e.key === 'Enter') send();
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// 主动开口也收着
|
|
72
|
+
var seq = null;
|
|
73
|
+
setInterval(function () {
|
|
74
|
+
fetch('/dsh-live2d/api/poll?since=' + (seq === null ? 0 : seq))
|
|
75
|
+
.then(function (r) { return r.json(); })
|
|
76
|
+
.then(function (d) {
|
|
77
|
+
if (seq === null) { seq = d.seq || 0; return; }
|
|
78
|
+
seq = d.seq || seq;
|
|
79
|
+
(d.items || []).forEach(function (it) { say(it.text, it.image, it.choices); });
|
|
80
|
+
})
|
|
81
|
+
.catch(function () {});
|
|
82
|
+
}, 3000);
|
|
83
|
+
})();
|
|
84
|
+
</script>
|
|
85
|
+
</body>
|
|
86
|
+
</html>
|
package/lib/index.js
CHANGED
|
@@ -185,21 +185,51 @@ function personaOf(meta) {
|
|
|
185
185
|
return p || DEFAULT_PERSONA;
|
|
186
186
|
}
|
|
187
187
|
|
|
188
|
-
function
|
|
188
|
+
function readChatStore(id) {
|
|
189
189
|
try {
|
|
190
190
|
const d = JSON.parse(readFileSync(join(MODELS, id, 'chat.json'), 'utf8'));
|
|
191
|
-
return
|
|
191
|
+
return {
|
|
192
|
+
messages: Array.isArray(d.messages) ? d.messages : [],
|
|
193
|
+
summary: typeof d.summary === 'string' ? d.summary : '',
|
|
194
|
+
};
|
|
192
195
|
} catch {
|
|
193
|
-
return [];
|
|
196
|
+
return { messages: [], summary: '' };
|
|
194
197
|
}
|
|
195
198
|
}
|
|
196
199
|
|
|
197
|
-
function
|
|
200
|
+
function readChat(id) {
|
|
201
|
+
return readChatStore(id).messages;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function writeChat(id, messages, summary) {
|
|
198
205
|
try {
|
|
199
|
-
|
|
206
|
+
const prev = readChatStore(id);
|
|
207
|
+
const data = {
|
|
208
|
+
messages: messages.slice(-40),
|
|
209
|
+
summary: summary === undefined ? prev.summary : summary,
|
|
210
|
+
};
|
|
211
|
+
writeFileSync(join(MODELS, id, 'chat.json'), JSON.stringify(data, null, 2), 'utf8');
|
|
200
212
|
} catch {}
|
|
201
213
|
}
|
|
202
214
|
|
|
215
|
+
/** 攒够了就把前二十句压成一段小结,人物记得住更早的事 */
|
|
216
|
+
async function compactChat(slot, store) {
|
|
217
|
+
const head = store.messages.slice(0, 20);
|
|
218
|
+
const tail = store.messages.slice(20);
|
|
219
|
+
if (!head.length) return store;
|
|
220
|
+
const text = head.map((m) => (m.role === 'user' ? '他:' : '你:') + String(m.text ?? '')).join('\n');
|
|
221
|
+
const prompt =
|
|
222
|
+
'把下面这段对话压成一段不超过一百二十字的小结,只留事实和情绪走向,用第三人称写,不要引号,不要分点:\n' + text;
|
|
223
|
+
let line = '';
|
|
224
|
+
try {
|
|
225
|
+
line = await generate(slot, prompt, 600);
|
|
226
|
+
} catch {
|
|
227
|
+
line = '';
|
|
228
|
+
}
|
|
229
|
+
const summary = [store.summary, line].filter(Boolean).join('\n').slice(-1200);
|
|
230
|
+
return { messages: tail, summary };
|
|
231
|
+
}
|
|
232
|
+
|
|
203
233
|
/* ── 出口队列:谁能说话,都往这儿扔 ───────────────────────── */
|
|
204
234
|
const OUTBOX = [];
|
|
205
235
|
const LAST_ACTIVE = { at: Date.now() };
|
|
@@ -482,7 +512,14 @@ async function handle(req, res, ctx) {
|
|
|
482
512
|
if (!slot) return sendJson(res, 503, { error: '模型接口不可用' });
|
|
483
513
|
|
|
484
514
|
const st = settingsOf(readConfig());
|
|
485
|
-
|
|
515
|
+
let store = st.memory ? readChatStore(id) : { messages: [], summary: '' };
|
|
516
|
+
if (st.memory && store.messages.length > 30) {
|
|
517
|
+
try {
|
|
518
|
+
store = await compactChat(slot, store);
|
|
519
|
+
writeChat(id, store.messages, store.summary);
|
|
520
|
+
} catch {}
|
|
521
|
+
}
|
|
522
|
+
const hist = store.messages;
|
|
486
523
|
const lines = hist
|
|
487
524
|
.slice(-8)
|
|
488
525
|
.map((m) => (m.role === 'user' ? '他:' : '你:') + String(m.text ?? ''))
|
|
@@ -490,7 +527,8 @@ async function handle(req, res, ctx) {
|
|
|
490
527
|
const prompt = [
|
|
491
528
|
personaOf(meta),
|
|
492
529
|
meta.isolation === false ? '' : ISOLATION,
|
|
493
|
-
'现在是 ' + nowText() + '
|
|
530
|
+
'现在是 ' + nowText() + '。按你这个人设,这个时间点你多半在做什么?让这句话带上那个处境,但不要说破。',
|
|
531
|
+
store.summary ? '之前聊过的,你还记得:\n' + store.summary : '',
|
|
494
532
|
lines ? '你们之前说的:\n' + lines : '',
|
|
495
533
|
'他说:' + text,
|
|
496
534
|
'回他一句。只输出这句话本身:一到三句,短,口语,像发消息。不要解释,不要加引号,不要旁白,不要括号里的动作。',
|
|
@@ -535,7 +573,7 @@ async function handle(req, res, ctx) {
|
|
|
535
573
|
writeChat(id, hist.concat([
|
|
536
574
|
{ role: 'user', text, at: Date.now() },
|
|
537
575
|
{ role: 'assistant', text: line, at: Date.now() },
|
|
538
|
-
]));
|
|
576
|
+
]), store.summary);
|
|
539
577
|
return sendJson(res, 200, { ok: true, reply: line, choices, image: image || undefined, id });
|
|
540
578
|
}
|
|
541
579
|
|
|
@@ -605,6 +643,11 @@ async function handle(req, res, ctx) {
|
|
|
605
643
|
return sendJson(res, 200, { seq: outSeq, items: OUTBOX.filter((m) => m.seq > since) });
|
|
606
644
|
}
|
|
607
645
|
|
|
646
|
+
// 手机上一页够用:她的脸 + 一个输入框
|
|
647
|
+
if (path === '/chat' || path === '/mobile') {
|
|
648
|
+
return sendFile(res, join(ASSETS, 'mobile.html'), MIME['.html']);
|
|
649
|
+
}
|
|
650
|
+
|
|
608
651
|
// 人设卡页
|
|
609
652
|
if (path === '/persona' || path === '/persona.html') {
|
|
610
653
|
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": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "给 DSH 网页右下角请一个 Live2D 角色:会眨眼、会呼吸、视线跟着鼠标、点她有反应、说话时嘴动、心情变了眉毛跟着动。模型不用内置——谁装谁放自己的文件夹;支持自定义 .exp3.json 表情和 .motion3.json 动作,卡片能拖能缩,背景可换。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|