pi-courier 0.1.41 → 0.1.42

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 (86) hide show
  1. package/assets/avatars/instance-01.png +0 -0
  2. package/assets/avatars/instance-02.png +0 -0
  3. package/assets/avatars/instance-03.png +0 -0
  4. package/assets/avatars/instance-04.png +0 -0
  5. package/assets/avatars/instance-05.png +0 -0
  6. package/assets/avatars/instance-06.png +0 -0
  7. package/assets/avatars/instance-07.png +0 -0
  8. package/assets/avatars/instance-08.png +0 -0
  9. package/assets/avatars/instance-09.png +0 -0
  10. package/assets/avatars/instance-10.png +0 -0
  11. package/assets/avatars/instance-11.png +0 -0
  12. package/assets/avatars/instance-12.png +0 -0
  13. package/assets/avatars/management.png +0 -0
  14. package/assets/avatars-drafts/.pi.txt +1 -0
  15. package/assets/avatars-drafts/instance-01-cat-raw.png +0 -0
  16. package/assets/avatars-drafts/instance-01-cat.png +0 -0
  17. package/assets/avatars-drafts/instance-01-cat2-raw.png +0 -0
  18. package/assets/avatars-drafts/instance-01-cat2.png +0 -0
  19. package/assets/avatars-drafts/instance-01-cat3-raw.png +0 -0
  20. package/assets/avatars-drafts/instance-01-cat3.png +0 -0
  21. package/assets/avatars-drafts/instance-01-raw.png +0 -0
  22. package/assets/avatars-drafts/instance-01-seedream-raw.png +0 -0
  23. package/assets/avatars-drafts/instance-01-seedream.png +0 -0
  24. package/assets/avatars-drafts/instance-01-v2-raw.png +0 -0
  25. package/assets/avatars-drafts/instance-01-v2.png +0 -0
  26. package/assets/avatars-drafts/instance-01-v3-raw.png +0 -0
  27. package/assets/avatars-drafts/instance-01-v3.png +0 -0
  28. package/assets/avatars-drafts/instance-01-v4-raw.png +0 -0
  29. package/assets/avatars-drafts/instance-01-v4.png +0 -0
  30. package/assets/avatars-drafts/instance-01-v5-raw.png +0 -0
  31. package/assets/avatars-drafts/instance-01-v5.png +0 -0
  32. package/assets/avatars-drafts/instance-01.png +0 -0
  33. package/assets/avatars-drafts/instance-02-raw.png +0 -0
  34. package/assets/avatars-drafts/instance-02-seedream-raw.png +0 -0
  35. package/assets/avatars-drafts/instance-02-seedream.png +0 -0
  36. package/assets/avatars-drafts/instance-02.png +0 -0
  37. package/assets/avatars-drafts/instance-03-raw.png +0 -0
  38. package/assets/avatars-drafts/instance-03-seedream-raw.png +0 -0
  39. package/assets/avatars-drafts/instance-03-seedream.png +0 -0
  40. package/assets/avatars-drafts/instance-03.png +0 -0
  41. package/assets/avatars-drafts/instance-04-raw.png +0 -0
  42. package/assets/avatars-drafts/instance-04-seedream-raw.png +0 -0
  43. package/assets/avatars-drafts/instance-04-seedream.png +0 -0
  44. package/assets/avatars-drafts/instance-04.png +0 -0
  45. package/assets/avatars-drafts/instance-05-raw.png +0 -0
  46. package/assets/avatars-drafts/instance-05-seedream-raw.png +0 -0
  47. package/assets/avatars-drafts/instance-05-seedream.png +0 -0
  48. package/assets/avatars-drafts/instance-05.png +0 -0
  49. package/assets/avatars-drafts/instance-06-raw.png +0 -0
  50. package/assets/avatars-drafts/instance-06-seedream-raw.png +0 -0
  51. package/assets/avatars-drafts/instance-06-seedream.png +0 -0
  52. package/assets/avatars-drafts/instance-06.png +0 -0
  53. package/assets/avatars-drafts/instance-07-raw.png +0 -0
  54. package/assets/avatars-drafts/instance-07-seedream-raw.png +0 -0
  55. package/assets/avatars-drafts/instance-07-seedream.png +0 -0
  56. package/assets/avatars-drafts/instance-07.png +0 -0
  57. package/assets/avatars-drafts/instance-08-raw.png +0 -0
  58. package/assets/avatars-drafts/instance-08-seedream-raw.png +0 -0
  59. package/assets/avatars-drafts/instance-08-seedream.png +0 -0
  60. package/assets/avatars-drafts/instance-08.png +0 -0
  61. package/assets/avatars-drafts/instance-09-raw.png +0 -0
  62. package/assets/avatars-drafts/instance-09-seedream-raw.png +0 -0
  63. package/assets/avatars-drafts/instance-09-seedream.png +0 -0
  64. package/assets/avatars-drafts/instance-09.png +0 -0
  65. package/assets/avatars-drafts/instance-10-raw.png +0 -0
  66. package/assets/avatars-drafts/instance-10-seedream-raw.png +0 -0
  67. package/assets/avatars-drafts/instance-10-seedream.png +0 -0
  68. package/assets/avatars-drafts/instance-10.png +0 -0
  69. package/assets/avatars-drafts/instance-11-raw.png +0 -0
  70. package/assets/avatars-drafts/instance-11-seedream-raw.png +0 -0
  71. package/assets/avatars-drafts/instance-11-seedream.png +0 -0
  72. package/assets/avatars-drafts/instance-11.png +0 -0
  73. package/assets/avatars-drafts/instance-12-raw.png +0 -0
  74. package/assets/avatars-drafts/instance-12-seedream-raw.png +0 -0
  75. package/assets/avatars-drafts/instance-12-seedream.png +0 -0
  76. package/assets/avatars-drafts/instance-12.png +0 -0
  77. package/assets/avatars-drafts/management-raw.png +0 -0
  78. package/assets/avatars-drafts/management-seedream-raw.png +0 -0
  79. package/assets/avatars-drafts/management-seedream.png +0 -0
  80. package/assets/avatars-drafts/management.png +0 -0
  81. package/assets/avatars-drafts/preview.html +14 -0
  82. package/package.json +1 -1
  83. package/scripts/generate-avatars-openrouter.mjs +174 -0
  84. package/scripts/generate-avatars-seedream.mjs +164 -0
  85. package/scripts/generate-avatars-zhipu.mjs +171 -0
  86. package/scripts/generate-avatars.mjs +4 -0
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
@@ -0,0 +1 @@
1
+ π
@@ -0,0 +1,14 @@
1
+ <!doctype html><html lang="zh"><head><meta charset="utf-8"><title>头像草稿审阅</title>
2
+ <style>
3
+ body{font-family:sans-serif;background:#1e2128;color:#eee;padding:24px}
4
+ .grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:16px;max-width:1400px}
5
+ .cell{background:#2a2e37;border-radius:12px;padding:14px;text-align:center}
6
+ .cell h3{margin:0 0 10px;font-size:15px} .cell small{color:#9aa3b2;font-weight:normal}
7
+ .pair{display:flex;gap:12px;justify-content:center;align-items:flex-end}
8
+ figure{margin:0} img{width:128px;height:128px;image-rendering:pixelated;background:#fff;border-radius:8px;display:block}
9
+ figcaption{font-size:12px;color:#9aa3b2;margin-top:6px}
10
+ .old img{opacity:.75}
11
+ </style></head><body>
12
+ <h1>头像草稿审阅(左:seedream 新版 / 右:旧版智谱)</h1>
13
+ <p>挑选方法:把满意的 seedream 图替换正式头像池,告诉我文件名即可,例如「全部替换」或「替换 01/02/05,07 重生成」。</p>
14
+ <div class="grid"><div class="cell"><h3>instance-01<br><small>橘猫</small></h3><div class="pair"><figure><img src="instance-01-seedream.png"><figcaption>seedream 新版</figcaption></figure><figure class="old"><img src="instance-01.png"><figcaption>旧版(智谱)</figcaption></figure></div></div><div class="cell"><h3>instance-02<br><small>企鹅</small></h3><div class="pair"><figure><img src="instance-02-seedream.png"><figcaption>seedream 新版</figcaption></figure><figure class="old"><img src="instance-02.png"><figcaption>旧版(智谱)</figcaption></figure></div></div><div class="cell"><h3>instance-03<br><small>狐狸</small></h3><div class="pair"><figure><img src="instance-03-seedream.png"><figcaption>seedream 新版</figcaption></figure><figure class="old"><img src="instance-03.png"><figcaption>旧版(智谱)</figcaption></figure></div></div><div class="cell"><h3>instance-04<br><small>猫头鹰</small></h3><div class="pair"><figure><img src="instance-04-seedream.png"><figcaption>seedream 新版</figcaption></figure><figure class="old"><img src="instance-04.png"><figcaption>旧版(智谱)</figcaption></figure></div></div><div class="cell"><h3>instance-05<br><small>白兔</small></h3><div class="pair"><figure><img src="instance-05-seedream.png"><figcaption>seedream 新版</figcaption></figure><figure class="old"><img src="instance-05.png"><figcaption>旧版(智谱)</figcaption></figure></div></div><div class="cell"><h3>instance-06<br><small>柴犬</small></h3><div class="pair"><figure><img src="instance-06-seedream.png"><figcaption>seedream 新版</figcaption></figure><figure class="old"><img src="instance-06.png"><figcaption>旧版(智谱)</figcaption></figure></div></div><div class="cell"><h3>instance-07<br><small>灰猫</small></h3><div class="pair"><figure><img src="instance-07-seedream.png"><figcaption>seedream 新版</figcaption></figure><figure class="old"><img src="instance-07.png"><figcaption>旧版(智谱)</figcaption></figure></div></div><div class="cell"><h3>instance-08<br><small>小棕熊</small></h3><div class="pair"><figure><img src="instance-08-seedream.png"><figcaption>seedream 新版</figcaption></figure><figure class="old"><img src="instance-08.png"><figcaption>旧版(智谱)</figcaption></figure></div></div><div class="cell"><h3>instance-09<br><small>熊猫</small></h3><div class="pair"><figure><img src="instance-09-seedream.png"><figcaption>seedream 新版</figcaption></figure><figure class="old"><img src="instance-09.png"><figcaption>旧版(智谱)</figcaption></figure></div></div><div class="cell"><h3>instance-10<br><small>青蛙</small></h3><div class="pair"><figure><img src="instance-10-seedream.png"><figcaption>seedream 新版</figcaption></figure><figure class="old"><img src="instance-10.png"><figcaption>旧版(智谱)</figcaption></figure></div></div><div class="cell"><h3>instance-11<br><small>松鼠</small></h3><div class="pair"><figure><img src="instance-11-seedream.png"><figcaption>seedream 新版</figcaption></figure><figure class="old"><img src="instance-11.png"><figcaption>旧版(智谱)</figcaption></figure></div></div><div class="cell"><h3>instance-12<br><small>小鸡</small></h3><div class="pair"><figure><img src="instance-12-seedream.png"><figcaption>seedream 新版</figcaption></figure><figure class="old"><img src="instance-12.png"><figcaption>旧版(智谱)</figcaption></figure></div></div><div class="cell"><h3>management<br><small>管理房间·皇冠猫</small></h3><div class="pair"><figure><img src="management-seedream.png"><figcaption>seedream 新版</figcaption></figure><figure class="old"><img src="management.png"><figcaption>旧版(智谱)</figcaption></figure></div></div></div></body></html>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-courier",
3
- "version": "0.1.41",
3
+ "version": "0.1.42",
4
4
  "description": "Run pi coding agent from Matrix. Slash commands, skills and prompts fully work from messengers via the RPC protocol.",
5
5
  "type": "module",
6
6
  "main": "dist/standalone.js",
@@ -0,0 +1,174 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Generate avatar drafts via OpenRouter text-to-image (bytedance-seed/seedream-4.5).
4
+ *
5
+ * NOT part of the runtime — this is a human-in-the-loop tool: it writes
6
+ * candidates to assets/avatars-drafts/ for review; winners get copied over
7
+ * the same-named file in assets/avatars/ by hand (the runtime pool contract:
8
+ * same filename, drop-in replacement).
9
+ *
10
+ * Usage:
11
+ * export OPENROUTER_API_KEY=sk-or-... # required
12
+ * node scripts/generate-avatars-openrouter.mjs # all 13
13
+ * node scripts/generate-avatars-openrouter.mjs --only instance-01,management
14
+ *
15
+ * Env:
16
+ * OPENROUTER_API_KEY (required) OpenRouter key
17
+ * OPENROUTER_MODEL (optional) default: google/gemini-2.5-flash-image
18
+ *
19
+ * Raw model output is kept as <name>-raw.<ext>; a 128×128 nearest-neighbour
20
+ * resize (ffmpeg, pixel-crisp) lands at <name>.png when ffmpeg is available.
21
+ */
22
+
23
+ import { execFileSync } from "node:child_process";
24
+ import { mkdirSync, writeFileSync, existsSync } from "node:fs";
25
+ import { dirname, join } from "node:path";
26
+ import { fileURLToPath } from "node:url";
27
+
28
+ const ROOT = join(dirname(fileURLToPath(import.meta.url)), "..");
29
+ const OUT_DIR = join(ROOT, "assets", "avatars-drafts");
30
+ const MODEL = process.env.OPENROUTER_MODEL || "google/gemini-2.5-flash-image";
31
+ const API = "https://openrouter.ai/api/v1/chat/completions";
32
+
33
+ const STYLE =
34
+ "Pixel art avatar icon, chunky blocky pixels, flat solid-color background, single subject, " +
35
+ "centered composition, clean silhouette, original artwork — must not resemble any existing " +
36
+ "game character, mob, logo or texture. Square format, no text, no watermark.";
37
+
38
+ const AVATARS = [
39
+ {
40
+ name: "instance-01",
41
+ prompt:
42
+ "A small amber-orange Greek letter π (pi) glyph placed in the BOTTOM-LEFT quarter of the canvas, " +
43
+ "occupying only about a quarter of the canvas area, on a dark slate-blue block background; " +
44
+ "the other three quarters stay mostly empty with a few subtle dim dots and a small plus mark as decoration. " + STYLE,
45
+ },
46
+ {
47
+ name: "instance-02",
48
+ prompt:
49
+ "A small pale-lavender Greek letter π (pi) glyph placed in the BOTTOM-LEFT quarter of the canvas, " +
50
+ "occupying only about a quarter of the canvas area, on a deep purple block background with a few " +
51
+ "tiny gold star sparkles scattered in the empty areas. " + STYLE,
52
+ },
53
+ { name: "instance-03", prompt: "A cute friendly robot head, teal/cyan tones, dark outline, square eyes and a small antenna. " + STYLE },
54
+ { name: "instance-04", prompt: "A cute friendly robot head, warm orange tones, dark outline, vertical slit eyes and two small horns. " + STYLE },
55
+ { name: "instance-05", prompt: "A shining cyan gem crystal on a gray stone block background. " + STYLE },
56
+ { name: "instance-06", prompt: "A green circuit board with golden traces, solder dots and a central dark chip. " + STYLE },
57
+ { name: "instance-07", prompt: "A gray cat face with pink inner ears and green eyes. " + STYLE },
58
+ { name: "instance-08", prompt: "A brown owl face with big square white eyes, orange beak and ear tufts. " + STYLE },
59
+ { name: "instance-09", prompt: "A small red-and-white rocket flying diagonally on a dark navy starry night background. " + STYLE },
60
+ { name: "instance-10", prompt: "A young green sprout with two leaves in a terracotta flower pot. " + STYLE },
61
+ { name: "instance-11", prompt: "A dark laptop with a glowing green command-line cursor on its screen. " + STYLE },
62
+ { name: "instance-12", prompt: "A white six-sided die showing the five-face, on a red block background. " + STYLE },
63
+ { name: "management", prompt: "A golden crown with red and cyan jewels on a dark navy block background. " + STYLE },
64
+ ];
65
+
66
+ // ---- OpenRouter call --------------------------------------------------------
67
+
68
+ function apiKey() {
69
+ const key = process.env.OPENROUTER_API_KEY;
70
+ if (!key) {
71
+ console.error("缺少 OPENROUTER_API_KEY 环境变量(OpenRouter 的 sk-or-... token)");
72
+ process.exit(1);
73
+ }
74
+ return key;
75
+ }
76
+
77
+ async function generateImage(key, prompt) {
78
+ const res = await fetch(API, {
79
+ method: "POST",
80
+ headers: { authorization: `Bearer ${key}`, "content-type": "application/json" },
81
+ body: JSON.stringify({
82
+ model: MODEL,
83
+ modalities: ["image", "text"],
84
+ messages: [{ role: "user", content: prompt }],
85
+ }),
86
+ });
87
+ if (!res.ok) {
88
+ const body = await res.text();
89
+ throw new Error(`HTTP ${res.status}: ${body.slice(0, 300)}`);
90
+ }
91
+ const data = await res.json();
92
+ const message = data?.choices?.[0]?.message;
93
+ const entry = message?.images?.[0];
94
+ const url = entry?.image_url?.url ?? entry?.url;
95
+ if (!url) {
96
+ const text = typeof message?.content === "string" ? message.content : JSON.stringify(message)?.slice(0, 300);
97
+ throw new Error(`响应中没有图片;模型文本:${text ?? "(空)"}`);
98
+ }
99
+ const match = /^data:([^;]+);base64,(.+)$/s.exec(url);
100
+ if (!match) throw new Error(`图片不是 base64 data URL: ${String(url).slice(0, 80)}...`);
101
+ return { mime: match[1], bytes: Buffer.from(match[2], "base64") };
102
+ }
103
+
104
+ // ---- ffmpeg post-process ----------------------------------------------------
105
+
106
+ function hasFfmpeg() {
107
+ try {
108
+ execFileSync("ffmpeg", ["-version"], { stdio: "ignore" });
109
+ return true;
110
+ } catch {
111
+ return false;
112
+ }
113
+ }
114
+
115
+ /** Nearest-neighbour resize to 128×128 — keeps the blocky look crisp and the
116
+ * file tiny. Falls back to the raw bytes when ffmpeg is unavailable. */
117
+ function toAvatar128(rawPath, outPath) {
118
+ try {
119
+ execFileSync(
120
+ "ffmpeg",
121
+ ["-y", "-loglevel", "error", "-i", rawPath, "-vf", "scale=128:128:flags=neighbor", outPath],
122
+ { stdio: "ignore" }
123
+ );
124
+ return true;
125
+ } catch {
126
+ return false;
127
+ }
128
+ }
129
+
130
+ // ---- run --------------------------------------------------------------------
131
+
132
+ const only = (() => {
133
+ const flag = process.argv.findIndex((a) => a === "--only" || a.startsWith("--only="));
134
+ if (flag === -1) return null;
135
+ const value = process.argv[flag].startsWith("--only=")
136
+ ? process.argv[flag].slice(7)
137
+ : process.argv[flag + 1] ?? "";
138
+ return value.split(",").filter(Boolean);
139
+ })();
140
+
141
+ const key = apiKey();
142
+ mkdirSync(OUT_DIR, { recursive: true });
143
+ const useFfmpeg = hasFfmpeg();
144
+ if (!useFfmpeg) console.log("⚠ 未找到 ffmpeg,保留原始大图,不做 128×128 缩放");
145
+
146
+ const targets = only ? AVATARS.filter((a) => only.includes(a.name)) : AVATARS;
147
+ const missing = only?.filter((n) => !AVATARS.some((a) => a.name === n));
148
+ if (missing?.length) {
149
+ console.error(`未知头像名: ${missing.join(",")}(可选: ${AVATARS.map((a) => a.name).join(" ")})`);
150
+ process.exit(1);
151
+ }
152
+
153
+ let ok = 0;
154
+ for (const avatar of targets) {
155
+ const attempts = 3;
156
+ for (let attempt = 1; attempt <= attempts; attempt++) {
157
+ try {
158
+ const { mime, bytes } = await generateImage(key, avatar.prompt);
159
+ const ext = mime === "image/jpeg" ? "jpg" : mime === "image/webp" ? "webp" : "png";
160
+ const rawPath = join(OUT_DIR, `${avatar.name}-raw.${ext}`);
161
+ writeFileSync(rawPath, bytes);
162
+ const outPath = join(OUT_DIR, `${avatar.name}.png`);
163
+ const resized = useFfmpeg && toAvatar128(rawPath, outPath) && existsSync(outPath);
164
+ console.log(`✔ ${avatar.name}(${mime}, ${(bytes.length / 1024).toFixed(0)} KB)→ ${resized ? "128×128" : "仅原图"}`);
165
+ ok++;
166
+ break;
167
+ } catch (err) {
168
+ console.warn(`✗ ${avatar.name} 第 ${attempt}/${attempts} 次失败: ${err.message ?? err}`);
169
+ if (attempt === attempts) console.error(`✗✗ ${avatar.name} 放弃,可稍后 --only=${avatar.name} 重试`);
170
+ else await new Promise((r) => setTimeout(r, 2000));
171
+ }
172
+ }
173
+ }
174
+ console.log(`\n完成 ${ok}/${targets.length} → ${OUT_DIR}`);
@@ -0,0 +1,164 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Generate animal-avatar drafts via OpenRouter /api/v1/images (seedream-4.5).
4
+ *
5
+ * Human-in-the-loop tool — candidates land in assets/avatars-drafts/ for
6
+ * review; winners are copied over the same-named file in assets/avatars/ by
7
+ * hand. Contract per avatar (user-approved template): perfectly symmetrical
8
+ * front-facing animal HEAD only, cut off at the chin, face filling the frame,
9
+ * sharp hard pixel edges (no anti-aliasing), solid background, tiny π letter
10
+ * in the extreme bottom-left corner, 8-bit retro game avatar style.
11
+ *
12
+ * Usage:
13
+ * export OPENROUTER_API_KEY=sk-or-... (or configure OpenRouter in ~/.zcode)
14
+ * node scripts/generate-avatars-seedream.mjs # all 13
15
+ * node scripts/generate-avatars-seedream.mjs --only instance-01
16
+ *
17
+ * Env: OPENROUTER_API_KEY, optional OPENROUTER_IMAGE_MODEL (default
18
+ * bytedance-seed/seedream-4.5).
19
+ */
20
+
21
+ import { execFileSync } from "node:child_process";
22
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
23
+ import { homedir } from "node:os";
24
+ import { dirname, join } from "node:path";
25
+ import { fileURLToPath } from "node:url";
26
+
27
+ const ROOT = join(dirname(fileURLToPath(import.meta.url)), "..");
28
+ const OUT_DIR = join(ROOT, "assets", "avatars-drafts");
29
+ const API = "https://openrouter.ai/api/v1/images";
30
+ const MODEL = process.env.OPENROUTER_IMAGE_MODEL || "bytedance-seed/seedream-4.5";
31
+
32
+ /** Per-animal subject line; the rest of the constraint frame is fixed. */
33
+ const ANIMALS = [
34
+ { name: "instance-01", subject: "an orange tabby cat's head", accent: "vibrant orange fur, symmetrical green eyes" },
35
+ { name: "instance-02", subject: "a penguin's head", accent: "black and white plumage, symmetrical round eyes, small orange beak" },
36
+ { name: "instance-03", subject: "a red fox's head", accent: "rust-orange fur, white muzzle, symmetrical amber eyes, pointed ears with dark tips" },
37
+ { name: "instance-04", subject: "a brown owl's head", accent: "brown feather pattern, big symmetrical round eyes, small sharp beak" },
38
+ { name: "instance-05", subject: "a white rabbit's head", accent: "soft white fur, long upright ears with pink inner side, symmetrical pink eyes" },
39
+ { name: "instance-06", subject: "a shiba inu dog's head", accent: "cream and orange fur, symmetrical happy squinting eyes" },
40
+ { name: "instance-07", subject: "a gray cat's head", accent: "blue-gray fur, symmetrical yellow eyes, unimused half-lidded look" },
41
+ { name: "instance-08", subject: "a brown bear cub's head", accent: "round fluffy brown fur, small rounded ears, symmetrical sleepy eyes" },
42
+ { name: "instance-09", subject: "a giant panda's head", accent: "white and black fur, black eye patches, symmetrical curious eyes" },
43
+ { name: "instance-10", subject: "a green frog's head", accent: "green skin, two bumps on top, symmetrical sleepy half-lidded eyes, wide closed mouth" },
44
+ { name: "instance-11", subject: "a red squirrel's head", accent: "russet fur, tufted ears, symmetrical bright excited eyes" },
45
+ { name: "instance-12", subject: "a yellow chick's head", accent: "fluffy yellow, tiny orange beak, symmetrical wide worried eyes" },
46
+ { name: "management", subject: "an orange cat's head wearing a small golden crown", accent: "vibrant orange fur, symmetrical smug green eyes, tiny golden crown between the ears" },
47
+ ];
48
+
49
+ const promptFor = (a) =>
50
+ `A flat 2D pixel art icon of ${a.subject}, perfectly symmetrical front-facing view, looking directly ` +
51
+ `at the viewer, eyes centered and staring straight ahead. Full frontal face. Head only, no neck, no body, ` +
52
+ `no chest. Face filling the entire frame, cut off exactly at the chin. ${a.accent}. ` +
53
+ `Sharp, crisp hard pixel edges, no anti-aliasing, isolated on a pure white solid background. ` +
54
+ `A small white Greek letter 'π' placed strictly in the extreme bottom left corner, not overlapping the face. ` +
55
+ `8-bit retro game avatar style.`;
56
+
57
+ // 负面提示词:封杀侧脸、偏头、透视、3D、身体与模糊(用户提供的定稿,全动物通用)
58
+ const NEGATIVE =
59
+ "3D, realistic, side profile, three-quarter view, perspective, angle, rotation, tilt, tilted head, " +
60
+ "looking away, body, chest, neck, shoulders, torso, blurry, fuzzy edges, soft, smooth, blending, " +
61
+ "gradient, shadows, depth, watermark, text box, logo background, overlapping, low quality, distorted";
62
+
63
+ // ---- key --------------------------------------------------------------------
64
+
65
+ function apiKey() {
66
+ if (process.env.OPENROUTER_API_KEY) return process.env.OPENROUTER_API_KEY;
67
+ try {
68
+ const cfg = JSON.parse(readFileSync(join(homedir(), ".zcode", "v2", "config.json"), "utf8"));
69
+ for (const p of Object.values(cfg.provider ?? {})) {
70
+ const opts = p.options ?? {};
71
+ if (String(opts.baseURL ?? "").includes("openrouter.ai") && opts.apiKey) return opts.apiKey;
72
+ }
73
+ } catch {
74
+ // fall through
75
+ }
76
+ console.error("缺少 OPENROUTER_API_KEY(或 ~/.zcode 里配置 OpenRouter provider)");
77
+ process.exit(1);
78
+ }
79
+
80
+ // ---- API --------------------------------------------------------------------
81
+
82
+ async function generateImage(key, prompt) {
83
+ const res = await fetch(API, {
84
+ method: "POST",
85
+ headers: { authorization: `Bearer ${key}`, "content-type": "application/json" },
86
+ body: JSON.stringify({ model: MODEL, prompt, negative_prompt: NEGATIVE }),
87
+ signal: AbortSignal.timeout(180_000), // 挂起的连接最多等 3 分钟,失败进重试
88
+ });
89
+ if (!res.ok) throw new Error(`HTTP ${res.status}: ${(await res.text()).slice(0, 250)}`);
90
+ const data = await res.json();
91
+ const item = data?.data?.[0];
92
+ if (item?.b64_json) return Buffer.from(item.b64_json, "base64");
93
+ if (item?.url) {
94
+ const img = await fetch(item.url);
95
+ if (!img.ok) throw new Error(`图片下载失败: HTTP ${img.status}`);
96
+ return Buffer.from(await img.arrayBuffer());
97
+ }
98
+ throw new Error(`响应无图: ${JSON.stringify(data).slice(0, 200)}`);
99
+ }
100
+
101
+ // ---- post-process -----------------------------------------------------------
102
+
103
+ function hasFfmpeg() {
104
+ try {
105
+ execFileSync("ffmpeg", ["-version"], { stdio: "ignore" });
106
+ return true;
107
+ } catch {
108
+ return false;
109
+ }
110
+ }
111
+
112
+ function toAvatar128(rawPath, outPath) {
113
+ execFileSync(
114
+ "ffmpeg",
115
+ ["-y", "-loglevel", "error", "-i", rawPath, "-vf", "scale=128:128:flags=neighbor", outPath],
116
+ { stdio: "ignore" }
117
+ );
118
+ }
119
+
120
+ // ---- run --------------------------------------------------------------------
121
+
122
+ const only = (() => {
123
+ const flag = process.argv.findIndex((a) => a === "--only" || a.startsWith("--only="));
124
+ if (flag === -1) return null;
125
+ const value = process.argv[flag].startsWith("--only=") ? process.argv[flag].slice(7) : process.argv[flag + 1] ?? "";
126
+ return value.split(",").filter(Boolean);
127
+ })();
128
+
129
+ const targets = only ? ANIMALS.filter((a) => only.includes(a.name)) : ANIMALS;
130
+ const missing = only?.filter((n) => !ANIMALS.some((a) => a.name === n));
131
+ if (missing?.length) {
132
+ console.error(`未知头像名: ${missing.join(",")}(可选: ${ANIMALS.map((a) => a.name).join(" ")})`);
133
+ process.exit(1);
134
+ }
135
+
136
+ const key = apiKey();
137
+ mkdirSync(OUT_DIR, { recursive: true });
138
+ const useFfmpeg = hasFfmpeg();
139
+
140
+ let ok = 0;
141
+ for (const avatar of targets) {
142
+ const attempts = 3;
143
+ for (let attempt = 1; attempt <= attempts; attempt++) {
144
+ try {
145
+ const bytes = await generateImage(key, promptFor(avatar));
146
+ const rawPath = join(OUT_DIR, `${avatar.name}-seedream-raw.png`);
147
+ writeFileSync(rawPath, bytes);
148
+ let note = "仅原图";
149
+ if (useFfmpeg) {
150
+ const outPath = join(OUT_DIR, `${avatar.name}-seedream.png`);
151
+ toAvatar128(rawPath, outPath);
152
+ note = "128×128";
153
+ }
154
+ console.log(`✔ ${avatar.name}(${(bytes.length / 1024).toFixed(0)} KB)→ ${note}`);
155
+ ok++;
156
+ break;
157
+ } catch (err) {
158
+ console.warn(`✗ ${avatar.name} 第 ${attempt}/${attempts} 次失败: ${err.message ?? err}`);
159
+ if (attempt === attempts) console.error(`✗✗ ${avatar.name} 放弃,可 --only=${avatar.name} 重试`);
160
+ else await new Promise((r) => setTimeout(r, 2000));
161
+ }
162
+ }
163
+ }
164
+ console.log(`\n完成 ${ok}/${targets.length} → ${OUT_DIR}`);
@@ -0,0 +1,171 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Generate animal-avatar drafts via 智谱 CogView-3-Flash (free tier).
4
+ *
5
+ * Human-in-the-loop tool: writes candidates to assets/avatars-drafts/ for
6
+ * review; winners are copied over the same-named file in assets/avatars/ by
7
+ * hand (runtime pool contract: same filename, drop-in replacement). Nothing
8
+ * here runs at pi-courier runtime and no key is shipped.
9
+ *
10
+ * Pipeline per avatar: CogView (Minecraft-style chunky pixel animal, ~90% of
11
+ * canvas, one distinct expression each) → ffmpeg crop of the right strip
12
+ * (CogView keeps painting garbled mini-text at the bottom-right edge) →
13
+ * 128×128 nearest-neighbour → drawtext amber π signature in the bottom-left
14
+ * (DejaVu Sans Bold; the text-model renders the glyph far better than image
15
+ * models do at tiny sizes).
16
+ *
17
+ * Usage:
18
+ * node scripts/generate-avatars-zhipu.mjs # all 13
19
+ * node scripts/generate-avatars-zhipu.mjs --only instance-02,management
20
+ * node scripts/generate-avatars-zhipu.mjs --only instance-05 # either syntax
21
+ *
22
+ * Key resolution: env ZHIPU_API_KEY, else the Bigmodel/GLM provider entry in
23
+ * ~/.zcode/v2/config.json (local convenience — never printed, never shipped).
24
+ */
25
+
26
+ import { execFileSync } from "node:child_process";
27
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
28
+ import { homedir } from "node:os";
29
+ import { dirname, join } from "node:path";
30
+ import { fileURLToPath } from "node:url";
31
+
32
+ const ROOT = join(dirname(fileURLToPath(import.meta.url)), "..");
33
+ const OUT_DIR = join(ROOT, "assets", "avatars-drafts");
34
+ const API = "https://open.bigmodel.cn/api/paas/v4/images/generations";
35
+ const MODEL = process.env.ZHIPU_IMAGE_MODEL || "cogview-3-flash"; // free tier
36
+ const FONT = "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf";
37
+
38
+ const STYLE =
39
+ "像游戏皮肤一样的大块平涂像素,整个画面就像低分辨率像素图被放大了很多倍,每个方块颗粒都清晰可见," +
40
+ "只用少数几种颜色平涂,没有渐变没有细腻阴影,边缘是锯齿方块";
41
+
42
+ const ANIMALS = [
43
+ { name: "instance-01", subject: "橘色小猫", look: "大头圆润,咧嘴大笑吐着舌头,表情夸张开心", bg: "深蓝灰色" },
44
+ { name: "instance-02", subject: "企鹅", look: "圆滚滚,呆萌好奇地张着小嘴", bg: "淡紫色" },
45
+ { name: "instance-03", subject: "狐狸", look: "尖耳朵大尾巴,狡黠地眯眼微笑", bg: "薄荷绿色" },
46
+ { name: "instance-04", subject: "猫头鹰", look: "圆眼睛一丝不苟的严肃表情", bg: "暖橙红色" },
47
+ { name: "instance-05", subject: "白兔", look: "长耳朵,害羞脸红", bg: "深绿色" },
48
+ { name: "instance-06", subject: "柴犬", look: "卷尾巴,经典的眯眼狗子微笑", bg: "雾蓝色" },
49
+ { name: "instance-07", subject: "灰色猫咪", look: "斜眼傲慢嫌弃的表情", bg: "暖米色" },
50
+ { name: "instance-08", subject: "小棕熊", look: "圆耳朵,困倦地打哈欠", bg: "青绿色" },
51
+ { name: "instance-09", subject: "熊猫", look: "黑眼圈,惊讶地张大嘴", bg: "天蓝色" },
52
+ { name: "instance-10", subject: "青蛙", look: "大嘴巴闭着微微上扬,慵懒地半睁着眼,看不到牙齿", bg: "浅粉色" },
53
+ { name: "instance-11", subject: "松鼠", look: "大尾巴,兴奋地睁大眼睛抱着一颗松果", bg: "淡黄色" },
54
+ { name: "instance-12", subject: "小鸡", look: "圆圆的黄色身子橙色小嘴,惊慌地张开小翅膀,只有黄白橙三色,配色简单干净", bg: "纯靛蓝色,没有任何景物" },
55
+ { name: "management", subject: "头顶戴着小小金色皇冠的橘猫", look: "得意的表情", bg: "深紫红色" },
56
+ ];
57
+
58
+ const promptFor = (a) =>
59
+ `我的世界游戏生物那样的超粗大方块像素画:一只${a.subject},${a.look},${STYLE};` +
60
+ `${a.subject}极其大,占据画面约九成,头顶和脚几乎贴到画布边缘,只留很窄的${a.bg}纯色背景边;` +
61
+ "整张图没有任何文字、字母或水印";
62
+
63
+ // ---- key --------------------------------------------------------------------
64
+
65
+ function apiKey() {
66
+ if (process.env.ZHIPU_API_KEY) return process.env.ZHIPU_API_KEY;
67
+ try {
68
+ const cfgPath = join(homedir(), ".zcode", "v2", "config.json");
69
+ const cfg = JSON.parse(readFileSync(cfgPath, "utf8"));
70
+ for (const p of Object.values(cfg.provider ?? {})) {
71
+ const opts = p.options ?? {};
72
+ if (String(opts.baseURL ?? "").includes("open.bigmodel.cn") && opts.apiKey) return opts.apiKey;
73
+ }
74
+ } catch {
75
+ // fall through to the error below
76
+ }
77
+ console.error("缺少智谱 API key:设 ZHIPU_API_KEY,或在 ~/.zcode/v2/config.json 配置 Bigmodel/GLM provider");
78
+ process.exit(1);
79
+ }
80
+
81
+ // ---- CogView call -----------------------------------------------------------
82
+
83
+ async function generateImage(key, prompt) {
84
+ const res = await fetch(API, {
85
+ method: "POST",
86
+ headers: { authorization: `Bearer ${key}`, "content-type": "application/json" },
87
+ body: JSON.stringify({ model: MODEL, size: "1024x1024", prompt }),
88
+ });
89
+ if (!res.ok) throw new Error(`HTTP ${res.status}: ${(await res.text()).slice(0, 200)}`);
90
+ const data = await res.json();
91
+ const item = data?.data?.[0];
92
+ if (!item) throw new Error(`响应无图: ${JSON.stringify(data).slice(0, 200)}`);
93
+ if (item.b64_json) return Buffer.from(item.b64_json, "base64");
94
+ if (item.url) {
95
+ const img = await fetch(item.url);
96
+ if (!img.ok) throw new Error(`图片下载失败: HTTP ${img.status}`);
97
+ return Buffer.from(await img.arrayBuffer());
98
+ }
99
+ throw new Error("响应既无 url 也无 b64_json");
100
+ }
101
+
102
+ // ---- ffmpeg pipeline --------------------------------------------------------
103
+
104
+ function ffmpeg(args, quiet = true) {
105
+ execFileSync("ffmpeg", ["-y", "-loglevel", "error", ...args], { stdio: quiet ? "ignore" : "inherit" });
106
+ }
107
+
108
+ /** Right strip crop kills CogView's garbled corner text, then 128×128
109
+ * nearest-neighbour keeps the blocks chunky. */
110
+ function toAvatar128(rawPath, outPath) {
111
+ ffmpeg(["-i", rawPath, "-vf", "crop=920:1024:0:0,scale=128:128:flags=neighbor", outPath]);
112
+ }
113
+
114
+ function stampPi(outPath) {
115
+ if (!existsSync(FONT)) {
116
+ console.warn(`⚠ 未找到 ${FONT},跳过 π 签名(仅此一张)`);
117
+ return false;
118
+ }
119
+ ffmpeg([
120
+ "-i", outPath,
121
+ "-vf",
122
+ `drawtext=textfile=${join(OUT_DIR, ".pi.txt")}:fontfile=${FONT}:fontsize=17:fontcolor=0xF6A821:x=8:y=128-th-8`,
123
+ join(OUT_DIR, ".tmp.png"),
124
+ ]);
125
+ execFileSync("mv", [join(OUT_DIR, ".tmp.png"), outPath]);
126
+ return true;
127
+ }
128
+
129
+ // ---- run --------------------------------------------------------------------
130
+
131
+ const only = (() => {
132
+ const flag = process.argv.findIndex((a) => a === "--only" || a.startsWith("--only="));
133
+ if (flag === -1) return null;
134
+ const value = process.argv[flag].startsWith("--only=") ? process.argv[flag].slice(7) : process.argv[flag + 1] ?? "";
135
+ return value.split(",").filter(Boolean);
136
+ })();
137
+
138
+ const targets = only ? ANIMALS.filter((a) => only.includes(a.name)) : ANIMALS;
139
+ const missing = only?.filter((n) => !ANIMALS.some((a) => a.name === n));
140
+ if (missing?.length) {
141
+ console.error(`未知头像名: ${missing.join(",")}(可选: ${ANIMALS.map((a) => a.name).join(" ")})`);
142
+ process.exit(1);
143
+ }
144
+
145
+ const key = apiKey();
146
+ mkdirSync(OUT_DIR, { recursive: true });
147
+ writeFileSync(join(OUT_DIR, ".pi.txt"), "π");
148
+
149
+ let ok = 0;
150
+ for (const avatar of targets) {
151
+ const prompt = promptFor(avatar);
152
+ const attempts = 3;
153
+ for (let attempt = 1; attempt <= attempts; attempt++) {
154
+ try {
155
+ const bytes = await generateImage(key, prompt);
156
+ const rawPath = join(OUT_DIR, `${avatar.name}-raw.png`);
157
+ writeFileSync(rawPath, bytes);
158
+ const outPath = join(OUT_DIR, `${avatar.name}.png`);
159
+ toAvatar128(rawPath, outPath);
160
+ const stamped = stampPi(outPath);
161
+ console.log(`✔ ${avatar.name}(${avatar.subject},${(bytes.length / 1024).toFixed(0)} KB)${stamped ? " +π" : ""}`);
162
+ ok++;
163
+ break;
164
+ } catch (err) {
165
+ console.warn(`✗ ${avatar.name} 第 ${attempt}/${attempts} 次失败: ${err.message ?? err}`);
166
+ if (attempt === attempts) console.error(`✗✗ ${avatar.name} 放弃,可 --only=${avatar.name} 重试`);
167
+ else await new Promise((r) => setTimeout(r, 2000));
168
+ }
169
+ }
170
+ }
171
+ console.log(`\n完成 ${ok}/${targets.length} → ${OUT_DIR}`);
@@ -1,5 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
+ * ⚠️ RETIRED v1 generator — running this overwrites the current AI-generated
4
+ * avatar pool (assets/avatars/, made with scripts/generate-avatars-seedream.mjs)
5
+ * with the old hand-drawn designs. Only run it deliberately to roll back.
6
+ *
3
7
  * Generate the bundled pixel avatars (assets/avatars/*.png) — 12 generic pool
4
8
  * images + 1 dedicated management-room image. Blocky pixel style, 16×16 grid
5
9
  * rendered at 8× (128×128), zero dependencies (zlib PNG encoder below).