@playcraft/cli 0.0.39 → 0.0.41

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 (121) hide show
  1. package/README.md +66 -3
  2. package/dist/atom-plan/validate-atom-plan.js +298 -0
  3. package/dist/cli-root-help.js +1 -1
  4. package/dist/commands/3d.js +363 -0
  5. package/dist/commands/create.js +337 -0
  6. package/dist/commands/fix-ids.js +17 -3
  7. package/dist/commands/fix-ids.test.js +264 -0
  8. package/dist/commands/image.js +1337 -43
  9. package/dist/commands/login.js +60 -2
  10. package/dist/commands/recommend.js +1 -1
  11. package/dist/commands/remix.js +213 -0
  12. package/dist/commands/skills.js +1379 -0
  13. package/dist/commands/tools-3d.js +473 -0
  14. package/dist/commands/tools-generation.js +454 -0
  15. package/dist/commands/tools-project.js +400 -0
  16. package/dist/commands/tools-research.js +37 -0
  17. package/dist/commands/tools-research.test.js +216 -0
  18. package/dist/commands/tools-utils.js +164 -0
  19. package/dist/commands/tools.js +7 -616
  20. package/dist/config.js +2 -0
  21. package/dist/index.js +20 -2
  22. package/dist/utils/agent-api-client.js +52 -16
  23. package/package.json +9 -3
  24. package/project-template/.claude/agents/designer.md +116 -0
  25. package/project-template/.claude/agents/developer.md +133 -0
  26. package/project-template/.claude/agents/pm.md +164 -0
  27. package/project-template/.claude/agents/refs/README.md +67 -0
  28. package/project-template/.claude/agents/refs/designer-art-style-catalog.md +533 -0
  29. package/project-template/.claude/agents/refs/designer-color-audio-recipes.md +153 -0
  30. package/project-template/.claude/agents/refs/designer-deliverable-spec.md +167 -0
  31. package/project-template/.claude/agents/refs/designer-dimension-axis.md +27 -0
  32. package/project-template/.claude/agents/refs/designer-handoff-v2-checklist.md +68 -0
  33. package/project-template/.claude/agents/refs/designer-master-composite-recipes.md +216 -0
  34. package/project-template/.claude/agents/refs/designer-style-exploration-flow.md +37 -0
  35. package/project-template/.claude/agents/refs/developer-dev-handoff.md +109 -0
  36. package/project-template/.claude/agents/refs/developer-impl-cookbook.md +134 -0
  37. package/project-template/.claude/agents/refs/developer-phase1-flow.md +211 -0
  38. package/project-template/.claude/agents/refs/pm-workflow-detail.md +545 -0
  39. package/project-template/.claude/agents/refs/reviewer-six-dimension-eval.md +286 -0
  40. package/project-template/.claude/agents/refs/ta-3d-flip-recipe.md +85 -0
  41. package/project-template/.claude/agents/refs/ta-atlas-deliverable-standard.md +46 -0
  42. package/project-template/.claude/agents/refs/ta-batch-pipeline-recipes.md +120 -0
  43. package/project-template/.claude/agents/refs/ta-image-generation-detail.md +356 -0
  44. package/project-template/.claude/agents/refs/ta-image-ops-reference.md +495 -0
  45. package/project-template/.claude/agents/refs/ta-pipeline-cookbook.md +699 -0
  46. package/project-template/.claude/agents/refs/ta-tools-reference.md +111 -0
  47. package/project-template/.claude/agents/refs/ta-vfx-preset-catalog.md +365 -0
  48. package/project-template/.claude/agents/reviewer.md +103 -0
  49. package/project-template/.claude/agents/technical-artist.md +111 -0
  50. package/project-template/.claude/hooks/README.md +36 -0
  51. package/project-template/.claude/hooks/validate-atom-plan.mjs +224 -0
  52. package/project-template/.claude/hooks/validate-workflow-stop.mjs +258 -0
  53. package/project-template/.claude/settings.json +32 -0
  54. package/project-template/.claude/settings.local.json +4 -0
  55. package/project-template/.claude/skills/playcraft-ad-psychology/SKILL.md +182 -0
  56. package/project-template/.claude/skills/playcraft-art-style-guide/SKILL.md +123 -0
  57. package/project-template/.claude/skills/playcraft-asset-state-sheet/SKILL.md +141 -0
  58. package/project-template/.claude/skills/playcraft-audio-generation/SKILL.md +280 -0
  59. package/project-template/.claude/skills/playcraft-batch-pipeline/SKILL.md +184 -0
  60. package/project-template/.claude/skills/playcraft-build-optimizer/SKILL.md +306 -0
  61. package/project-template/.claude/skills/playcraft-image-generation/SKILL.md +229 -0
  62. package/project-template/.claude/skills/playcraft-image-generation/reference/build-sprite-sheet.template.mjs +123 -0
  63. package/project-template/.claude/skills/playcraft-image-generation/reference/compare-style.template.mjs +254 -0
  64. package/project-template/.claude/skills/playcraft-image-generation/reference/gen-batch-sprite.template.mjs +235 -0
  65. package/project-template/.claude/skills/playcraft-image-generation/reference/gen-batch.template.mjs +97 -0
  66. package/project-template/.claude/skills/playcraft-image-generation/reference/gen-edit-variants.template.mjs +118 -0
  67. package/project-template/.claude/skills/playcraft-image-generation/reference/process-batch.template.mjs +137 -0
  68. package/project-template/.claude/skills/playcraft-image-generation/reference/prompt-cookbook.md +397 -0
  69. package/project-template/.claude/skills/playcraft-image-generation/reference/validate-sprite-sheet.template.mjs +296 -0
  70. package/project-template/.claude/skills/playcraft-image-ops/SKILL.md +122 -0
  71. package/project-template/.claude/skills/playcraft-masking/SKILL.md +373 -0
  72. package/project-template/.claude/skills/playcraft-research/SKILL.md +212 -0
  73. package/project-template/.claude/skills/playcraft-sprite-generation/SKILL.md +423 -0
  74. package/project-template/.claude/skills/playcraft-storyboard/SKILL.md +148 -0
  75. package/project-template/.claude/skills/playcraft-style-qa/SKILL.md +270 -0
  76. package/project-template/.claude/skills/playcraft-text-rendering/SKILL.md +236 -0
  77. package/project-template/.claude/skills/playcraft-vfx-animation/SKILL.md +130 -0
  78. package/project-template/.claude/skills/playcraft-workflow/SKILL.md +396 -0
  79. package/project-template/.cursor/hooks.json +17 -0
  80. package/project-template/.cursor/rules/playcraft-orchestrator.mdc +87 -0
  81. package/project-template/.cursor/rules/playcraft-subagent-boundary.mdc +18 -0
  82. package/project-template/CLAUDE.md +240 -0
  83. package/project-template/assets/audio/bgm/.gitkeep +0 -0
  84. package/project-template/assets/audio/sfx/.gitkeep +0 -0
  85. package/project-template/assets/bundles/.gitkeep +0 -0
  86. package/project-template/assets/images/bg/.gitkeep +0 -0
  87. package/project-template/assets/images/reference/.gitkeep +0 -0
  88. package/project-template/assets/images/storyboard/.gitkeep +0 -0
  89. package/project-template/assets/images/tiles/.gitkeep +0 -0
  90. package/project-template/assets/images/ui/.gitkeep +0 -0
  91. package/project-template/assets/images/vfx/.gitkeep +0 -0
  92. package/project-template/assets/models/.gitkeep +0 -0
  93. package/project-template/docs/team/agent-conduct.md +105 -0
  94. package/project-template/docs/team/agent-runtime-matrix.md +62 -0
  95. package/project-template/docs/team/atom-plan-format.md +74 -0
  96. package/project-template/docs/team/collaboration.md +288 -0
  97. package/project-template/docs/team/core-model.md +50 -0
  98. package/project-template/docs/team/platform-capabilities.md +15 -0
  99. package/project-template/docs/team/workflow-changelog.md +51 -0
  100. package/project-template/docs/team/workflow-consistency-checklist.md +128 -0
  101. package/project-template/game/config/.gitkeep +0 -0
  102. package/project-template/game/gameplay/.gitkeep +0 -0
  103. package/project-template/game/scenes/.gitkeep +0 -0
  104. package/project-template/logs/.gitkeep +0 -0
  105. package/project-template/ta-workspace/logs/.gitkeep +0 -0
  106. package/project-template/ta-workspace/scripts/.gitkeep +0 -0
  107. package/project-template/ta-workspace/tmp/.gitkeep +0 -0
  108. package/project-template/templates/atom-plan.template.json +26 -0
  109. package/project-template/templates/atom-plan.template.md +76 -0
  110. package/project-template/templates/design-brief.template.md +195 -0
  111. package/project-template/templates/design-lens-checklist.reference.md +117 -0
  112. package/project-template/templates/design-methodology.md +99 -0
  113. package/project-template/templates/designer-log.template.md +98 -0
  114. package/project-template/templates/developer-log.template.md +140 -0
  115. package/project-template/templates/five-axis-framework.md +186 -0
  116. package/project-template/templates/intent-clarifications.template.md +58 -0
  117. package/project-template/templates/layout-spec.template.md +132 -0
  118. package/project-template/templates/project-state.template.md +219 -0
  119. package/project-template/templates/review-report.template.md +166 -0
  120. package/project-template/templates/style-exploration.template.md +93 -0
  121. package/project-template/templates/ta-log.template.md +205 -0
