dsh-live2dwaifu 0.5.0 → 0.6.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/assets/persona.html +149 -0
- package/lib/client.js +2 -0
- package/lib/index.js +37 -5
- package/package.json +1 -1
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="zh">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<title>人设卡 — dsh-live2dwaifu</title>
|
|
6
|
+
<style>
|
|
7
|
+
:root { --bg:#12141a; --card:#171a20; --line:#2c313a; --txt:#dfe4ec; --dim:#7d858f; --acc:#6aa9ff; }
|
|
8
|
+
* { box-sizing: border-box; }
|
|
9
|
+
body { margin:0; min-height:100vh; background:radial-gradient(1100px 560px at 50% -10%, #1b1f27, var(--bg));
|
|
10
|
+
color:var(--txt); font:13px/1.6 "Microsoft YaHei", system-ui, sans-serif;
|
|
11
|
+
display:flex; align-items:flex-start; justify-content:center; gap:26px; padding:26px; flex-wrap:wrap; }
|
|
12
|
+
.left { display:flex; flex-direction:column; gap:10px; width:330px; }
|
|
13
|
+
.card { width:330px; height:440px; background:var(--card); border:1px solid var(--line); border-radius:14px;
|
|
14
|
+
overflow:hidden; box-shadow:0 18px 50px rgba(0,0,0,.45); }
|
|
15
|
+
.card iframe { display:block; width:100%; height:100%; border:0; }
|
|
16
|
+
.panel { width:460px; }
|
|
17
|
+
.panel h1 { font-size:15px; margin:0 0 3px; font-weight:600; }
|
|
18
|
+
.panel p.sub { margin:0 0 14px; color:var(--dim); font-size:12px; }
|
|
19
|
+
.group { border:1px solid var(--line); border-radius:12px; padding:12px; margin-bottom:12px; background:#15181e; }
|
|
20
|
+
.group h2 { font-size:11px; letter-spacing:.6px; color:var(--dim); margin:0 0 10px; font-weight:600; }
|
|
21
|
+
select { width:100%; padding:8px 10px; border-radius:9px; border:1px solid #3c424c; background:#181c22;
|
|
22
|
+
color:var(--txt); font:inherit; font-size:13px; }
|
|
23
|
+
textarea { width:100%; height:300px; resize:vertical; padding:10px 12px; border-radius:10px;
|
|
24
|
+
border:1px solid #3c424c; background:#181c22; color:var(--txt);
|
|
25
|
+
font:12px/1.75 "Microsoft YaHei", system-ui, sans-serif; outline:none; }
|
|
26
|
+
textarea:focus { border-color:#3f4a5c; }
|
|
27
|
+
.switch { display:flex; align-items:flex-start; gap:10px; padding:4px 0; cursor:pointer; }
|
|
28
|
+
.switch input { margin-top:3px; accent-color:var(--acc); }
|
|
29
|
+
.switch b { font-weight:600; font-size:12.5px; }
|
|
30
|
+
.switch em { display:block; font-style:normal; color:var(--dim); font-size:11.5px; line-height:1.6; }
|
|
31
|
+
.foot { display:flex; align-items:center; gap:10px; }
|
|
32
|
+
button.act { padding:9px 16px; border-radius:9px; border:1px solid #3c424c; background:#232830;
|
|
33
|
+
color:#dfe4ec; cursor:pointer; font:inherit; font-size:12px; }
|
|
34
|
+
button.act.primary { background:#27405f; border-color:#3e6b9e; color:#eaf2ff; }
|
|
35
|
+
button.act:hover { filter:brightness(1.12); }
|
|
36
|
+
.state { color:var(--dim); font-size:11px; margin-left:auto; }
|
|
37
|
+
.tip { color:#6f7783; font-size:11px; margin-top:10px; line-height:1.7; }
|
|
38
|
+
</style>
|
|
39
|
+
</head>
|
|
40
|
+
<body>
|
|
41
|
+
|
|
42
|
+
<div class="left">
|
|
43
|
+
<div class="card"><iframe id="live" src="/dsh-live2d/viewer?embed=1"></iframe></div>
|
|
44
|
+
<div class="tip">左边就是她本人。改完存一下,她下一句就按新的人设说。</div>
|
|
45
|
+
</div>
|
|
46
|
+
|
|
47
|
+
<div class="panel">
|
|
48
|
+
<h1>人设卡</h1>
|
|
49
|
+
<p class="sub">她是谁、怎么说话,全在这段字里。</p>
|
|
50
|
+
|
|
51
|
+
<div class="group">
|
|
52
|
+
<h2>配给哪个模型</h2>
|
|
53
|
+
<select id="models"></select>
|
|
54
|
+
</div>
|
|
55
|
+
|
|
56
|
+
<div class="group">
|
|
57
|
+
<h2>人设</h2>
|
|
58
|
+
<textarea id="persona" spellcheck="false"></textarea>
|
|
59
|
+
</div>
|
|
60
|
+
|
|
61
|
+
<div class="group">
|
|
62
|
+
<h2>世界隔离</h2>
|
|
63
|
+
<label class="switch">
|
|
64
|
+
<input type="checkbox" id="isolation" checked />
|
|
65
|
+
<span>
|
|
66
|
+
<b>让她待在自己的世界里</b>
|
|
67
|
+
<em>打开后,她不知道什么是程序、界面、插件、模型。问起来就当你在她身边,不提“系统”“设置”“网页”。关掉她就会老老实实承认自己是插件里的角色。</em>
|
|
68
|
+
</span>
|
|
69
|
+
</label>
|
|
70
|
+
</div>
|
|
71
|
+
|
|
72
|
+
<div class="foot">
|
|
73
|
+
<button class="act primary" id="save">存</button>
|
|
74
|
+
<button class="act" id="reload">撤销改动</button>
|
|
75
|
+
<span class="state" id="state"></span>
|
|
76
|
+
</div>
|
|
77
|
+
<div class="tip">存在 <code>~/.dsh/dsh-live2d/models/<id>/meta.json</code> 的 <code>persona</code> 字段里,想手改也行。</div>
|
|
78
|
+
</div>
|
|
79
|
+
|
|
80
|
+
<script>
|
|
81
|
+
(function () {
|
|
82
|
+
var frame = document.getElementById('live');
|
|
83
|
+
var sel = document.getElementById('models');
|
|
84
|
+
var area = document.getElementById('persona');
|
|
85
|
+
var iso = document.getElementById('isolation');
|
|
86
|
+
var state = document.getElementById('state');
|
|
87
|
+
var modelId = '';
|
|
88
|
+
|
|
89
|
+
function $(id) { return document.getElementById(id); }
|
|
90
|
+
|
|
91
|
+
function load() {
|
|
92
|
+
if (!modelId) return;
|
|
93
|
+
state.textContent = '读…';
|
|
94
|
+
fetch('/dsh-live2d/api/persona?id=' + encodeURIComponent(modelId))
|
|
95
|
+
.then(function (r) { return r.json(); })
|
|
96
|
+
.then(function (d) {
|
|
97
|
+
area.value = d.persona || '';
|
|
98
|
+
iso.checked = d.isolation !== false;
|
|
99
|
+
state.textContent = d.custom ? '这份是手写的' : '还是默认那份';
|
|
100
|
+
})
|
|
101
|
+
.catch(function (e) { state.textContent = '读不了:' + e.message; });
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function save() {
|
|
105
|
+
if (!modelId) return;
|
|
106
|
+
state.textContent = '存…';
|
|
107
|
+
fetch('/dsh-live2d/api/persona', {
|
|
108
|
+
method: 'POST',
|
|
109
|
+
headers: { 'content-type': 'application/json' },
|
|
110
|
+
body: JSON.stringify({ id: modelId, persona: area.value, isolation: iso.checked })
|
|
111
|
+
})
|
|
112
|
+
.then(function (r) { return r.json(); })
|
|
113
|
+
.then(function (d) {
|
|
114
|
+
state.textContent = d.ok ? '存好了' : '没存上:' + (d.error || '?');
|
|
115
|
+
if (d.ok) {
|
|
116
|
+
frame.src = '/dsh-live2d/viewer?embed=1&t=' + Date.now();
|
|
117
|
+
}
|
|
118
|
+
})
|
|
119
|
+
.catch(function (e) { state.textContent = '没存上:' + e.message; });
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
$('save').addEventListener('click', save);
|
|
123
|
+
$('reload').addEventListener('click', load);
|
|
124
|
+
sel.addEventListener('change', function () {
|
|
125
|
+
modelId = sel.value;
|
|
126
|
+
load();
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
fetch('/dsh-live2d/api/models')
|
|
130
|
+
.then(function (r) { return r.json(); })
|
|
131
|
+
.then(function (d) {
|
|
132
|
+
var list = d.models || [];
|
|
133
|
+
sel.innerHTML = '';
|
|
134
|
+
list.forEach(function (m) {
|
|
135
|
+
var o = document.createElement('option');
|
|
136
|
+
o.value = m.id;
|
|
137
|
+
o.textContent = (m.name || m.id) + (m.id === d.selected ? '(正在显示)' : '');
|
|
138
|
+
sel.appendChild(o);
|
|
139
|
+
});
|
|
140
|
+
modelId = d.selected || (list[0] && list[0].id) || '';
|
|
141
|
+
if (modelId) sel.value = modelId;
|
|
142
|
+
if (!list.length) { state.textContent = '还没有模型'; return; }
|
|
143
|
+
load();
|
|
144
|
+
})
|
|
145
|
+
.catch(function (e) { state.textContent = '读不了模型表:' + e.message; });
|
|
146
|
+
})();
|
|
147
|
+
</script>
|
|
148
|
+
</body>
|
|
149
|
+
</html>
|
package/lib/client.js
CHANGED
|
@@ -177,6 +177,7 @@
|
|
|
177
177
|
|
|
178
178
|
const manage = () => ref.current && ref.current.contentWindow.postMessage({ type: 'dsh-live2d/manage' }, '*');
|
|
179
179
|
const openTone = () => window.open(BASE + '/tone', '_blank', 'width=1140,height=780');
|
|
180
|
+
const openPersona = () => window.open(BASE + '/persona', '_blank', 'width=1080,height=740');
|
|
180
181
|
|
|
181
182
|
const send = () => {
|
|
182
183
|
const t = draft.trim();
|
|
@@ -222,6 +223,7 @@
|
|
|
222
223
|
open && h('button', { title: '模型管理', onPointerDown: noDrag, onClick: manage }, '⚙'),
|
|
223
224
|
open && h('button', { title: '背景:' + BG_LABEL[bg], onPointerDown: noDrag, onClick: cycleBg }, '◐'),
|
|
224
225
|
open && h('button', { title: '音色', onPointerDown: noDrag, onClick: openTone }, '♪'),
|
|
226
|
+
open && h('button', { title: '人设卡', onPointerDown: noDrag, onClick: openPersona }, '✎'),
|
|
225
227
|
open && h('button', { title: '刷新', onPointerDown: noDrag, onClick: () => setNonce((n) => n + 1) }, '↻'),
|
|
226
228
|
h('button', { title: open ? '收起' : '展开', onPointerDown: noDrag, onClick: () => setOpen(!open) }, open ? '⌄' : '⌃'),
|
|
227
229
|
),
|
package/lib/index.js
CHANGED
|
@@ -100,6 +100,23 @@ const DEFAULT_PERSONA = [
|
|
|
100
100
|
'不解释心情,不总结,不升华,不写括号里的动作。',
|
|
101
101
|
].join('\n');
|
|
102
102
|
|
|
103
|
+
/** 没说清要让你挑什么的时候,按情绪给两个短答 */
|
|
104
|
+
const FALLBACK_CHOICES = {
|
|
105
|
+
开心: ['好', '嗯嗯'],
|
|
106
|
+
生气: ['……', '别气'],
|
|
107
|
+
委屈: ['我在', '怎么了'],
|
|
108
|
+
害羞: ['唔', '别看'],
|
|
109
|
+
无语: ['行吧', '你说'],
|
|
110
|
+
困: ['睡吧', '嗯'],
|
|
111
|
+
平静: ['嗯', '在的'],
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const ISOLATION = [
|
|
115
|
+
'【世界隔离】你不知道什么是程序、界面、插件、模型、参数。',
|
|
116
|
+
'有人问起,你就当他就站在你身边。不要提“系统”“设置”“网页”“token”这些词。',
|
|
117
|
+
'你不知道他那边什么样,得他讲给你听。',
|
|
118
|
+
].join('\n');
|
|
119
|
+
|
|
103
120
|
function personaOf(meta) {
|
|
104
121
|
const p = typeof meta?.persona === 'string' ? meta.persona.trim() : '';
|
|
105
122
|
return p || DEFAULT_PERSONA;
|
|
@@ -403,6 +420,7 @@ async function handle(req, res, ctx) {
|
|
|
403
420
|
.join('\n');
|
|
404
421
|
const prompt = [
|
|
405
422
|
personaOf(meta),
|
|
423
|
+
meta.isolation === false ? '' : ISOLATION,
|
|
406
424
|
'现在是 ' + nowText() + '。',
|
|
407
425
|
lines ? '你们之前说的:\n' + lines : '',
|
|
408
426
|
'他说:' + text,
|
|
@@ -421,8 +439,11 @@ async function handle(req, res, ctx) {
|
|
|
421
439
|
LAST_ACTIVE.at = Date.now();
|
|
422
440
|
const parsed = splitChoices(reply || '');
|
|
423
441
|
let line = parsed.text;
|
|
424
|
-
|
|
442
|
+
let choices = st.choices ? parsed.choices : [];
|
|
425
443
|
if (line.length > 240) line = line.slice(0, 240);
|
|
444
|
+
if (st.choices && !choices.length && line) {
|
|
445
|
+
choices = FALLBACK_CHOICES[guessMood(line)] || FALLBACK_CHOICES['平静'];
|
|
446
|
+
}
|
|
426
447
|
if (!line && !choices.length) return sendJson(res, 200, { ok: true, reply: '', id });
|
|
427
448
|
const image = st.memes && guessMood(line) && Math.random() < 0.35 ? pickMeme(guessMood(line)) : null;
|
|
428
449
|
|
|
@@ -437,7 +458,12 @@ async function handle(req, res, ctx) {
|
|
|
437
458
|
if (path === '/api/persona' && method === 'GET') {
|
|
438
459
|
const id = String(url.searchParams.get('id') ?? '');
|
|
439
460
|
const meta = id ? readMeta(id) : null;
|
|
440
|
-
return sendJson(res, 200, {
|
|
461
|
+
return sendJson(res, 200, {
|
|
462
|
+
id,
|
|
463
|
+
persona: personaOf(meta),
|
|
464
|
+
custom: typeof meta?.persona === 'string' && !!meta.persona.trim(),
|
|
465
|
+
isolation: meta?.isolation !== false,
|
|
466
|
+
});
|
|
441
467
|
}
|
|
442
468
|
|
|
443
469
|
if (path === '/api/persona' && method === 'POST') {
|
|
@@ -445,9 +471,10 @@ async function handle(req, res, ctx) {
|
|
|
445
471
|
const id = String(body.id ?? '');
|
|
446
472
|
const meta = id ? readMeta(id) : null;
|
|
447
473
|
if (!meta) return sendJson(res, 404, { error: 'model not found' });
|
|
448
|
-
const persona = typeof body.persona === 'string' ? body.persona :
|
|
449
|
-
|
|
450
|
-
|
|
474
|
+
const persona = typeof body.persona === 'string' ? body.persona : meta.persona;
|
|
475
|
+
const isolation = typeof body.isolation === 'boolean' ? body.isolation : meta.isolation !== false;
|
|
476
|
+
writeFileSync(join(MODELS, id, 'meta.json'), JSON.stringify({ ...meta, persona, isolation }, null, 2), 'utf8');
|
|
477
|
+
return sendJson(res, 200, { ok: true, persona: personaOf({ persona }), isolation });
|
|
451
478
|
}
|
|
452
479
|
|
|
453
480
|
// 开关:哪些功能开着
|
|
@@ -490,6 +517,11 @@ async function handle(req, res, ctx) {
|
|
|
490
517
|
return sendJson(res, 200, { seq: outSeq, items: OUTBOX.filter((m) => m.seq > since) });
|
|
491
518
|
}
|
|
492
519
|
|
|
520
|
+
// 人设卡页
|
|
521
|
+
if (path === '/persona' || path === '/persona.html') {
|
|
522
|
+
return sendFile(res, join(ASSETS, 'persona.html'), MIME['.html']);
|
|
523
|
+
}
|
|
524
|
+
|
|
493
525
|
// 音色调音页
|
|
494
526
|
if (path === '/tone' || path === '/tone.html') {
|
|
495
527
|
return sendFile(res, join(ASSETS, 'tone.html'), MIME['.html']);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-live2dwaifu",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "给 DSH 网页右下角请一个 Live2D 角色:会眨眼、会呼吸、视线跟着鼠标、点她有反应、说话时嘴动、心情变了眉毛跟着动。模型不用内置——谁装谁放自己的文件夹;支持自定义 .exp3.json 表情和 .motion3.json 动作,卡片能拖能缩,背景可换。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|