@my-life-buddies/cli 0.10.0 → 0.13.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 +26 -12
- package/dist/bin/buddy.js +68 -12
- package/dist/bin/buddy.js.map +3 -3
- package/dist/bin/core.js +1077 -454
- package/dist/bin/core.js.map +4 -4
- package/dist/bin/preview.js +164 -2
- package/dist/bin/preview.js.map +4 -4
- package/dist/web/app.css +38 -0
- package/dist/web/app.js +103 -12
- package/dist/web/index.html +1 -1
- package/dist/web/widgets.js +4 -3
- package/package.json +3 -3
- package/resources/dada-object-avatar/SKILL.md +37 -0
- package/resources/dada-object-avatar/agents/openai.yaml +4 -0
- package/resources/dada-object-avatar/assets/examples/cat.png +0 -0
- package/resources/dada-object-avatar/assets/examples/cooking.png +0 -0
- package/resources/dada-object-avatar/assets/examples/divination.png +0 -0
- package/resources/dada-object-avatar/assets/examples/doctor.png +0 -0
- package/resources/dada-object-avatar/assets/examples/relationship.png +0 -0
- package/resources/dada-object-avatar/assets/examples/work.png +0 -0
- package/resources/dada-object-avatar/assets/preview.png +0 -0
- package/resources/dada-object-avatar/references/evaluation-rubric.md +22 -0
- package/resources/dada-object-avatar/references/examples.json +8 -0
- package/resources/dada-object-avatar/references/prompt-template.md +27 -0
- package/resources/dada-object-avatar/references/style-system.md +54 -0
- package/resources/dada-object-avatar/references/subject-design.md +23 -0
- package/resources/dada-object-avatar/references/training-guide.md +14 -0
- package/resources/dada-object-avatar/references/training-log.jsonl +0 -0
- package/resources/dada-object-avatar/references/training-record.schema.json +20 -0
- package/resources/dada-object-avatar/scripts/choose_gaze.py +36 -0
- package/resources/dada-object-avatar/scripts/record_example.py +22 -0
- package/resources/dada-object-avatar/scripts/validate_png.py +52 -0
- package/resources/dada-object-avatar.manifest.json +85 -0
package/dist/web/app.css
CHANGED
|
@@ -513,3 +513,41 @@
|
|
|
513
513
|
*, *::before, *::after { animation-duration: 120ms !important; animation-iteration-count: 1 !important; transition-duration: 120ms !important; }
|
|
514
514
|
}
|
|
515
515
|
}
|
|
516
|
+
|
|
517
|
+
.message-avatar img { width: 100%; height: 100%; object-fit: contain; border-radius: inherit; }
|
|
518
|
+
#buddy-profile-avatar { width: 32px; height: 32px; object-fit: contain; border-radius: 50%; }
|
|
519
|
+
#buddy-profile-avatar[hidden] { display: none; }
|
|
520
|
+
|
|
521
|
+
/* The hosted console keeps debugger and simulator side by side, including narrow panels. */
|
|
522
|
+
body[data-hosted-preview] { min-width:700px; height:100dvh; min-height:0; display:flex; flex-direction:column; overflow-x:auto; overflow-y:hidden; }
|
|
523
|
+
body[data-hosted-preview] .brand { display:none; }
|
|
524
|
+
body[data-hosted-preview] .app-shell { flex:1; min-height:0; display:flex; flex-direction:column; }
|
|
525
|
+
body[data-hosted-preview] .sidebar { position:static; flex:none; width:auto; padding:8px 16px; border-inline-end:0; border-bottom:1px solid var(--color-rule); }
|
|
526
|
+
body[data-hosted-preview] .sidebar-section { margin:0; }
|
|
527
|
+
body[data-hosted-preview] .sidebar-label { display:none; }
|
|
528
|
+
body[data-hosted-preview] .debug-tabs { display:flex; flex-wrap:wrap; gap:8px; }
|
|
529
|
+
body[data-hosted-preview] .debug-tabs button { width:auto; }
|
|
530
|
+
body[data-hosted-preview] .application { margin-inline-start:0; flex:1; min-height:0; display:flex; flex-direction:column; }
|
|
531
|
+
body[data-hosted-preview] .topbar { display:contents; }
|
|
532
|
+
body[data-hosted-preview] .project-heading { display:none; }
|
|
533
|
+
body[data-hosted-preview] .debugger > .sidebar { position:sticky; top:0; z-index:var(--z-sticky); }
|
|
534
|
+
body[data-hosted-preview] .workspace { flex:1; min-height:0; grid-template-columns:minmax(340px,1fr) clamp(340px,40%,520px); overflow:hidden; }
|
|
535
|
+
body[data-hosted-preview] .debugger { overflow-y:auto; overscroll-behavior:contain; }
|
|
536
|
+
body[data-hosted-preview] .simulator-area { position:relative; inset:auto; height:100%; min-height:0; max-height:none; border-block-start:0; border-inline-start:1px solid var(--color-rule); }
|
|
537
|
+
body[data-hosted-preview] .run-strip { padding:20px 24px 0; }
|
|
538
|
+
body[data-hosted-preview] .run-metrics { margin-top:20px; }
|
|
539
|
+
body[data-hosted-preview] .debug-panel { padding:24px; }
|
|
540
|
+
body[data-hosted-preview] #dev-control[data-action="stop"] { background:var(--color-paper-raised); color:var(--color-ink); border:1px solid var(--color-rule); }
|
|
541
|
+
.local-connection { flex:none; padding:10px 20px; border-bottom:1px solid var(--color-rule); background:var(--color-paper-raised); font-size:13px; }
|
|
542
|
+
.local-connection-heading { display:grid; grid-template-columns:minmax(0,1fr) auto; align-items:center; gap:8px 12px; }
|
|
543
|
+
.local-connection-heading strong { grid-column:1 / -1; min-width:0; overflow-wrap:anywhere; font-size:12px; font-weight:500; }
|
|
544
|
+
.local-connection pre { padding:14px; border-radius:8px; background:var(--color-paper); white-space:pre-wrap; overflow-wrap:anywhere; user-select:all; }
|
|
545
|
+
.local-connection p { line-height:1.7; }
|
|
546
|
+
#connect-instructions { max-height:250px; overflow-y:auto; }
|
|
547
|
+
@media (max-width:900px) {
|
|
548
|
+
body[data-hosted-preview] .workspace { grid-template-columns:minmax(340px,1fr) 320px; }
|
|
549
|
+
body[data-hosted-preview] .run-heading { flex-direction:column; gap:12px; }
|
|
550
|
+
body[data-hosted-preview] .run-identity, body[data-hosted-preview] .run-controls { flex:auto; width:100%; }
|
|
551
|
+
body[data-hosted-preview] .run-strip, body[data-hosted-preview] .debug-panel { padding-inline:16px; }
|
|
552
|
+
body[data-hosted-preview] .run-metrics { gap:8px; }
|
|
553
|
+
}
|
package/dist/web/app.js
CHANGED
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
(() => {
|
|
2
2
|
"use strict";
|
|
3
3
|
|
|
4
|
+
const hosted = document.body?.hasAttribute?.("data-hosted-preview") === true;
|
|
5
|
+
const hostedBuddyId = hosted ? new URL(location.href).searchParams.get("buddyId") : "";
|
|
6
|
+
const connectionUrl = hosted ? new URL(`../api/buddies/${encodeURIComponent(hostedBuddyId)}/preview`, location.href) : undefined;
|
|
7
|
+
let connectionInstance = "", connectionReady = !hosted, connectionBusy = false, devPollBusy = false;
|
|
8
|
+
function previewApiUrl(path) {
|
|
9
|
+
if (!hosted) return path;
|
|
10
|
+
const url = new URL(connectionUrl.href + path);
|
|
11
|
+
url.searchParams.set("instance", connectionInstance);
|
|
12
|
+
return url.href;
|
|
13
|
+
}
|
|
14
|
+
window.previewApiUrl = previewApiUrl;
|
|
4
15
|
const $ = (selector) => document.querySelector(selector);
|
|
5
16
|
const elements = {
|
|
6
17
|
projectName: $("#project-name"),
|
|
@@ -88,7 +99,8 @@
|
|
|
88
99
|
}
|
|
89
100
|
|
|
90
101
|
async function api(path, options = {}) {
|
|
91
|
-
const
|
|
102
|
+
const instance = connectionInstance;
|
|
103
|
+
const response = await fetch(previewApiUrl(path), {
|
|
92
104
|
...options,
|
|
93
105
|
headers: {
|
|
94
106
|
Accept: "application/json",
|
|
@@ -97,8 +109,9 @@
|
|
|
97
109
|
},
|
|
98
110
|
});
|
|
99
111
|
const value = await response.json().catch(() => ({}));
|
|
112
|
+
if (hosted && (!connectionReady || instance !== connectionInstance)) throw new Error("本地连接已改变,请等待重新连接");
|
|
100
113
|
if (!response.ok) {
|
|
101
|
-
const error = new Error(value?.error?.message || `请求失败(HTTP ${response.status})`);
|
|
114
|
+
const error = new Error((typeof value?.error === "string" ? value.error : value?.error?.message) || `请求失败(HTTP ${response.status})`);
|
|
102
115
|
error.code = value?.error?.code || "PREVIEW_REQUEST_FAILED";
|
|
103
116
|
error.status = response.status;
|
|
104
117
|
throw error;
|
|
@@ -159,15 +172,23 @@
|
|
|
159
172
|
class: `message-avatar ${role === "user" ? "person-avatar" : "buddy-message-avatar"}`,
|
|
160
173
|
"aria-hidden": "true",
|
|
161
174
|
});
|
|
162
|
-
if (role !== "user")
|
|
175
|
+
if (role !== "user") {
|
|
176
|
+
const picture = (state.dev.ready?.buddy || state.buddy)?.avatarPreview;
|
|
177
|
+
if (picture) {
|
|
178
|
+
const image = node("img", {src: picture, alt: ""});
|
|
179
|
+
image.addEventListener("error", () => { avatar.textContent = "搭"; }, {once: true});
|
|
180
|
+
avatar.append(image);
|
|
181
|
+
} else avatar.textContent = "搭";
|
|
182
|
+
}
|
|
163
183
|
return avatar;
|
|
164
184
|
}
|
|
165
185
|
|
|
166
186
|
function phaseCopy(phase) {
|
|
167
187
|
return {
|
|
168
|
-
idle: "未启动",
|
|
188
|
+
idle: hosted ? "已连接但 DEV 未启动" : "未启动",
|
|
189
|
+
disconnected: "未连接本地工程",
|
|
169
190
|
starting: "正在启动",
|
|
170
|
-
ready: "DEV
|
|
191
|
+
ready: "DEV 运行中",
|
|
171
192
|
stopping: "正在停止",
|
|
172
193
|
failed: "启动失败",
|
|
173
194
|
}[phase] || phase;
|
|
@@ -207,6 +228,12 @@
|
|
|
207
228
|
elements.projectName.textContent = name;
|
|
208
229
|
elements.buddyName.textContent = name;
|
|
209
230
|
elements.buddyName.title = buddy?.tagline || "";
|
|
231
|
+
const picture = document.querySelector("#buddy-profile-avatar");
|
|
232
|
+
if (picture) {
|
|
233
|
+
picture.hidden = !buddy?.avatarPreview;
|
|
234
|
+
if (buddy?.avatarPreview) picture.src = buddy.avatarPreview;
|
|
235
|
+
else picture.removeAttribute("src");
|
|
236
|
+
}
|
|
210
237
|
const note = document.querySelector(".preview-note");
|
|
211
238
|
if (note) note.textContent = buddy ? (buddy.isDev ? "开发预览版 · 仅你可见" : "搭搭 App") : "搭搭 App · DEV";
|
|
212
239
|
elements.environmentRuntime.textContent = "Node.js · Buddy Runtime";
|
|
@@ -217,11 +244,11 @@
|
|
|
217
244
|
let pairingRequest = 0;
|
|
218
245
|
|
|
219
246
|
function renderDev() {
|
|
220
|
-
const phase = state.dev.phase;
|
|
247
|
+
const phase = connectionReady ? state.dev.phase : "disconnected";
|
|
221
248
|
if (!elements.pairingPanel.hidden && pairingRevision !== state.dev.revision) void togglePairing(true);
|
|
222
249
|
elements.connectionState.dataset.phase = phase;
|
|
223
|
-
elements.connectionLabel.textContent = phaseCopy(phase);
|
|
224
|
-
elements.devControl.disabled = phase === "starting" || phase === "stopping";
|
|
250
|
+
elements.connectionLabel.textContent = phase === "disconnected" ? "未连接本地工程" : phaseCopy(phase);
|
|
251
|
+
elements.devControl.disabled = !connectionReady || phase === "starting" || phase === "stopping";
|
|
225
252
|
elements.devControl.dataset.action = phase === "ready" ? "stop" : "start";
|
|
226
253
|
elements.devControl.textContent = phase === "ready" ? "停止 DEV" : phase === "starting" ? "正在启动…" : phase === "stopping" ? "正在停止…" : "启动 DEV";
|
|
227
254
|
elements.environmentAgent.textContent = phase === "ready"
|
|
@@ -525,17 +552,17 @@
|
|
|
525
552
|
}
|
|
526
553
|
|
|
527
554
|
function syncPolling() {
|
|
528
|
-
if (state.dev.phase === "ready" && state.polling === undefined) {
|
|
555
|
+
if (connectionReady && state.dev.phase === "ready" && state.polling === undefined) {
|
|
529
556
|
void refreshSimulator();
|
|
530
557
|
state.polling = window.setInterval(() => void refreshSimulator(), 1200);
|
|
531
|
-
} else if (state.dev.phase !== "ready" && state.polling !== undefined) {
|
|
558
|
+
} else if ((!connectionReady || state.dev.phase !== "ready") && state.polling !== undefined) {
|
|
532
559
|
window.clearInterval(state.polling);
|
|
533
560
|
state.polling = undefined;
|
|
534
561
|
}
|
|
535
562
|
}
|
|
536
563
|
|
|
537
564
|
async function controlDev() {
|
|
538
|
-
if (state.dev.phase === "starting" || state.dev.phase === "stopping") return;
|
|
565
|
+
if (!connectionReady || state.dev.phase === "starting" || state.dev.phase === "stopping") return;
|
|
539
566
|
elements.devControl.disabled = true;
|
|
540
567
|
try {
|
|
541
568
|
const path = state.dev.phase === "ready" ? "/api/dev/stop" : "/api/dev/start";
|
|
@@ -725,6 +752,7 @@
|
|
|
725
752
|
}
|
|
726
753
|
|
|
727
754
|
async function boot() {
|
|
755
|
+
if (hosted && !connectionReady) { void refreshConnection(); return; }
|
|
728
756
|
try {
|
|
729
757
|
const [project, dev, debug] = await Promise.all([
|
|
730
758
|
api("/api/project"),
|
|
@@ -735,7 +763,7 @@
|
|
|
735
763
|
state.dev = dev.state;
|
|
736
764
|
state.debug = debug.debug;
|
|
737
765
|
renderAll();
|
|
738
|
-
connectEvents();
|
|
766
|
+
if (!hosted) connectEvents();
|
|
739
767
|
// Load the cloud profile independently of Agent startup. If it fails,
|
|
740
768
|
// the local project and DEV controls remain usable and show the error.
|
|
741
769
|
const { buddy } = await api("/api/buddy");
|
|
@@ -746,6 +774,69 @@
|
|
|
746
774
|
}
|
|
747
775
|
}
|
|
748
776
|
|
|
777
|
+
function disconnected(message = "未连接本地工程") {
|
|
778
|
+
connectionReady = false;
|
|
779
|
+
state.dev = { phase: "idle", revision: -1 };
|
|
780
|
+
state.simulatorRequest++;
|
|
781
|
+
widgets?.reset();
|
|
782
|
+
void togglePairing(false);
|
|
783
|
+
renderDev(); syncPolling();
|
|
784
|
+
$("#local-status").textContent = message;
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
async function refreshConnection() {
|
|
788
|
+
if (connectionBusy) return;
|
|
789
|
+
connectionBusy = true;
|
|
790
|
+
try {
|
|
791
|
+
const response = await fetch(connectionUrl, { signal: AbortSignal.timeout(5000) });
|
|
792
|
+
const value = await response.json();
|
|
793
|
+
if (!response.ok) throw new Error(value.error || "连接状态读取失败");
|
|
794
|
+
$("#connect-command").textContent = value.command;
|
|
795
|
+
$("#copy-connect-command").disabled = false;
|
|
796
|
+
if (!value.connected) { disconnected(); return; }
|
|
797
|
+
const changed = connectionInstance !== value.instanceId;
|
|
798
|
+
const newlyConnected = !connectionReady || changed;
|
|
799
|
+
connectionInstance = value.instanceId;
|
|
800
|
+
connectionReady = true;
|
|
801
|
+
$("#local-status").textContent = ["已连接本地工程", value.machineName, value.projectRoot].filter(Boolean).join(" · ");
|
|
802
|
+
if (newlyConnected) {
|
|
803
|
+
state.dev = { phase: "idle", revision: -1 };
|
|
804
|
+
if (changed) {
|
|
805
|
+
state.simulator = { messages: [] }; state.debug = { runs: [], events: [], messages: [] };
|
|
806
|
+
state.selectedRunId = ""; pendingSend = undefined; state.buddy = undefined;
|
|
807
|
+
}
|
|
808
|
+
elements.errorBanner.hidden = true;
|
|
809
|
+
await boot();
|
|
810
|
+
}
|
|
811
|
+
} catch (cause) { disconnected(cause.message || "连接已中断,正在重试…"); }
|
|
812
|
+
finally { connectionBusy = false; }
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
async function pollHostedDev() {
|
|
816
|
+
if (!connectionReady || devPollBusy) return;
|
|
817
|
+
devPollBusy = true;
|
|
818
|
+
try {
|
|
819
|
+
const [dev, debug] = await Promise.all([api("/api/dev"), api("/api/debug")]);
|
|
820
|
+
applyDevState(dev.state); state.debug = debug.debug; renderAll();
|
|
821
|
+
} catch (cause) { showError(cause); }
|
|
822
|
+
finally { devPollBusy = false; }
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
if (hosted) {
|
|
826
|
+
renderDev();
|
|
827
|
+
$("#get-connect-command").addEventListener("click", () => {
|
|
828
|
+
$("#connect-instructions").hidden = !$("#connect-instructions").hidden;
|
|
829
|
+
void refreshConnection();
|
|
830
|
+
});
|
|
831
|
+
$("#copy-connect-command").addEventListener("click", async () => {
|
|
832
|
+
try { await navigator.clipboard.writeText($("#connect-command").textContent); toast("连接指令已复制,请在当前搭子的工程目录执行"); }
|
|
833
|
+
catch { showError(new Error("请手动复制下方的完整连接指令")); }
|
|
834
|
+
});
|
|
835
|
+
const statusTimer = window.setInterval(() => void refreshConnection(), 2000);
|
|
836
|
+
const devTimer = window.setInterval(() => void pollHostedDev(), 1200);
|
|
837
|
+
window.addEventListener("pagehide", () => { window.clearInterval(statusTimer); window.clearInterval(devTimer); window.clearInterval(state.polling); });
|
|
838
|
+
}
|
|
839
|
+
|
|
749
840
|
elements.resetConversation.addEventListener("click", () => void resetConversation());
|
|
750
841
|
elements.latestMessages.addEventListener("click", followLatestMessages);
|
|
751
842
|
elements.chatMessages.addEventListener("scroll", () => {
|
package/dist/web/index.html
CHANGED
|
@@ -197,7 +197,7 @@
|
|
|
197
197
|
<button class="back-chevron" type="button" aria-label="返回(预览中不可用)" disabled>
|
|
198
198
|
<svg aria-hidden="true" viewBox="0 0 24 24"><path d="m15 18-6-6 6-6"/></svg>
|
|
199
199
|
</button>
|
|
200
|
-
<div class="buddy-identity"><h3 id="buddy-name">我的搭子</h3><p id="buddy-status" class="sr-only">DEV 未连接</p></div>
|
|
200
|
+
<img id="buddy-profile-avatar" alt="搭子头像" hidden><div class="buddy-identity"><h3 id="buddy-name">我的搭子</h3><p id="buddy-status" class="sr-only">DEV 未连接</p></div>
|
|
201
201
|
<div class="chat-navigation-actions" aria-hidden="true">
|
|
202
202
|
<button class="chat-more" type="button" aria-label="更多(预览中不可用)" disabled tabindex="-1">
|
|
203
203
|
<svg viewBox="0 0 24 24"><circle cx="5" cy="12" r="1.3"/><circle cx="12" cy="12" r="1.3"/><circle cx="19" cy="12" r="1.3"/></svg>
|
package/dist/web/widgets.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/* Client reference: my-life-buddies-ios dev 547818d, DadaConversationWidgets / DadaHostedWidgetPage. */
|
|
2
2
|
(() => {
|
|
3
|
+
const previewUrl = path => window.previewApiUrl?.(path) ?? path;
|
|
3
4
|
const el = (tag, className, text) => { const n = document.createElement(tag); n.className = className; if (text !== undefined) n.textContent = text; return n; };
|
|
4
5
|
window.createSimulatorWidgets = ({ api, avatar }) => {
|
|
5
6
|
const shortcut = document.querySelector('#widget-shortcut');
|
|
@@ -30,7 +31,7 @@
|
|
|
30
31
|
for (const item of items) { const b = button(item.title + ' ›', () => { choices.remove(); pill.setAttribute('aria-expanded', 'false'); void open(item); }); b.setAttribute('role', 'menuitem'); choices.append(b); }
|
|
31
32
|
shortcut.append(choices); pill.setAttribute('aria-expanded', 'true'); choices.firstChild?.focus();
|
|
32
33
|
}, 'widget-pill');
|
|
33
|
-
const image = avatar(); image.
|
|
34
|
+
const image = avatar(); image.classList.add('widget-pill-avatar');
|
|
34
35
|
pill.append(image, el('span', 'widget-pill-title', items.length === 1 ? items[0].title : '全部小挂件'));
|
|
35
36
|
if (items.length > 1) { pill.append(el('span', 'widget-chevron', '⌄')); pill.setAttribute('aria-haspopup', 'menu'); pill.setAttribute('aria-expanded', 'false'); }
|
|
36
37
|
shortcut.append(pill);
|
|
@@ -77,7 +78,7 @@
|
|
|
77
78
|
for (const child of phone.children) if (child !== overlay) child.inert = true;
|
|
78
79
|
dismiss.focus();
|
|
79
80
|
try {
|
|
80
|
-
const url = `/api/simulator/widgets/${encodeURIComponent(item.id)}
|
|
81
|
+
const url = previewUrl(`/api/simulator/widgets/${encodeURIComponent(item.id)}`);
|
|
81
82
|
// Probe errors in the parent; actual HTML executes only inside the opaque sandbox document.
|
|
82
83
|
const response = await fetch(url, { credentials: 'same-origin' });
|
|
83
84
|
if (!response.ok) { const error = await response.json(); throw new Error(error.error?.message || error.error || '小挂件暂时无法打开'); }
|
|
@@ -117,7 +118,7 @@
|
|
|
117
118
|
card(item) {
|
|
118
119
|
const card = el('div', 'widget-entry'); card.append(el('h4', '', item.title));
|
|
119
120
|
const details = el('div', 'widget-entry-details');
|
|
120
|
-
if (item.type && item.imageMediaId) { const image = el('img', 'widget-entry-photo'); image.alt = `${item.title}的照片`; image.src = `/api/simulator/widgets/${encodeURIComponent(item.id)}/image?mediaId=${encodeURIComponent(item.imageMediaId)}
|
|
121
|
+
if (item.type && item.imageMediaId) { const image = el('img', 'widget-entry-photo'); image.alt = `${item.title}的照片`; image.src = previewUrl(`/api/simulator/widgets/${encodeURIComponent(item.id)}/image?mediaId=${encodeURIComponent(item.imageMediaId)}`); image.addEventListener('error', () => { image.hidden = true; }); details.append(image); }
|
|
121
122
|
details.append(el('p', '', item.type ? item.summary?.trim() || '打开查看最新内容' : '此小挂件已删除或不可用')); card.append(details);
|
|
122
123
|
if (item.type) card.append(button('去查看', () => open(item), 'widget-entry-open'));
|
|
123
124
|
return card;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@my-life-buddies/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "搭搭 Developer Platform 命令行工具",
|
|
6
6
|
"publishConfig": {
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"ws": "8.21.3"
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|
|
42
|
-
"@buddy/cli-core": "0.
|
|
43
|
-
"@buddy/preview": "0.
|
|
42
|
+
"@buddy/cli-core": "0.13.0",
|
|
43
|
+
"@buddy/preview": "0.13.0"
|
|
44
44
|
}
|
|
45
45
|
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: dada-object-avatar
|
|
3
|
+
description: "Generate or revise Dada-style retro object avatars from a companion name and Emoji. Use when the user wants the current unified object-character system: recognizable semantic object, varied coordinated eyes, two lateral three-digit hands, restrained retro material, circular-crop-safe composition, and a genuine transparent PNG."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# 搭搭复古物件头像
|
|
7
|
+
|
|
8
|
+
把搭子名字与 Emoji 转译成一个可辨认的物件角色。保留“人”的眼神与姿态,同时避免真人头像和写实动物身体。
|
|
9
|
+
|
|
10
|
+
## Workflow
|
|
11
|
+
|
|
12
|
+
1. Read [references/style-system.md](references/style-system.md). Treat its invariants as the current accepted style.
|
|
13
|
+
2. For a new subject, read [references/subject-design.md](references/subject-design.md) and choose one primary object plus at most two identity details.
|
|
14
|
+
3. Before generation, allocate the character's shared gaze direction with `python3 scripts/choose_gaze.py --recent <directions>`. Pass recent gallery directions so neighboring characters do not repeat.
|
|
15
|
+
4. Read [references/prompt-template.md](references/prompt-template.md), fill the subject, palette, gaze and detail hierarchy fields, then invoke the built-in image generation tool. Use the closest retained asset from `assets/examples/` as the style reference. For revisions, also include the exact previous subject image as the first reference and state which properties must remain locked.
|
|
16
|
+
5. Inspect the result on both light and dark backgrounds and inside a centered circular crop. Confirm recognizable identity, coordinated gaze, hand anatomy, detail spacing and continuous body volume.
|
|
17
|
+
6. Deliver a genuine RGBA PNG. Run `python3 scripts/validate_png.py <image.png>` after any matte removal or alpha reconstruction. A checkerboard drawn into RGB pixels is not transparency.
|
|
18
|
+
7. When the user gives feedback, record the concrete trigger, failure and accepted correction with `python3 scripts/record_example.py ...`. Use [references/training-record.schema.json](references/training-record.schema.json) as the stable training format.
|
|
19
|
+
|
|
20
|
+
For later style training and promotion of new canonical examples, follow [references/training-guide.md](references/training-guide.md).
|
|
21
|
+
|
|
22
|
+
## Generation invariants
|
|
23
|
+
|
|
24
|
+
- One square image contains one character. The central body continues naturally through the bottom edge; the top retains transparent breathing room.
|
|
25
|
+
- The semantic object remains legible at avatar size. Do not replace it with an unrelated blob or generic person.
|
|
26
|
+
- Volume comes from continuous geometry, overlap, structural turns and plausible attachment. Lighting only explains the geometry. Do not add seams to simulate thickness.
|
|
27
|
+
- Use ivory embedded eye whites and black pupils. Both whites and pupils vary in size, aspect ratio, tilt and local position. Assign one shared gaze vector per character; neighboring characters use different directions.
|
|
28
|
+
- Add exactly two small lateral hands below eye level. Each hand has exactly three digits total: one broad thumb and two smaller fingers.
|
|
29
|
+
- Nearby handles, lugs, clasps, symbols and ornaments remain secondary. If near a hand, make them roughly 60–75% of hand width and separate them vertically by at least half a hand height.
|
|
30
|
+
- No eyebrows, mouth or nose by default. No legs or feet. No realistic anatomy, dirty texture, dense skin grain, text or watermark.
|
|
31
|
+
- Use genuine transparency around the silhouette and through real openings. Eye whites and subject remain opaque.
|
|
32
|
+
|
|
33
|
+
## Iteration boundary
|
|
34
|
+
|
|
35
|
+
Lock accepted properties. If feedback concerns only gaze, edit only the eyes. If it concerns volume, keep silhouette, identity details and proportions unless the user explicitly requests a redesign. Generate one corrected candidate per subject before expanding the family.
|
|
36
|
+
|
|
37
|
+
For scoring or dataset curation, read [references/evaluation-rubric.md](references/evaluation-rubric.md).
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Evaluation rubric
|
|
2
|
+
|
|
3
|
+
Score each category 0–2. Accept only when all hard gates pass and the total is at least 13/16.
|
|
4
|
+
|
|
5
|
+
| Category | 0 | 1 | 2 |
|
|
6
|
+
|---|---|---|---|
|
|
7
|
+
| Semantic identity | unclear/generic | recognizable with explanation | instantly recognizable at avatar size |
|
|
8
|
+
| Structural volume | flat, seam-based | partial believable depth | continuous plausible solid volume |
|
|
9
|
+
| Eye morphology | mirrored/uniform | one layer varies | whites and pupils both vary |
|
|
10
|
+
| Gaze | crossed/repeated family direction | coordinated but generic | coordinated and distinct from neighbors |
|
|
11
|
+
| Hands | wrong count/anatomy | minor ambiguity | exactly two lateral three-digit hands |
|
|
12
|
+
| Detail hierarchy | crowded/equal scale | acceptable spacing | clear 60–75% secondary scale and separation |
|
|
13
|
+
| Circular framing | clipped/blank bottom | usable | safe details and natural 6-o'clock fill |
|
|
14
|
+
| Material/color | dirty or wrong association | acceptable | restrained texture and intentional semantics |
|
|
15
|
+
|
|
16
|
+
Hard gates:
|
|
17
|
+
|
|
18
|
+
- genuine RGBA transparency;
|
|
19
|
+
- four transparent corners;
|
|
20
|
+
- subject reaches the lower-center band;
|
|
21
|
+
- no checkerboard embedded in output;
|
|
22
|
+
- no extra fingers, legs, mouth, nose, eyebrows, text or watermark.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
[
|
|
2
|
+
{"file":"assets/examples/cat.png","subject":"短毛猫铲屎官","gaze":"right","role":"seamless animal-object volume, thin ears, prop interaction"},
|
|
3
|
+
{"file":"assets/examples/cooking.png","subject":"小涂阿姨","gaze":"lower-left","role":"small high lugs separated from lower hands"},
|
|
4
|
+
{"file":"assets/examples/relationship.png","subject":"情感专家","gaze":"left","role":"bright coral semantic color and complete heart silhouette"},
|
|
5
|
+
{"file":"assets/examples/doctor.png","subject":"林医生","gaze":"lower-right","role":"semantic gaze toward a small medical symbol"},
|
|
6
|
+
{"file":"assets/examples/work.png","subject":"工作那些事","gaze":"left","role":"compact clasps and horizontal gaze"},
|
|
7
|
+
{"file":"assets/examples/divination.png","subject":"塔罗星人","gaze":"upper-left","role":"secondary ornament and distinct gaze"}
|
|
8
|
+
]
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Prompt template
|
|
2
|
+
|
|
3
|
+
Use the closest accepted example as the style reference. For an edit, make the current subject the first reference.
|
|
4
|
+
|
|
5
|
+
```text
|
|
6
|
+
Create a square Dada-style retro object avatar for {NAME} {EMOJI}.
|
|
7
|
+
|
|
8
|
+
SUBJECT
|
|
9
|
+
Use {PRIMARY_OBJECT} as the recognizable primary silhouette. Preserve/add only {DETAIL_1} and {DETAIL_2}. Use {PALETTE} with restrained {MATERIAL}. Avoid {UNWANTED_ASSOCIATION}.
|
|
10
|
+
|
|
11
|
+
VOLUME
|
|
12
|
+
Build one continuous believable volume with rounded structural turns, overlap and physically plausible attachment. Do not add seams, panel lines or exaggerated highlights to fake thickness.
|
|
13
|
+
|
|
14
|
+
EYES
|
|
15
|
+
Use embedded warm-ivory eye whites and matte-black pupils. Do not mirror the pair. Make the whites differ by about 8–18% in size/aspect and 4–10 degrees in tilt. Make the pupils differ by about 10–18% in size/aspect and place them differently within their whites. Assign the whole character a coordinated {GAZE_DIRECTION} gaze; both pupils follow that vector without crossing or diverging. This direction must differ from neighboring gallery characters.
|
|
16
|
+
|
|
17
|
+
HANDS AND DETAILS
|
|
18
|
+
Add exactly two small lateral hands below eye level. Each has exactly three digits total: one broad thumb plus two smaller rounded fingers. Hand width is about 12–15% of body width. Any nearby handle, lug, clasp, symbol or ornament is 60–75% of hand width and at least half a hand-height away. Keep details secondary and unobstructed.
|
|
19
|
+
|
|
20
|
+
FRAMING
|
|
21
|
+
One character, 1:1. About 88% visual scale. Keep every hand, handle and prop inside a centered circular crop. The central body continues naturally through the bottom edge; no transparent strip or floating base.
|
|
22
|
+
|
|
23
|
+
OUTPUT
|
|
24
|
+
Genuine RGBA transparent PNG. Alpha 0 outside the silhouette and through real openings. Eye whites and subject remain opaque. No checkerboard, backdrop, floor, external shadow, mouth, nose, eyebrows, legs, feet, text or watermark.
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
For an eye-only revision, explicitly say: `Preserve every non-eye pixel and change only eye whites, pupils and gaze.` Then specify one direction rather than asking for generic randomness.
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# Style system
|
|
2
|
+
|
|
3
|
+
## 1. Identity
|
|
4
|
+
|
|
5
|
+
Start from the companion role, not the literal title. Convert it to a familiar object silhouette:
|
|
6
|
+
|
|
7
|
+
- cooking → rounded enamel pot
|
|
8
|
+
- health/doctor → first-aid case
|
|
9
|
+
- work → briefcase
|
|
10
|
+
- divination → tarot card
|
|
11
|
+
- pet care → simplified cat-shaped object with one litter scoop
|
|
12
|
+
- relationship → complete graphic heart
|
|
13
|
+
- travel → suitcase
|
|
14
|
+
- fitness → kettlebell
|
|
15
|
+
|
|
16
|
+
Use one dominant silhouette and no more than two secondary identity details. A user-provided mapping overrides this list.
|
|
17
|
+
|
|
18
|
+
## 2. Shape and volume
|
|
19
|
+
|
|
20
|
+
Use orderly, softly rounded geometry. Show thickness through front/side relationships, overlap, rim depth and believable attachment. Do not use a decorative center seam, panel line or strong gradient as a substitute for form. Thin structures such as cat ears must stay physically plausible and naturally join the body.
|
|
21
|
+
|
|
22
|
+
## 3. Materials and color
|
|
23
|
+
|
|
24
|
+
Use restrained retro resin, enamel, soft metal or leather-like material with sparse fine grain. Prefer warm Memphis-adjacent colors: cream, teal, tomato red, coral, mustard, cocoa, plum and charcoal. Check semantic associations: a heart should use bright coral or tomato red rather than dark liver-like burgundy.
|
|
25
|
+
|
|
26
|
+
## 4. Eye system
|
|
27
|
+
|
|
28
|
+
Eye whites are warm ivory and slightly embedded; pupils are matte black.
|
|
29
|
+
|
|
30
|
+
For each pair:
|
|
31
|
+
|
|
32
|
+
- white size or aspect difference: approximately 8–18%
|
|
33
|
+
- white tilt difference: approximately 4–10 degrees
|
|
34
|
+
- pupil size or aspect difference: approximately 10–18%
|
|
35
|
+
- pupil local placement: different within each white, but both follow one shared gaze vector
|
|
36
|
+
|
|
37
|
+
Choose one vector from `front`, `left`, `right`, `upper-left`, `upper-right`, `lower-left`, `lower-right`. Do not repeat the same or near-identical vector on neighboring gallery cards. Do not default the family to upper-right. Both pupils must remain coordinated; avoid crossed or divergent eyes.
|
|
38
|
+
|
|
39
|
+
## 5. Hands and detail hierarchy
|
|
40
|
+
|
|
41
|
+
Place one hand on each outer side below eye level. Use very short arms. Each hand has exactly three digits total: one broad thumb, about 1.4 times a small finger, and two rounded smaller fingers. Hand width is about 12–15% of body width. Hands turn slightly sideways; no front-facing open palm.
|
|
42
|
+
|
|
43
|
+
When a handle, lug, clasp or ornament approaches a hand:
|
|
44
|
+
|
|
45
|
+
- reduce the detail to roughly 60–75% of hand width;
|
|
46
|
+
- separate it by at least half a hand height;
|
|
47
|
+
- move it to a different vertical zone if crowding remains;
|
|
48
|
+
- preserve recognizability before decoration.
|
|
49
|
+
|
|
50
|
+
## 6. Framing and alpha
|
|
51
|
+
|
|
52
|
+
Use a 1:1 master. Target roughly 88% of the prior oversized-object scale. Keep hands, handles and identity props inside a centered circle. The main body passes naturally through the square bottom edge and fills the circle at 6 o'clock. Leave no transparent horizontal strip below the body and no floating base.
|
|
53
|
+
|
|
54
|
+
Export true RGBA. All four corners should be alpha 0. Real handle openings and enclosed gaps should be transparent. Never render a checkerboard as background pixels.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Subject design
|
|
2
|
+
|
|
3
|
+
Use this decision order for a new companion:
|
|
4
|
+
|
|
5
|
+
1. **Role verb:** what does this companion help the user do?
|
|
6
|
+
2. **Primary object:** which single object communicates that action fastest at 64 px?
|
|
7
|
+
3. **Identity details:** choose one or two details that disambiguate the object.
|
|
8
|
+
4. **Conflict check:** compare every detail with the hands. Reduce or move any feature with similar size and proximity.
|
|
9
|
+
5. **Palette check:** confirm the color evokes the intended emotion rather than food, anatomy or danger by accident.
|
|
10
|
+
6. **Gaze:** allocate a direction that differs from the neighboring examples and, when useful, points toward a semantic detail.
|
|
11
|
+
|
|
12
|
+
Keep the primary silhouette stable through revisions. Character variation comes from object shape, palette, eye morphology, gaze and one meaningful pose, rather than extra facial features.
|
|
13
|
+
|
|
14
|
+
## Existing accepted assignments
|
|
15
|
+
|
|
16
|
+
| Subject | Primary object | Details | Gaze |
|
|
17
|
+
|---|---|---|---|
|
|
18
|
+
| 短毛猫铲屎官 | cream cat-shaped object | thin ears, coral scoop | toward scoop |
|
|
19
|
+
| 小涂阿姨 | tomato enamel pot | cream lid, small high lugs | attentive inward/downward |
|
|
20
|
+
| 情感专家 | bright coral heart | complete rounded lobes | gentle side glance |
|
|
21
|
+
| 林医生 | cream first-aid case | teal handle/band, coral cross | lower-right toward cross |
|
|
22
|
+
| 工作那些事 | cocoa briefcase | compact handle, small brass clasps | horizontal left |
|
|
23
|
+
| 塔罗星人 | plum tarot card | thin gold border, small crescent | upper-left |
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Iterative style training
|
|
2
|
+
|
|
3
|
+
Treat training as controlled prompt-and-reference refinement:
|
|
4
|
+
|
|
5
|
+
1. Save every candidate prompt and output path.
|
|
6
|
+
2. Record the user's verdict and exact visible failure with `scripts/record_example.py`; avoid vague labels such as “bad style”.
|
|
7
|
+
3. On revision, list the accepted properties that must remain locked and change only the rejected property.
|
|
8
|
+
4. Promote an image to `assets/examples/` only after it passes the rubric and the user accepts the direction.
|
|
9
|
+
5. Add the promoted image to `references/examples.json` with its subject, gaze and the single rule it demonstrates best.
|
|
10
|
+
6. Keep complementary examples. Do not retain several samples that teach the same gaze and silhouette while leaving other cases uncovered.
|
|
11
|
+
7. Before changing a global rule, confirm the same failure appears in more than one subject. Keep one-off feedback in the subject prompt.
|
|
12
|
+
8. Re-run `scripts/validate_png.py` on every retained PNG and forward-test one unseen companion after a global rule change.
|
|
13
|
+
|
|
14
|
+
A useful JSONL record contains the original prompt, output, verdict, concrete feedback, failure tags, accepted correction and locked properties. This supports later retrieval, preference modeling or manual dataset conversion without mixing rejected and accepted examples.
|
|
File without changes
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"title": "Dada Object Avatar Training Record",
|
|
4
|
+
"type": "object",
|
|
5
|
+
"required": ["timestamp", "subject", "prompt", "output", "verdict"],
|
|
6
|
+
"properties": {
|
|
7
|
+
"timestamp": {"type": "string", "format": "date-time"},
|
|
8
|
+
"subject": {"type": "string"},
|
|
9
|
+
"emoji": {"type": "string"},
|
|
10
|
+
"gaze": {"enum": ["front", "left", "right", "upper-left", "upper-right", "lower-left", "lower-right"]},
|
|
11
|
+
"prompt": {"type": "string"},
|
|
12
|
+
"output": {"type": "string"},
|
|
13
|
+
"verdict": {"enum": ["accepted", "rejected", "needs-revision"]},
|
|
14
|
+
"feedback": {"type": "string"},
|
|
15
|
+
"failure_tags": {"type": "array", "items": {"type": "string"}},
|
|
16
|
+
"accepted_correction": {"type": "string"},
|
|
17
|
+
"locked_properties": {"type": "array", "items": {"type": "string"}}
|
|
18
|
+
},
|
|
19
|
+
"additionalProperties": false
|
|
20
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Choose a coordinated gaze direction that differs from recent gallery items."""
|
|
3
|
+
import argparse, hashlib, json, math, random
|
|
4
|
+
|
|
5
|
+
VECTORS = {
|
|
6
|
+
"front": (0.0, 0.0), "left": (-1.0, 0.0), "right": (1.0, 0.0),
|
|
7
|
+
"upper-left": (-0.707, -0.707), "upper-right": (0.707, -0.707),
|
|
8
|
+
"lower-left": (-0.707, 0.707), "lower-right": (0.707, 0.707),
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
def near(a, b):
|
|
12
|
+
if a == b: return True
|
|
13
|
+
if "front" in (a, b): return False
|
|
14
|
+
ax, ay = VECTORS[a]; bx, by = VECTORS[b]
|
|
15
|
+
return ax * bx + ay * by > 0.70
|
|
16
|
+
|
|
17
|
+
def main():
|
|
18
|
+
ap = argparse.ArgumentParser()
|
|
19
|
+
ap.add_argument("--recent", default="", help="Comma-separated recent directions")
|
|
20
|
+
ap.add_argument("--seed", default="", help="Optional reproducible seed")
|
|
21
|
+
args = ap.parse_args()
|
|
22
|
+
recent = [x.strip() for x in args.recent.split(",") if x.strip()]
|
|
23
|
+
unknown = [x for x in recent if x not in VECTORS]
|
|
24
|
+
if unknown: ap.error(f"unknown directions: {', '.join(unknown)}")
|
|
25
|
+
last = recent[-1] if recent else None
|
|
26
|
+
candidates = [d for d in VECTORS if not any(near(d, previous) for previous in recent[-3:])]
|
|
27
|
+
if not candidates: candidates = [d for d in VECTORS if d != last]
|
|
28
|
+
rng = random.Random(int(hashlib.sha256(args.seed.encode()).hexdigest(), 16) if args.seed else None)
|
|
29
|
+
direction = rng.choice(candidates)
|
|
30
|
+
print(json.dumps({
|
|
31
|
+
"direction": direction,
|
|
32
|
+
"vector": VECTORS[direction],
|
|
33
|
+
"prompt": f"Assign one coordinated {direction.upper()} gaze to this character; both pupils follow this vector without crossing or diverging."
|
|
34
|
+
}, ensure_ascii=False))
|
|
35
|
+
|
|
36
|
+
if __name__ == "__main__": main()
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Append one structured feedback example to a JSONL training log."""
|
|
3
|
+
import argparse, datetime, json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
def split(value): return [x.strip() for x in value.split(',') if x.strip()]
|
|
7
|
+
|
|
8
|
+
def main():
|
|
9
|
+
ap=argparse.ArgumentParser()
|
|
10
|
+
ap.add_argument('--log',required=True); ap.add_argument('--subject',required=True); ap.add_argument('--emoji',default='')
|
|
11
|
+
ap.add_argument('--gaze',choices=['front','left','right','upper-left','upper-right','lower-left','lower-right'])
|
|
12
|
+
ap.add_argument('--prompt-file',required=True); ap.add_argument('--output',required=True)
|
|
13
|
+
ap.add_argument('--verdict',required=True,choices=['accepted','rejected','needs-revision'])
|
|
14
|
+
ap.add_argument('--feedback',default=''); ap.add_argument('--failure-tags',default=''); ap.add_argument('--accepted-correction',default=''); ap.add_argument('--locked-properties',default='')
|
|
15
|
+
a=ap.parse_args()
|
|
16
|
+
record={"timestamp":datetime.datetime.now(datetime.timezone.utc).isoformat(),"subject":a.subject,"emoji":a.emoji,"prompt":Path(a.prompt_file).read_text(),"output":a.output,"verdict":a.verdict,"feedback":a.feedback,"failure_tags":split(a.failure_tags),"accepted_correction":a.accepted_correction,"locked_properties":split(a.locked_properties)}
|
|
17
|
+
if a.gaze: record['gaze']=a.gaze
|
|
18
|
+
log=Path(a.log); log.parent.mkdir(parents=True,exist_ok=True)
|
|
19
|
+
with log.open('a',encoding='utf-8') as f: f.write(json.dumps(record,ensure_ascii=False)+'\n')
|
|
20
|
+
print(json.dumps(record,ensure_ascii=False,indent=2))
|
|
21
|
+
|
|
22
|
+
if __name__=='__main__': main()
|