@@ -0,0 +1,254 @@
1
+ /**
2
+ * compare-style.template.mjs
3
+ *
4
+ * 色彩风格一致性定量检查脚本。
5
+ * 用途:TA Micro-Batch Validation 辅助工具,对 AI 补全素材与 Designer 样本做客观色彩比较。
6
+ *
7
+ * 使用方法:
8
+ * cp .claude/skills/playcraft-image-generation/reference/compare-style.template.mjs \
9
+ * ta-workspace/scripts/compare-style.mjs
10
+ * # 修改下方 CONFIG 区域后运行:
11
+ * node ta-workspace/scripts/compare-style.mjs
12
+ *
13
+ * 输出示例:
14
+ * [COMPARE] tile_sample_1.png vs tile_comp_01.png
15
+ * 色相距离(加权): 8.3° → ✅ 优秀(< 15°)
16
+ * 明度差异: 0.06 → ✅ 优秀(< 0.10)
17
+ * 饱和度差异: 0.04 → ✅ 优秀(< 0.10)
18
+ * 综合一致性评分: 4.7 / 5 → ✅ 通过(≥ 3.0)
19
+ *
20
+ * 依赖:@gltf-transform/core(CLI 已内置)、sharp(CLI 已内置)
21
+ * 若 sharp 未全局可用,运行前:npm install sharp --no-save
22
+ */
23
+
24
+ import { execSync } from 'child_process';
25
+ import { existsSync, mkdirSync, appendFileSync } from 'fs';
26
+ import path from 'path';
27
+
28
+ // ─── CONFIG ───────────────────────────────────────────────────────────────────
29
+
30
+ const CONFIG = {
31
+ /** Designer 样本图(风格基准) */
32
+ referenceImage: 'assets/images/tiles/tile_sample_1.png',
33
+
34
+ /** 要检查的 TA 补全素材目录或文件列表 */
35
+ targetDir: 'assets/images/tiles',
36
+
37
+ /** 只检查该目录下的哪些文件(glob 后缀匹配) */
38
+ targetGlob: '.png',
39
+
40
+ /** 排除文件名包含这些关键词的文件(Designer 样本自身) */
41
+ excludeKeywords: ['sample', 'concept', 'selected', 'storyboard'],
42
+
43
+ /** 日志输出路径 */
44
+ logFile: 'ta-workspace/logs/style-compare.log',
45
+
46
+ /** 评分阈值(低于此值输出警告) */
47
+ warnThreshold: 3.0,
48
+ };
49
+
50
+ // ─── HELPERS ──────────────────────────────────────────────────────────────────
51
+
52
+ /**
53
+ * 用 playcraft image info 提取图片的 dominant colors(HSL 列表)
54
+ * 返回格式:[{ h, s, l, weight }](weight 为面积占比估算)
55
+ */
56
+ function extractDominantColors(imagePath) {
57
+ try {
58
+ const raw = execSync(
59
+ `playcraft image info --input "${imagePath}" --json`,
60
+ { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }
61
+ );
62
+ const info = JSON.parse(raw);
63
+
64
+ // playcraft image info 返回 dominantColors 数组:[{ r, g, b, hex, percent }]
65
+ const colors = (info.dominantColors || []).slice(0, 5);
66
+ return colors.map(c => {
67
+ const hsl = rgbToHsl(c.r, c.g, c.b);
68
+ return { ...hsl, weight: (c.percent || 20) / 100 };
69
+ });
70
+ } catch {
71
+ return [];
72
+ }
73
+ }
74
+
75
+ function rgbToHsl(r, g, b) {
76
+ r /= 255; g /= 255; b /= 255;
77
+ const max = Math.max(r, g, b), min = Math.min(r, g, b);
78
+ const l = (max + min) / 2;
79
+ if (max === min) return { h: 0, s: 0, l };
80
+ const d = max - min;
81
+ const s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
82
+ let h;
83
+ switch (max) {
84
+ case r: h = ((g - b) / d + (g < b ? 6 : 0)) / 6; break;
85
+ case g: h = ((b - r) / d + 2) / 6; break;
86
+ default: h = ((r - g) / d + 4) / 6;
87
+ }
88
+ return { h: h * 360, s, l };
89
+ }
90
+
91
+ /**
92
+ * 计算两组 dominant colors 之间的加权色相距离(°)
93
+ * 色相距离取最短弧(0–180°)
94
+ */
95
+ function weightedHueDist(colorsA, colorsB) {
96
+ if (!colorsA.length || !colorsB.length) return 999;
97
+ let totalDist = 0, totalWeight = 0;
98
+ for (const a of colorsA) {
99
+ // 找 B 中色相最近的颜色
100
+ let minDist = Infinity;
101
+ for (const b of colorsB) {
102
+ const raw = Math.abs(a.h - b.h);
103
+ const dist = raw > 180 ? 360 - raw : raw;
104
+ if (dist < minDist) minDist = dist;
105
+ }
106
+ totalDist += minDist * a.weight;
107
+ totalWeight += a.weight;
108
+ }
109
+ return totalWeight > 0 ? totalDist / totalWeight : 999;
110
+ }
111
+
112
+ /**
113
+ * 计算明度 / 饱和度的加权平均差异(0–1)
114
+ */
115
+ function weightedChannelDiff(colorsA, colorsB, channel) {
116
+ if (!colorsA.length || !colorsB.length) return 1;
117
+ const avgA = colorsA.reduce((s, c) => s + c[channel] * c.weight, 0) /
118
+ colorsA.reduce((s, c) => s + c.weight, 0);
119
+ const avgB = colorsB.reduce((s, c) => s + c[channel] * c.weight, 0) /
120
+ colorsB.reduce((s, c) => s + c.weight, 0);
121
+ return Math.abs(avgA - avgB);
122
+ }
123
+
124
+ /**
125
+ * 综合一致性评分 1–5
126
+ *
127
+ * 评分规则(与 playcraft-style-qa Micro-Batch 标准对齐):
128
+ * 色相距离 < 15° 且 明度/饱和度差 < 0.10 → 5 分
129
+ * 色相距离 < 20° 且 明度/饱和度差 < 0.15 → 4 分
130
+ * 色相距离 < 30° 且 明度/饱和度差 < 0.20 → 3 分(勉强可接受)
131
+ * 色相距离 < 45° 且 明度/饱和度差 < 0.30 → 2 分(需调整)
132
+ * 其他 → 1 分(停止,提问 Designer)
133
+ */
134
+ function calcScore(hueDist, lightDiff, satDiff) {
135
+ const channelOk = Math.max(lightDiff, satDiff);
136
+ if (hueDist < 15 && channelOk < 0.10) return 5;
137
+ if (hueDist < 20 && channelOk < 0.15) return 4;
138
+ if (hueDist < 30 && channelOk < 0.20) return 3;
139
+ if (hueDist < 45 && channelOk < 0.30) return 2;
140
+ return 1;
141
+ }
142
+
143
+ function scoreLabel(score) {
144
+ if (score >= 4) return '✅ 良好';
145
+ if (score >= 3) return '⚠️ 可接受';
146
+ return '❌ 偏差过大,需停止';
147
+ }
148
+
149
+ function hueLabel(dist) {
150
+ if (dist < 15) return '✅ 优秀(< 15°)';
151
+ if (dist < 30) return '⚠️ 偏差(< 30°)';
152
+ return '❌ 严重偏差(≥ 30°)';
153
+ }
154
+
155
+ function channelLabel(diff) {
156
+ if (diff < 0.10) return '✅ 优秀(< 0.10)';
157
+ if (diff < 0.20) return '⚠️ 偏差(< 0.20)';
158
+ return '❌ 严重偏差(≥ 0.20)';
159
+ }
160
+
161
+ // ─── MAIN ─────────────────────────────────────────────────────────────────────
162
+
163
+ async function main() {
164
+ const logDir = path.dirname(CONFIG.logFile);
165
+ if (!existsSync(logDir)) mkdirSync(logDir, { recursive: true });
166
+
167
+ const refPath = CONFIG.referenceImage;
168
+ if (!existsSync(refPath)) {
169
+ console.error(`[ERROR] 参考图不存在: ${refPath}`);
170
+ process.exit(1);
171
+ }
172
+
173
+ console.log(`\n📊 色彩风格一致性检查`);
174
+ console.log(` 参考图: ${refPath}\n`);
175
+
176
+ const refColors = extractDominantColors(refPath);
177
+ if (!refColors.length) {
178
+ console.error('[ERROR] 无法从参考图提取颜色,请确认 playcraft image info 可用');
179
+ process.exit(1);
180
+ }
181
+
182
+ // 收集目标文件
183
+ let targets = [];
184
+ try {
185
+ const { readdirSync } = await import('fs');
186
+ targets = readdirSync(CONFIG.targetDir)
187
+ .filter(f => f.endsWith(CONFIG.targetGlob))
188
+ .filter(f => !CONFIG.excludeKeywords.some(kw => f.includes(kw)))
189
+ .map(f => path.join(CONFIG.targetDir, f));
190
+ } catch {
191
+ console.error(`[ERROR] 无法读取目录: ${CONFIG.targetDir}`);
192
+ process.exit(1);
193
+ }
194
+
195
+ if (!targets.length) {
196
+ console.log(' 未找到需要检查的目标文件');
197
+ return;
198
+ }
199
+
200
+ const results = [];
201
+ let failCount = 0;
202
+
203
+ for (const target of targets) {
204
+ const targetColors = extractDominantColors(target);
205
+ if (!targetColors.length) {
206
+ console.log(` [SKIP] ${path.basename(target)} — 无法提取颜色`);
207
+ continue;
208
+ }
209
+
210
+ const hueDist = weightedHueDist(refColors, targetColors);
211
+ const lightDiff = weightedChannelDiff(refColors, targetColors, 'l');
212
+ const satDiff = weightedChannelDiff(refColors, targetColors, 's');
213
+ const score = calcScore(hueDist, lightDiff, satDiff);
214
+
215
+ if (score < CONFIG.warnThreshold) failCount++;
216
+
217
+ const line = [
218
+ `[COMPARE] ${path.basename(refPath)} vs ${path.basename(target)}`,
219
+ ` 色相距离(加权): ${hueDist.toFixed(1)}° → ${hueLabel(hueDist)}`,
220
+ ` 明度差异: ${lightDiff.toFixed(2)} → ${channelLabel(lightDiff)}`,
221
+ ` 饱和度差异: ${satDiff.toFixed(2)} → ${channelLabel(satDiff)}`,
222
+ ` 综合一致性评分: ${score} / 5 → ${scoreLabel(score)}`,
223
+ '',
224
+ ].join('\n');
225
+
226
+ console.log(line);
227
+ results.push({ file: path.basename(target), score, hueDist, lightDiff, satDiff });
228
+ }
229
+
230
+ // 汇总
231
+ const avgScore = results.reduce((s, r) => s + r.score, 0) / (results.length || 1);
232
+ const summary = [
233
+ `=== 汇总 ===`,
234
+ ` 检查文件数: ${results.length}`,
235
+ ` 平均评分: ${avgScore.toFixed(1)} / 5`,
236
+ ` 低于阈值数: ${failCount}(阈值 ${CONFIG.warnThreshold})`,
237
+ failCount > 0
238
+ ? ` ⚠️ 建议:停止低分文件类型的批量生产,调整生成参数或向 Designer 提问 ICP`
239
+ : ` ✅ 全部通过,可继续批量生产`,
240
+ '',
241
+ ].join('\n');
242
+
243
+ console.log(summary);
244
+
245
+ // 写日志
246
+ const timestamp = new Date().toISOString();
247
+ appendFileSync(CONFIG.logFile, `\n[${timestamp}]\n${results.map(r =>
248
+ `${r.file}: score=${r.score} hue=${r.hueDist.toFixed(1)}° L=${r.lightDiff.toFixed(2)} S=${r.satDiff.toFixed(2)}`
249
+ ).join('\n')}\n${summary}`);
250
+
251
+ if (failCount > 0) process.exit(1);
252
+ }
253
+
254
+ main().catch(e => { console.error(e); process.exit(1); });
@@ -0,0 +1,235 @@
1
+ // ============================================================
2
+ // gen-batch-sprite.template.mjs — gpt-image-2 精灵图直出脚本模板
3
+ //
4
+ // 核心原则:让 gpt-image-2 一次 API 调用直接生成包含多个元素的
5
+ // 网格精灵图,然后用 sprite-split 切分。比逐个生成效率高 N 倍。
6
+ //
7
+ // 适用场景:
8
+ // - 卡牌(扑克/麻将/游戏卡):一批 13 张以内
9
+ // - 图标集:一批 9-16 张以内
10
+ // - 数字/字母贴图:一批 ≤16 个
11
+ //
12
+ // 使用方式:
13
+ // 1. cp .claude/skills/playcraft-image-generation/reference/gen-batch-sprite.template.mjs ta-workspace/scripts/gen-<name>-sprite.mjs
14
+ // 2. 修改下方 CONFIG 和 BATCHES 区域
15
+ // 3. node ta-workspace/scripts/gen-<name>-sprite.mjs
16
+ // ============================================================
17
+
18
+ import { execSync } from 'child_process';
19
+ import { existsSync, mkdirSync, appendFileSync } from 'fs';
20
+
21
+ // ============ CONFIG — 修改这里 ============
22
+ const CONFIG = {
23
+ /**
24
+ * 精灵图直出模型:永远用 gpt-image-2(图像理解 + 布局能力最强)
25
+ * mulerouter 质量更高(建议作为主模型),iegg 更快(备用)
26
+ * 注意:精灵图生成比单图慢,通常需要 90-150s,timeout 设 240 以上
27
+ */
28
+ model: 'mulerouter/gpt-image-2',
29
+
30
+ /**
31
+ * 每批精灵图的列数
32
+ * 行数由 items.length / columns 自动计算。
33
+ * 推荐:每批 ≤16 个元素(如 columns=4 则每批最多 4 行),超过会出现混乱。
34
+ */
35
+ columns: 4,
36
+
37
+ /**
38
+ * 宽高比:根据 columns/rows 比例选择
39
+ * - 1:1 → 4×4 方形网格
40
+ * - 3:2 → 4×3 横向网格(4列3行)
41
+ * - 2:3 → 3×4 纵向网格(3列4行)
42
+ * - 16:9 → 适合宽横向布局
43
+ */
44
+ aspectRatio: '1:1',
45
+
46
+ /** 生成的精灵图输出目录 */
47
+ sheetDir: 'assets/gen/sheets',
48
+ /** sprite-split 切分后的单图输出目录 */
49
+ outputDir: 'assets/images/batch',
50
+
51
+ /**
52
+ * 通用风格描述(追加到每个批次 prompt 末尾)
53
+ *
54
+ * 元素需要透明背景(独立元素:角色/车/道具/图标)→ 绿幕策略:
55
+ * style: 'flat 2D illustration, isolated on solid bright green #00FF00 background, centered, no shadow'
56
+ * removeBg: true (切分后逐帧去背景)
57
+ *
58
+ * 元素本身就是矩形(卡牌/棋子/UI 面板)→ 白底策略:
59
+ * style: 'flat 2D, white background, no shadow, no drop shadow, clean edges'
60
+ * removeBg: false
61
+ */
62
+ style: 'flat 2D illustration, isolated on solid bright green #00FF00 background, centered, no shadow',
63
+ /**
64
+ * 切分后是否对每帧执行 remove-background(绿幕去背景)
65
+ * true → 独立元素(需透明通道):remove-background --tolerance 25
66
+ * false → 卡牌/UI 面板等矩形元素:直接 resize,不去背景
67
+ */
68
+ removeBg: true,
69
+ };
70
+
71
+ // ============ DATA — 分批定义元素 ============
72
+ // 规则:每批 ≤ CONFIG.columns × CONFIG.rows 个元素
73
+ // 每个元素必须有 name(输出文件名)和 desc(精灵图中该格的内容描述)
74
+ const BATCHES = [
75
+ {
76
+ sheetName: 'sheet_batch_1',
77
+ items: [
78
+ { name: 'item_01', desc: '<格 1 内容描述>' },
79
+ { name: 'item_02', desc: '<格 2 内容描述>' },
80
+ { name: 'item_03', desc: '<格 3 内容描述>' },
81
+ { name: 'item_04', desc: '<格 4 内容描述>' },
82
+ // 最多 CONFIG.columns × CONFIG.rows 个
83
+ ],
84
+ },
85
+ {
86
+ sheetName: 'sheet_batch_2',
87
+ items: [
88
+ { name: 'item_05', desc: '<格 5 内容描述>' },
89
+ { name: 'item_06', desc: '<格 6 内容描述>' },
90
+ // ...
91
+ ],
92
+ },
93
+ // 继续添加批次...
94
+ ];
95
+
96
+ // ============ 精灵图 Prompt 构建器 ============
97
+ /**
98
+ * 根据 items 列表构建精灵图网格 prompt
99
+ * 关键词组合让 gpt-image-2 理解"网格布局"意图
100
+ */
101
+ function buildSpriteSheetPrompt(items, columns, style, removeBg) {
102
+ const actualRows = Math.ceil(items.length / columns);
103
+ const cellDescs = items.map((item, i) => {
104
+ const col = (i % columns) + 1;
105
+ const row = Math.floor(i / columns) + 1;
106
+ return `[Row${row},Col${col}]: ${item.desc}`;
107
+ }).join('; ');
108
+
109
+ // 绿幕模式强制在 prompt 里明确背景色,避免模型用白底
110
+ const bgInstruction = removeBg
111
+ ? 'Each element isolated on solid bright green #00FF00 background, no shadow, no gradient background.'
112
+ : 'Each element on clean white background.';
113
+
114
+ return [
115
+ `Sprite sheet grid layout, ${columns} columns × ${actualRows} rows.`,
116
+ `Each cell contains one element. Cell contents: ${cellDescs}.`,
117
+ bgInstruction,
118
+ `Grid layout, uniform cell size, equal spacing, elements perfectly centered in each cell.`,
119
+ `No borders between cells, no labels, no numbers on cells.`,
120
+ style,
121
+ ].join(' ');
122
+ }
123
+
124
+ // ============ LOGIC — 通常不需要修改 ============
125
+
126
+ const LOG_FILE = 'ta-workspace/logs/gen-batch-sprite.log';
127
+ mkdirSync(CONFIG.sheetDir, { recursive: true });
128
+ mkdirSync(CONFIG.outputDir, { recursive: true });
129
+ mkdirSync('ta-workspace/logs', { recursive: true });
130
+
131
+ function log(msg) {
132
+ const line = `[${new Date().toISOString()}] ${msg}`;
133
+ console.log(line);
134
+ appendFileSync(LOG_FILE, line + '\n');
135
+ }
136
+
137
+ const totalElements = BATCHES.reduce((s, b) => s + b.items.length, 0);
138
+ log(`=== Batch Sprite Generation Start ===`);
139
+ log(`Total: ${BATCHES.length} batches, ${totalElements} elements`);
140
+ log(`Model: ${CONFIG.model}`);
141
+
142
+ let doneBatches = 0, failedBatches = 0;
143
+
144
+ for (let bi = 0; bi < BATCHES.length; bi++) {
145
+ const batch = BATCHES[bi];
146
+ const sheetPath = `${CONFIG.sheetDir}/${batch.sheetName}.png`;
147
+
148
+ // ── Step 1: 生成精灵图(幂等跳过)────────────────────────────────
149
+ if (existsSync(sheetPath)) {
150
+ log(`[SKIP-SHEET] (${bi + 1}/${BATCHES.length}) ${batch.sheetName} already exists`);
151
+ } else {
152
+ log(`[GEN-SHEET ] (${bi + 1}/${BATCHES.length}) Generating ${batch.sheetName} (${batch.items.length} elements)`);
153
+ const prompt = buildSpriteSheetPrompt(batch.items, CONFIG.columns, CONFIG.style, CONFIG.removeBg);
154
+ const promptEscaped = prompt.replace(/"/g, '\\"');
155
+
156
+ const cmd =
157
+ `playcraft tools generate-image` +
158
+ ` --prompt "${promptEscaped}"` +
159
+ ` --output "${sheetPath}"` +
160
+ ` --aspect-ratio ${CONFIG.aspectRatio}` +
161
+ ` --image-model "${CONFIG.model}"` +
162
+ ` --timeout 240`;
163
+
164
+ try {
165
+ execSync(cmd, { stdio: 'inherit', timeout: 300_000 });
166
+ log(`[OK-SHEET ] ${batch.sheetName}`);
167
+ } catch (e) {
168
+ log(`[FAIL-SHEET] ${batch.sheetName}: ${e.message?.slice(0, 200)}`);
169
+ failedBatches++;
170
+ continue;
171
+ }
172
+ }
173
+
174
+ // ── Step 2: sprite-split 切分精灵图 ──────────────────────────────
175
+ const actualRows = Math.ceil(batch.items.length / CONFIG.columns);
176
+ // 检查第一个切分结果是否已存在(幂等)
177
+ const firstItemPath = `${CONFIG.outputDir}/${batch.items[0]?.name}.png`;
178
+ if (batch.items[0] && existsSync(firstItemPath)) {
179
+ log(`[SKIP-SPLIT] ${batch.sheetName} already split`);
180
+ doneBatches++;
181
+ continue;
182
+ }
183
+
184
+ log(`[SPLIT ] Splitting ${batch.sheetName} (${CONFIG.columns}×${actualRows})`);
185
+ const splitCmd =
186
+ `playcraft image sprite-split` +
187
+ ` --input "${sheetPath}"` +
188
+ ` --output-dir "ta-workspace/tmp/${batch.sheetName}"` +
189
+ ` --rows ${actualRows}` +
190
+ ` --columns ${CONFIG.columns}`;
191
+
192
+ try {
193
+ execSync(splitCmd, { stdio: 'inherit' });
194
+ } catch (e) {
195
+ log(`[FAIL-SPLIT] ${batch.sheetName}: ${e.message?.slice(0, 200)}`);
196
+ failedBatches++;
197
+ continue;
198
+ }
199
+
200
+ // ── Step 3: 重命名(+可选绿幕去背景)切分结果到最终位置 ─────────
201
+ log(`[RENAME ] Renaming ${batch.items.length} cells${CONFIG.removeBg ? ' + remove-background' : ''}`);
202
+ for (let j = 0; j < batch.items.length; j++) {
203
+ const item = batch.items[j];
204
+ const row = Math.floor(j / CONFIG.columns);
205
+ const col = j % CONFIG.columns;
206
+ // sprite-split 默认命名:frame_r{row}_c{col}.png(0-indexed)
207
+ const srcPath = `ta-workspace/tmp/${batch.sheetName}/frame_r${row}_c${col}.png`;
208
+ const dstPath = `${CONFIG.outputDir}/${item.name}.png`;
209
+
210
+ if (!existsSync(srcPath)) {
211
+ log(`[WARN] Expected cell not found: ${srcPath} (index ${j} out of ${batch.items.length})`);
212
+ continue;
213
+ }
214
+ try {
215
+ if (CONFIG.removeBg) {
216
+ // 绿幕去背景(tolerance 25 适合纯绿色 #00FF00 背景)
217
+ execSync(`playcraft image remove-background --input "${srcPath}" --output "${dstPath}" --tolerance 25`);
218
+ } else {
219
+ execSync(`cp "${srcPath}" "${dstPath}"`);
220
+ }
221
+ } catch (e) {
222
+ log(`[WARN] Failed to process ${srcPath} → ${dstPath}: ${e.message?.slice(0, 80)}`);
223
+ }
224
+ }
225
+
226
+ log(`[OK-BATCH ] (${bi + 1}/${BATCHES.length}) ${batch.sheetName} → ${batch.items.length} files`);
227
+ doneBatches++;
228
+ }
229
+
230
+ log(`\n=== Complete: Done=${doneBatches} Failed=${failedBatches}/${BATCHES.length} batches ===`);
231
+ log(`Output: ${CONFIG.outputDir}`);
232
+ if (failedBatches > 0) {
233
+ log('Re-run to retry failed batches (successful batches will be skipped)');
234
+ process.exit(1);
235
+ }
@@ -0,0 +1,97 @@
1
+ // ============================================================
2
+ // gen-batch.template.mjs — 批量 AI 生图脚本模板
3
+ // 用途:从零批量生成一组风格一致的素材(reference-image 模式)
4
+ //
5
+ // 使用方式:
6
+ // 1. cp .claude/skills/playcraft-image-generation/reference/gen-batch.template.mjs ta-workspace/scripts/gen-<name>.mjs
7
+ // 2. 修改下方 CONFIG 和 ITEMS 区域
8
+ // 3. node ta-workspace/scripts/gen-<name>.mjs
9
+ // ============================================================
10
+
11
+ import { execSync } from 'child_process';
12
+ import { existsSync, mkdirSync, appendFileSync } from 'fs';
13
+
14
+ // ============ CONFIG — 修改这里 ============
15
+ const CONFIG = {
16
+ /**
17
+ * 生图模型。
18
+ * - Phase 1 概念图(追求质量):用 gpt-image-2
19
+ * model: 'iegg-litellm/gpt-image-2'
20
+ * - Phase 2 批量(reference-image 一致性):用 google/,更稳定
21
+ * model: 'google/gemini-3.1-flash-image-preview'
22
+ *
23
+ * 后端按优先级自动 fallback 多个 provider,无需手动指定 provider。
24
+ * text+image CAPABILITY 的模型才支持 --reference-image
25
+ */
26
+ model: 'google/gemini-3.1-flash-image-preview',
27
+ /** 概念图路径(确认过风格的样本,所有后续图的参考来源) */
28
+ referenceImage: 'assets/images/concept/approved_sample.png',
29
+ /** 输出目录 */
30
+ outputDir: 'assets/images/batch',
31
+ /** 图片比例 */
32
+ aspectRatio: '1:1',
33
+ /**
34
+ * 统一风格描述(从 design-brief.md 复制,粘贴到每个 prompt 末尾)
35
+ * 示例:"flat 2D icon, vibrant colors, clean edges, white background, no shadow"
36
+ */
37
+ style: '<从 design-brief 复制的统一风格描述>',
38
+ };
39
+ // ============ DATA — 定义要生成的素材列表 ============
40
+ const ITEMS = [
41
+ { name: 'item_01', desc: '<素材描述 1>' },
42
+ { name: 'item_02', desc: '<素材描述 2>' },
43
+ // 继续添加...
44
+ ];
45
+ // ============ LOGIC — 通常不需要修改 ============
46
+
47
+ const LOG_FILE = 'ta-workspace/logs/gen-batch.log';
48
+ mkdirSync(CONFIG.outputDir, { recursive: true });
49
+ mkdirSync('ta-workspace/logs', { recursive: true });
50
+
51
+ function log(msg) {
52
+ const line = `[${new Date().toISOString()}] ${msg}`;
53
+ console.log(line);
54
+ appendFileSync(LOG_FILE, line + '\n');
55
+ }
56
+
57
+ log(`=== Batch Generation Start ===`);
58
+ log(`Total: ${ITEMS.length} | Model: ${CONFIG.model}`);
59
+
60
+ let done = 0, skipped = 0, failed = 0;
61
+ const failures = [];
62
+
63
+ for (let i = 0; i < ITEMS.length; i++) {
64
+ const item = ITEMS[i];
65
+ const outPath = `${CONFIG.outputDir}/${item.name}.png`;
66
+
67
+ if (existsSync(outPath)) {
68
+ log(`[SKIP] (${i + 1}/${ITEMS.length}) ${item.name}`);
69
+ skipped++;
70
+ continue;
71
+ }
72
+
73
+ log(`[GEN ] (${i + 1}/${ITEMS.length}) ${item.name}`);
74
+
75
+ const promptEscaped = `${item.desc}. ${CONFIG.style}`.replace(/"/g, '\\"');
76
+ const cmd =
77
+ `playcraft tools generate-image` +
78
+ ` --prompt "${promptEscaped}"` +
79
+ ` --output "${outPath}"` +
80
+ ` --aspect-ratio ${CONFIG.aspectRatio}` +
81
+ ` --reference-image "${CONFIG.referenceImage}"` +
82
+ ` --image-model "${CONFIG.model}"`;
83
+
84
+ try {
85
+ execSync(cmd, { stdio: 'inherit', timeout: 120_000 });
86
+ done++;
87
+ log(`[OK ] ${item.name}`);
88
+ } catch (e) {
89
+ log(`[FAIL] ${item.name}: ${e.message?.slice(0, 100)}`);
90
+ failures.push(item.name);
91
+ failed++;
92
+ }
93
+ }
94
+
95
+ log(`\n=== Complete: Done=${done} Skipped=${skipped} Failed=${failed}/${ITEMS.length} ===`);
96
+ if (failures.length > 0) log(`Failed: ${failures.join(', ')}`);
97
+ if (failed > 0) process.exit(1);