@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.
Files changed (33) hide show
  1. package/README.md +26 -12
  2. package/dist/bin/buddy.js +68 -12
  3. package/dist/bin/buddy.js.map +3 -3
  4. package/dist/bin/core.js +1077 -454
  5. package/dist/bin/core.js.map +4 -4
  6. package/dist/bin/preview.js +164 -2
  7. package/dist/bin/preview.js.map +4 -4
  8. package/dist/web/app.css +38 -0
  9. package/dist/web/app.js +103 -12
  10. package/dist/web/index.html +1 -1
  11. package/dist/web/widgets.js +4 -3
  12. package/package.json +3 -3
  13. package/resources/dada-object-avatar/SKILL.md +37 -0
  14. package/resources/dada-object-avatar/agents/openai.yaml +4 -0
  15. package/resources/dada-object-avatar/assets/examples/cat.png +0 -0
  16. package/resources/dada-object-avatar/assets/examples/cooking.png +0 -0
  17. package/resources/dada-object-avatar/assets/examples/divination.png +0 -0
  18. package/resources/dada-object-avatar/assets/examples/doctor.png +0 -0
  19. package/resources/dada-object-avatar/assets/examples/relationship.png +0 -0
  20. package/resources/dada-object-avatar/assets/examples/work.png +0 -0
  21. package/resources/dada-object-avatar/assets/preview.png +0 -0
  22. package/resources/dada-object-avatar/references/evaluation-rubric.md +22 -0
  23. package/resources/dada-object-avatar/references/examples.json +8 -0
  24. package/resources/dada-object-avatar/references/prompt-template.md +27 -0
  25. package/resources/dada-object-avatar/references/style-system.md +54 -0
  26. package/resources/dada-object-avatar/references/subject-design.md +23 -0
  27. package/resources/dada-object-avatar/references/training-guide.md +14 -0
  28. package/resources/dada-object-avatar/references/training-log.jsonl +0 -0
  29. package/resources/dada-object-avatar/references/training-record.schema.json +20 -0
  30. package/resources/dada-object-avatar/scripts/choose_gaze.py +36 -0
  31. package/resources/dada-object-avatar/scripts/record_example.py +22 -0
  32. package/resources/dada-object-avatar/scripts/validate_png.py +52 -0
  33. 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 response = await fetch(path, {
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") avatar.textContent = "搭";
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", () => {
@@ -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>
@@ -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.textContent = ''; image.classList.add('widget-pill-avatar');
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)}`; image.addEventListener('error', () => { image.hidden = true; }); details.append(image); }
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.10.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.10.0",
43
- "@buddy/preview": "0.10.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).
@@ -0,0 +1,4 @@
1
+ interface:
2
+ display_name: "搭搭复古物件头像"
3
+ short_description: "生成带随机协调眼神、三指侧手和透明背景的复古物件头像"
4
+ default_prompt: "使用 $dada-object-avatar 根据名字和 Emoji 生成一张复古物件表情头像。"
@@ -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.
@@ -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()