@playcraft/cli 0.0.40 → 0.0.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 (130) 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/image.js +1337 -43
  7. package/dist/commands/recommend.js +1 -1
  8. package/dist/commands/remix.js +213 -0
  9. package/dist/commands/skills.js +1379 -0
  10. package/dist/commands/tools-3d.js +473 -0
  11. package/dist/commands/tools-generation.js +452 -0
  12. package/dist/commands/tools-project.js +400 -0
  13. package/dist/commands/tools-research.js +37 -0
  14. package/dist/commands/tools-research.test.js +216 -0
  15. package/dist/commands/tools-utils.js +183 -0
  16. package/dist/commands/tools.js +7 -616
  17. package/dist/config.js +2 -0
  18. package/dist/index.js +19 -1
  19. package/dist/utils/version-checker.js +8 -11
  20. package/package.json +9 -3
  21. package/project-template/.claude/agents/designer.md +120 -0
  22. package/project-template/.claude/agents/developer.md +124 -0
  23. package/project-template/.claude/agents/pm.md +164 -0
  24. package/project-template/.claude/agents/refs/README.md +73 -0
  25. package/project-template/.claude/agents/refs/designer-art-style-catalog.md +533 -0
  26. package/project-template/.claude/agents/refs/designer-color-audio-recipes.md +153 -0
  27. package/project-template/.claude/agents/refs/designer-deliverable-spec.md +191 -0
  28. package/project-template/.claude/agents/refs/designer-dimension-axis.md +27 -0
  29. package/project-template/.claude/agents/refs/designer-handoff-v2-checklist.md +68 -0
  30. package/project-template/.claude/agents/refs/designer-master-composite-recipes.md +208 -0
  31. package/project-template/.claude/agents/refs/designer-style-exploration-flow.md +37 -0
  32. package/project-template/.claude/agents/refs/developer-dev-handoff.md +109 -0
  33. package/project-template/.claude/agents/refs/developer-impl-cookbook.md +134 -0
  34. package/project-template/.claude/agents/refs/developer-phase1-flow.md +136 -0
  35. package/project-template/.claude/agents/refs/pm-workflow-detail.md +551 -0
  36. package/project-template/.claude/agents/refs/reviewer-convergence-eval.md +130 -0
  37. package/project-template/.claude/agents/refs/reviewer-six-dimension-eval.md +6 -0
  38. package/project-template/.claude/agents/refs/ta-3d-flip-recipe.md +85 -0
  39. package/project-template/.claude/agents/refs/ta-atlas-deliverable-standard.md +67 -0
  40. package/project-template/.claude/agents/refs/ta-batch-pipeline-recipes.md +120 -0
  41. package/project-template/.claude/agents/refs/ta-image-generation-detail.md +356 -0
  42. package/project-template/.claude/agents/refs/ta-image-ops-reference.md +495 -0
  43. package/project-template/.claude/agents/refs/ta-pipeline-cookbook.md +1108 -0
  44. package/project-template/.claude/agents/refs/ta-tools-reference.md +111 -0
  45. package/project-template/.claude/agents/refs/ta-vfx-preset-catalog.md +365 -0
  46. package/project-template/.claude/agents/reviewer.md +127 -0
  47. package/project-template/.claude/agents/technical-artist.md +122 -0
  48. package/project-template/.claude/hooks/README.md +44 -0
  49. package/project-template/.claude/hooks/validate-atom-plan.mjs +224 -0
  50. package/project-template/.claude/hooks/validate-workflow-stop.mjs +343 -0
  51. package/project-template/.claude/settings.json +36 -0
  52. package/project-template/.claude/settings.local.json +4 -0
  53. package/project-template/.claude/skills/playcraft-ad-psychology/SKILL.md +182 -0
  54. package/project-template/.claude/skills/playcraft-art-style-guide/SKILL.md +123 -0
  55. package/project-template/.claude/skills/playcraft-asset-state-sheet/SKILL.md +141 -0
  56. package/project-template/.claude/skills/playcraft-audio-generation/SKILL.md +280 -0
  57. package/project-template/.claude/skills/playcraft-batch-pipeline/SKILL.md +184 -0
  58. package/project-template/.claude/skills/playcraft-build-optimizer/SKILL.md +306 -0
  59. package/project-template/.claude/skills/playcraft-image-generation/SKILL.md +279 -0
  60. package/project-template/.claude/skills/playcraft-image-generation/reference/build-sprite-sheet.template.mjs +123 -0
  61. package/project-template/.claude/skills/playcraft-image-generation/reference/compare-style.template.mjs +254 -0
  62. package/project-template/.claude/skills/playcraft-image-generation/reference/gen-batch-sprite.template.mjs +235 -0
  63. package/project-template/.claude/skills/playcraft-image-generation/reference/gen-batch.template.mjs +97 -0
  64. package/project-template/.claude/skills/playcraft-image-generation/reference/gen-edit-variants.template.mjs +118 -0
  65. package/project-template/.claude/skills/playcraft-image-generation/reference/process-batch.template.mjs +137 -0
  66. package/project-template/.claude/skills/playcraft-image-generation/reference/prompt-cookbook.md +397 -0
  67. package/project-template/.claude/skills/playcraft-image-generation/reference/validate-sprite-sheet.template.mjs +296 -0
  68. package/project-template/.claude/skills/playcraft-image-ops/SKILL.md +122 -0
  69. package/project-template/.claude/skills/playcraft-masking/SKILL.md +373 -0
  70. package/project-template/.claude/skills/playcraft-research/SKILL.md +212 -0
  71. package/project-template/.claude/skills/playcraft-sprite-generation/SKILL.md +423 -0
  72. package/project-template/.claude/skills/playcraft-storyboard/SKILL.md +167 -0
  73. package/project-template/.claude/skills/playcraft-style-qa/SKILL.md +270 -0
  74. package/project-template/.claude/skills/playcraft-text-rendering/SKILL.md +236 -0
  75. package/project-template/.claude/skills/playcraft-vfx-animation/SKILL.md +130 -0
  76. package/project-template/.claude/skills/playcraft-workflow/SKILL.md +485 -0
  77. package/project-template/.claude/skills/playwright-cli/SKILL.md +390 -0
  78. package/project-template/.claude/skills/playwright-cli/references/element-attributes.md +23 -0
  79. package/project-template/.claude/skills/playwright-cli/references/playwright-tests.md +39 -0
  80. package/project-template/.claude/skills/playwright-cli/references/request-mocking.md +87 -0
  81. package/project-template/.claude/skills/playwright-cli/references/running-code.md +240 -0
  82. package/project-template/.claude/skills/playwright-cli/references/session-management.md +226 -0
  83. package/project-template/.claude/skills/playwright-cli/references/spec-driven-testing.md +312 -0
  84. package/project-template/.claude/skills/playwright-cli/references/storage-state.md +275 -0
  85. package/project-template/.claude/skills/playwright-cli/references/test-generation.md +138 -0
  86. package/project-template/.claude/skills/playwright-cli/references/tracing.md +142 -0
  87. package/project-template/.claude/skills/playwright-cli/references/video-recording.md +157 -0
  88. package/project-template/.cursor/hooks.json +17 -0
  89. package/project-template/.cursor/rules/playcraft-orchestrator.mdc +137 -0
  90. package/project-template/.cursor/rules/playcraft-subagent-boundary.mdc +18 -0
  91. package/project-template/CLAUDE.md +280 -0
  92. package/project-template/assets/audio/bgm/.gitkeep +0 -0
  93. package/project-template/assets/audio/sfx/.gitkeep +0 -0
  94. package/project-template/assets/bundles/.gitkeep +0 -0
  95. package/project-template/assets/images/bg/.gitkeep +0 -0
  96. package/project-template/assets/images/reference/.gitkeep +0 -0
  97. package/project-template/assets/images/storyboard/.gitkeep +0 -0
  98. package/project-template/assets/images/tiles/.gitkeep +0 -0
  99. package/project-template/assets/images/ui/.gitkeep +0 -0
  100. package/project-template/assets/images/vfx/.gitkeep +0 -0
  101. package/project-template/assets/models/.gitkeep +0 -0
  102. package/project-template/docs/team/agent-conduct.md +121 -0
  103. package/project-template/docs/team/agent-runtime-matrix.md +62 -0
  104. package/project-template/docs/team/atom-plan-format.md +105 -0
  105. package/project-template/docs/team/collaboration.md +297 -0
  106. package/project-template/docs/team/core-model.md +50 -0
  107. package/project-template/docs/team/platform-capabilities.md +15 -0
  108. package/project-template/docs/team/workflow-changelog.md +65 -0
  109. package/project-template/docs/team/workflow-consistency-checklist.md +140 -0
  110. package/project-template/game/config/.gitkeep +0 -0
  111. package/project-template/game/gameplay/.gitkeep +0 -0
  112. package/project-template/game/scenes/.gitkeep +0 -0
  113. package/project-template/logs/.gitkeep +0 -0
  114. package/project-template/ta-workspace/logs/.gitkeep +0 -0
  115. package/project-template/ta-workspace/scripts/.gitkeep +0 -0
  116. package/project-template/ta-workspace/tmp/.gitkeep +0 -0
  117. package/project-template/templates/atom-plan.template.json +26 -0
  118. package/project-template/templates/atom-plan.template.md +108 -0
  119. package/project-template/templates/design-brief.template.md +195 -0
  120. package/project-template/templates/design-lens-checklist.reference.md +117 -0
  121. package/project-template/templates/design-methodology.md +99 -0
  122. package/project-template/templates/designer-log.template.md +114 -0
  123. package/project-template/templates/developer-log.template.md +134 -0
  124. package/project-template/templates/five-axis-framework.md +186 -0
  125. package/project-template/templates/intent-clarifications.template.md +58 -0
  126. package/project-template/templates/layout-spec.template.md +146 -0
  127. package/project-template/templates/project-state.template.md +237 -0
  128. package/project-template/templates/review-report.template.md +91 -0
  129. package/project-template/templates/style-exploration.template.md +93 -0
  130. package/project-template/templates/ta-log.template.md +343 -0
@@ -0,0 +1,473 @@
1
+ import { writeFileSync, mkdirSync, readFileSync } from 'fs';
2
+ import { dirname } from 'path';
3
+ import { AgentApiClient } from '../utils/agent-api-client.js';
4
+ import { mimeTypeForImagePath, handleError } from './tools-utils.js';
5
+ const AVATAR_TEMPLATES = [
6
+ 'basketball', 'badminton', 'pingpong', 'gymnastics', 'pilidance',
7
+ 'tennis', 'athletics', 'footballboykicking1', 'footballboykicking2',
8
+ 'guitar', 'footballboy', 'skateboard', 'futuresoilder', 'explorer',
9
+ 'beardollgirl', 'bibpantsboy', 'womansitpose', 'womanstandpose2',
10
+ 'mysteriousprincess', 'manstandpose2',
11
+ ];
12
+ export function register3DCommands(tools) {
13
+ tools.command('list-3d-models')
14
+ .description('List available 3D model generation configs (modelKind=3d)')
15
+ .option('--json', 'Output as JSON')
16
+ .action(async (opts) => {
17
+ try {
18
+ const client = new AgentApiClient();
19
+ const models = await client.get('/3d-models');
20
+ if (opts.json) {
21
+ console.log(JSON.stringify({ models }, null, 2));
22
+ return;
23
+ }
24
+ if (!models.length) {
25
+ console.log('No 3D model configs found.');
26
+ console.log('Add one at Settings → AI Config → 3D Model Generation');
27
+ console.log('\nRecommended: Tencent Cloud HunyuanTo3D');
28
+ console.log(' Provider: tencent-cloud');
29
+ console.log(' Model: hy-3d-3.0 (quality) or hy-3d-express (fast)');
30
+ return;
31
+ }
32
+ console.log('\nAvailable 3D Model Configs:\n');
33
+ for (const m of models) {
34
+ const mark = m.isDefault ? ' [default]' : '';
35
+ console.log(` ${m.modelRef}${mark}`);
36
+ console.log(` ${m.capability}`);
37
+ }
38
+ console.log('\nUsage: playcraft tools generate-3d --prompt "..." --output model.glb');
39
+ }
40
+ catch (e) {
41
+ handleError(e);
42
+ }
43
+ });
44
+ tools.command('generate-3d')
45
+ .description('Generate a 3D model from a text prompt or input image (uses HunyuanTo3D or configured provider)')
46
+ .option('--prompt <text>', 'Text description of the 3D model to generate')
47
+ .option('--input <path>', 'Input image (local path or URL) for image-to-3D generation')
48
+ .requiredOption('--output <path>', 'Output model file path')
49
+ .option('--model <ref>', 'Model ref in provider/model-id format (e.g. tencent-cloud/hy-3d-3.1)')
50
+ .option('--format <fmt>', 'Output format: glb|obj|stl|fbx|usdz (default: glb)', 'glb')
51
+ .option('--generate-type <type>', 'Generation mode: Normal|Geometry|LowPoly|Sketch (Geometry = white model without texture)')
52
+ .option('--face-count <n>', 'Target polygon count (3000–1500000, default: 500000)', parseInt)
53
+ .option('--enable-pbr', 'Enable PBR material generation (base color + normal + roughness + metalness maps)')
54
+ .option('--polygon-type <type>', 'Polygon mesh type for LowPoly mode: triangle|quadrilateral')
55
+ .option('--input-mime <mime>', 'MIME type of input image (default: auto-detected or image/png)')
56
+ .option('--save-preview <path>', 'Save preview image to this path (PNG)')
57
+ .action(async (opts) => {
58
+ try {
59
+ if (!opts.prompt && !opts.input) {
60
+ console.error('Error: --prompt or --input is required');
61
+ process.exit(1);
62
+ }
63
+ const client = new AgentApiClient();
64
+ const payload = {
65
+ outputFormat: opts.format ?? 'glb',
66
+ };
67
+ if (opts.prompt)
68
+ payload.prompt = opts.prompt;
69
+ if (opts.model)
70
+ payload.modelRef = opts.model;
71
+ if (opts.generateType)
72
+ payload.generateType = opts.generateType;
73
+ if (opts.faceCount != null)
74
+ payload.faceCount = opts.faceCount;
75
+ if (opts.enablePbr)
76
+ payload.enablePBR = true;
77
+ if (opts.polygonType)
78
+ payload.polygonType = opts.polygonType;
79
+ if (opts.input) {
80
+ let inputImageBase64;
81
+ let inputImageMimeType = opts.inputMime;
82
+ const isUrl = opts.input.startsWith('http://') || opts.input.startsWith('https://');
83
+ if (isUrl) {
84
+ console.log(`Downloading input image from URL...`);
85
+ try {
86
+ const response = await fetch(opts.input);
87
+ if (!response.ok) {
88
+ console.error(`Error: Failed to download image from URL (HTTP ${response.status})`);
89
+ process.exit(1);
90
+ }
91
+ if (!inputImageMimeType) {
92
+ const ct = response.headers.get('content-type') ?? '';
93
+ inputImageMimeType = ct.split(';')[0].trim() || 'image/png';
94
+ }
95
+ const buffer = Buffer.from(await response.arrayBuffer());
96
+ inputImageBase64 = buffer.toString('base64');
97
+ }
98
+ catch (downloadErr) {
99
+ const msg = downloadErr instanceof Error ? downloadErr.message : String(downloadErr);
100
+ console.error(`Error: Failed to download image from URL: ${msg}`);
101
+ process.exit(1);
102
+ }
103
+ }
104
+ else {
105
+ if (!inputImageMimeType) {
106
+ inputImageMimeType = mimeTypeForImagePath(opts.input);
107
+ }
108
+ try {
109
+ inputImageBase64 = readFileSync(opts.input).toString('base64');
110
+ }
111
+ catch {
112
+ console.error(`Error: Input image file not found: ${opts.input}`);
113
+ process.exit(1);
114
+ }
115
+ }
116
+ payload.inputImageBase64 = inputImageBase64;
117
+ if (inputImageMimeType)
118
+ payload.inputImageMimeType = inputImageMimeType;
119
+ }
120
+ const modelRef = opts.model ?? 'default configured 3D model';
121
+ console.log(`Generating 3D model...`);
122
+ console.log(` Model: ${modelRef}`);
123
+ if (opts.prompt)
124
+ console.log(` Prompt: ${opts.prompt}`);
125
+ if (opts.input)
126
+ console.log(` Input image: ${opts.input}`);
127
+ if (opts.generateType)
128
+ console.log(` Generate type: ${opts.generateType}`);
129
+ if (opts.faceCount)
130
+ console.log(` Face count: ${opts.faceCount}`);
131
+ if (opts.enablePbr)
132
+ console.log(` PBR: enabled`);
133
+ console.log(` (This may take 1–5 minutes for quality models)`);
134
+ const t0 = Date.now();
135
+ const result = await client.post('/generate-3d', payload);
136
+ const elapsed = ((Date.now() - t0) / 1000).toFixed(1);
137
+ const glbBuffer = Buffer.from(result.modelBase64, 'base64');
138
+ const outputDir = dirname(opts.output);
139
+ mkdirSync(outputDir, { recursive: true });
140
+ writeFileSync(opts.output, glbBuffer);
141
+ const sizeMB = (glbBuffer.length / 1024 / 1024).toFixed(2);
142
+ console.log(`\n3D model generated: ${opts.output} (${sizeMB}MB, ${elapsed}s)`);
143
+ console.log(` Provider: ${result.provider} / ${result.model}`);
144
+ if (opts.savePreview && result.previewImageBase64) {
145
+ const previewBuf = Buffer.from(result.previewImageBase64, 'base64');
146
+ const previewDir = dirname(opts.savePreview);
147
+ mkdirSync(previewDir, { recursive: true });
148
+ writeFileSync(opts.savePreview, previewBuf);
149
+ console.log(` Preview saved: ${opts.savePreview}`);
150
+ }
151
+ console.log(`\nNext steps:`);
152
+ console.log(` Inspect: playcraft 3d info ${opts.output}`);
153
+ console.log(` Apply texture: playcraft tools generate-texture --model-url <url> --reference-image <img> --output out.glb`);
154
+ console.log(` Optimize: playcraft tools retopology --model-url <url> --face-level medium --output optimized.glb`);
155
+ }
156
+ catch (e) {
157
+ handleError(e);
158
+ }
159
+ });
160
+ // ─── generate-texture ─────────────────────────────────────────────────────
161
+ tools.command('generate-texture')
162
+ .description('Generate PBR textures for an existing 3D model (SubmitTextureTo3DJob).')
163
+ .requiredOption('--model-url <url>', 'Input 3D model URL (GLB or OBJ, ≤60MB)')
164
+ .option('--model-format <fmt>', 'Model format: GLB|OBJ (auto-detected from URL if omitted)')
165
+ .option('--reference-image <path>', 'Texture reference image (local path or URL)')
166
+ .option('--prompt <text>', 'Text description for texture generation (alternative to reference image)')
167
+ .option('--model-version <ver>', 'HunyuanTo3D model version: 3.0|3.1 (default: 3.0)', '3.0')
168
+ .requiredOption('--output <path>', 'Output model file path (.glb)')
169
+ .action(async (opts) => {
170
+ try {
171
+ if (!opts.referenceImage && !opts.prompt) {
172
+ console.error('Error: --reference-image or --prompt is required');
173
+ process.exit(1);
174
+ }
175
+ const client = new AgentApiClient();
176
+ const payload = {
177
+ modelUrl: opts.modelUrl,
178
+ model: opts.modelVersion,
179
+ };
180
+ if (opts.modelFormat) {
181
+ payload.modelFormat = opts.modelFormat.toUpperCase();
182
+ }
183
+ if (opts.prompt)
184
+ payload.prompt = opts.prompt;
185
+ if (opts.referenceImage) {
186
+ const isUrl = opts.referenceImage.startsWith('http://') || opts.referenceImage.startsWith('https://');
187
+ if (isUrl) {
188
+ payload.referenceImageUrl = opts.referenceImage;
189
+ }
190
+ else {
191
+ payload.referenceImageBase64 = readFileSync(opts.referenceImage).toString('base64');
192
+ }
193
+ }
194
+ console.log('Generating textures for 3D model...');
195
+ console.log(` Input model: ${opts.modelUrl}`);
196
+ if (opts.referenceImage)
197
+ console.log(` Reference image: ${opts.referenceImage}`);
198
+ if (opts.prompt)
199
+ console.log(` Prompt: ${opts.prompt}`);
200
+ console.log(' (This may take 1–5 minutes)');
201
+ const t0 = Date.now();
202
+ const result = await client.post('/generate-texture', payload);
203
+ const elapsed = ((Date.now() - t0) / 1000).toFixed(1);
204
+ if (!result.modelBase64) {
205
+ console.error('Error: No model data in response');
206
+ process.exit(1);
207
+ }
208
+ const buf = Buffer.from(result.modelBase64, 'base64');
209
+ const outDir = dirname(opts.output);
210
+ mkdirSync(outDir, { recursive: true });
211
+ writeFileSync(opts.output, buf);
212
+ const sizeMB = (buf.length / 1024 / 1024).toFixed(2);
213
+ console.log(`\nTextured model saved: ${opts.output} (${sizeMB}MB, ${elapsed}s)`);
214
+ if (result.resultUrl)
215
+ console.log(` Source URL: ${result.resultUrl}`);
216
+ }
217
+ catch (e) {
218
+ handleError(e);
219
+ }
220
+ });
221
+ // ─── retopology ───────────────────────────────────────────────────────────
222
+ tools.command('retopology')
223
+ .description('Reduce polygon count with smart retopology (SubmitReduceFaceJob).')
224
+ .requiredOption('--model-url <url>', 'Input 3D model URL (GLB or OBJ, ≤60MB)')
225
+ .option('--model-format <fmt>', 'Model format: GLB|OBJ (auto-detected from URL if omitted)')
226
+ .option('--face-level <level>', 'Target polygon level: high|medium|low (default: medium)', 'medium')
227
+ .option('--polygon-type <type>', 'Mesh type: triangle|quadrilateral (default: triangle)', 'triangle')
228
+ .requiredOption('--output <path>', 'Output model file path (.glb)')
229
+ .action(async (opts) => {
230
+ try {
231
+ const client = new AgentApiClient();
232
+ const payload = {
233
+ modelUrl: opts.modelUrl,
234
+ faceLevel: opts.faceLevel,
235
+ polygonType: opts.polygonType,
236
+ };
237
+ if (opts.modelFormat) {
238
+ payload.modelFormat = opts.modelFormat.toUpperCase();
239
+ }
240
+ console.log('Running smart retopology...');
241
+ console.log(` Input model: ${opts.modelUrl}`);
242
+ console.log(` Face level: ${opts.faceLevel}`);
243
+ console.log(` Polygon type: ${opts.polygonType}`);
244
+ console.log(' (This may take 1–3 minutes)');
245
+ const t0 = Date.now();
246
+ const result = await client.post('/retopology', payload);
247
+ const elapsed = ((Date.now() - t0) / 1000).toFixed(1);
248
+ if (!result.modelBase64) {
249
+ console.error('Error: No model data in response');
250
+ process.exit(1);
251
+ }
252
+ const buf = Buffer.from(result.modelBase64, 'base64');
253
+ const outDir = dirname(opts.output);
254
+ mkdirSync(outDir, { recursive: true });
255
+ writeFileSync(opts.output, buf);
256
+ const sizeMB = (buf.length / 1024 / 1024).toFixed(2);
257
+ console.log(`\nRetopologized model saved: ${opts.output} (${sizeMB}MB, ${elapsed}s)`);
258
+ }
259
+ catch (e) {
260
+ handleError(e);
261
+ }
262
+ });
263
+ // ─── uv-unwrap ────────────────────────────────────────────────────────────
264
+ tools.command('uv-unwrap')
265
+ .description('Auto UV unwrap a 3D model (SubmitHunyuanTo3DUVJob).')
266
+ .requiredOption('--model-url <url>', 'Input 3D model URL (FBX / OBJ / GLB, ≤60MB)')
267
+ .option('--model-format <fmt>', 'Model format: FBX|OBJ|GLB (auto-detected from URL if omitted)')
268
+ .requiredOption('--output <path>', 'Output model file path')
269
+ .action(async (opts) => {
270
+ try {
271
+ const client = new AgentApiClient();
272
+ const payload = {
273
+ modelUrl: opts.modelUrl,
274
+ };
275
+ if (opts.modelFormat) {
276
+ payload.modelFormat = opts.modelFormat.toUpperCase();
277
+ }
278
+ console.log('Running UV unwrap...');
279
+ console.log(` Input model: ${opts.modelUrl}`);
280
+ console.log(' (This may take 1–3 minutes)');
281
+ const t0 = Date.now();
282
+ const result = await client.post('/uv-unwrap', payload);
283
+ const elapsed = ((Date.now() - t0) / 1000).toFixed(1);
284
+ if (!result.modelBase64) {
285
+ console.error('Error: No model data in response');
286
+ process.exit(1);
287
+ }
288
+ const buf = Buffer.from(result.modelBase64, 'base64');
289
+ const outDir = dirname(opts.output);
290
+ mkdirSync(outDir, { recursive: true });
291
+ writeFileSync(opts.output, buf);
292
+ const sizeMB = (buf.length / 1024 / 1024).toFixed(2);
293
+ console.log(`\nUV-unwrapped model saved: ${opts.output} (${sizeMB}MB, ${elapsed}s)`);
294
+ }
295
+ catch (e) {
296
+ handleError(e);
297
+ }
298
+ });
299
+ // ─── convert-3d ───────────────────────────────────────────────────────────
300
+ tools.command('convert-3d')
301
+ .description('Convert 3D model format synchronously (Convert3DFormat).')
302
+ .requiredOption('--model-url <url>', 'Input 3D model URL (GLB / OBJ / FBX, ≤60MB)')
303
+ .requiredOption('--format <fmt>', 'Target format: STL|USDZ|FBX|MP4|GIF')
304
+ .requiredOption('--output <path>', 'Output file path')
305
+ .action(async (opts) => {
306
+ try {
307
+ const client = new AgentApiClient();
308
+ const payload = {
309
+ modelUrl: opts.modelUrl,
310
+ format: opts.format.toUpperCase(),
311
+ };
312
+ console.log(`Converting 3D model to ${opts.format.toUpperCase()}...`);
313
+ console.log(` Input: ${opts.modelUrl}`);
314
+ const t0 = Date.now();
315
+ const result = await client.post('/convert-3d', payload);
316
+ const elapsed = ((Date.now() - t0) / 1000).toFixed(1);
317
+ console.log(` Downloading result from: ${result.resultUrl}`);
318
+ const dlRes = await fetch(result.resultUrl);
319
+ if (!dlRes.ok) {
320
+ console.error(`Error: Failed to download result file (HTTP ${dlRes.status})`);
321
+ process.exit(1);
322
+ }
323
+ const buf = Buffer.from(await dlRes.arrayBuffer());
324
+ const outDir = dirname(opts.output);
325
+ mkdirSync(outDir, { recursive: true });
326
+ writeFileSync(opts.output, buf);
327
+ const sizeMB = (buf.length / 1024 / 1024).toFixed(2);
328
+ console.log(`\nConverted model saved: ${opts.output} (${sizeMB}MB, ${elapsed}s)`);
329
+ console.log(` Format: ${result.format.toUpperCase()}`);
330
+ }
331
+ catch (e) {
332
+ handleError(e);
333
+ }
334
+ });
335
+ // ─── generate-3d-parts ────────────────────────────────────────────────────
336
+ tools.command('generate-3d-parts')
337
+ .description('Auto-generate 3D components from a model (SubmitHunyuan3DPartJob). FBX only.')
338
+ .requiredOption('--model-url <url>', 'Input FBX model URL (≤60MB)')
339
+ .option('--model-version <ver>', 'Model version (default: 1.5)', '1.5')
340
+ .requiredOption('--output <path>', 'Output model file path (.fbx)')
341
+ .action(async (opts) => {
342
+ try {
343
+ const client = new AgentApiClient();
344
+ const payload = {
345
+ modelUrl: opts.modelUrl,
346
+ model: opts.modelVersion,
347
+ };
348
+ console.log('Generating 3D parts...');
349
+ console.log(` Input model: ${opts.modelUrl}`);
350
+ console.log(' (This may take 1–5 minutes)');
351
+ const t0 = Date.now();
352
+ const result = await client.post('/generate-3d-parts', payload);
353
+ const elapsed = ((Date.now() - t0) / 1000).toFixed(1);
354
+ if (!result.modelBase64) {
355
+ console.error('Error: No model data in response');
356
+ process.exit(1);
357
+ }
358
+ const buf = Buffer.from(result.modelBase64, 'base64');
359
+ const outDir = dirname(opts.output);
360
+ mkdirSync(outDir, { recursive: true });
361
+ writeFileSync(opts.output, buf);
362
+ const sizeMB = (buf.length / 1024 / 1024).toFixed(2);
363
+ console.log(`\n3D parts model saved: ${opts.output} (${sizeMB}MB, ${elapsed}s)`);
364
+ }
365
+ catch (e) {
366
+ handleError(e);
367
+ }
368
+ });
369
+ // ─── generate-motion ──────────────────────────────────────────────────────
370
+ tools.command('generate-motion')
371
+ .description('Generate 3D character motion from text (SubmitHunyuanTo3DMotionJob). Outputs animated FBX.')
372
+ .requiredOption('--prompt <text>', 'Text description of the motion (max 128 chars)')
373
+ .option('--duration <n>', 'Animation duration in seconds (1–12, default: 5)', parseInt)
374
+ .option('--retarget-model-url <url>', 'Apply motion to this FBX model URL (must be from HunyuanTo3D)')
375
+ .option('--no-mesh', 'Exclude skinned mesh from output FBX')
376
+ .requiredOption('--output <path>', 'Output FBX file path')
377
+ .action(async (opts) => {
378
+ try {
379
+ const client = new AgentApiClient();
380
+ const payload = {
381
+ prompt: opts.prompt,
382
+ duration: opts.duration,
383
+ enableMesh: opts.mesh !== false,
384
+ };
385
+ if (opts.retargetModelUrl)
386
+ payload.retargetModelUrl = opts.retargetModelUrl;
387
+ console.log('Generating 3D motion...');
388
+ console.log(` Prompt: ${opts.prompt}`);
389
+ if (opts.duration)
390
+ console.log(` Duration: ${opts.duration}s`);
391
+ if (opts.retargetModelUrl)
392
+ console.log(` Retarget model: ${opts.retargetModelUrl}`);
393
+ console.log(' (This may take 1–5 minutes)');
394
+ const t0 = Date.now();
395
+ const result = await client.post('/generate-motion', payload);
396
+ const elapsed = ((Date.now() - t0) / 1000).toFixed(1);
397
+ if (!result.modelBase64) {
398
+ console.error('Error: No model data in response');
399
+ process.exit(1);
400
+ }
401
+ const buf = Buffer.from(result.modelBase64, 'base64');
402
+ const outDir = dirname(opts.output);
403
+ mkdirSync(outDir, { recursive: true });
404
+ writeFileSync(opts.output, buf);
405
+ const sizeMB = (buf.length / 1024 / 1024).toFixed(2);
406
+ console.log(`\nMotion FBX saved: ${opts.output} (${sizeMB}MB, ${elapsed}s)`);
407
+ }
408
+ catch (e) {
409
+ handleError(e);
410
+ }
411
+ });
412
+ // ─── generate-avatar ──────────────────────────────────────────────────────
413
+ tools.command('generate-avatar')
414
+ .description('Generate a 3D character from a portrait photo (SubmitProfileTo3DJob).')
415
+ .option('--profile-image <path>', 'Portrait photo (local path or URL)')
416
+ .option('--template <name>', `Character template: ${AVATAR_TEMPLATES.slice(0, 5).join('|')}... (run with --list-templates to see all)`)
417
+ .option('--list-templates', 'Print all available avatar templates and exit')
418
+ .requiredOption('--output <path>', 'Output model file path')
419
+ .action(async (opts) => {
420
+ try {
421
+ if (opts.listTemplates) {
422
+ console.log('Available avatar templates:');
423
+ const descriptions = {
424
+ basketball: '动感球手', badminton: '羽扬中华', pingpong: '国球荣耀',
425
+ gymnastics: '勇攀巅峰', pilidance: '舞动青春', tennis: '网球甜心',
426
+ athletics: '东方疾风', footballboykicking1: '激情逐风', footballboykicking2: '绿茵之星',
427
+ guitar: '甜酷弦音', footballboy: '足球小将', skateboard: '滑跃青春',
428
+ futuresoilder: '未来战士', explorer: '逐梦旷野', beardollgirl: '可爱女孩',
429
+ bibpantsboy: '都市白领', womansitpose: '职业丽影', womanstandpose2: '悠闲时光',
430
+ mysteriousprincess: '海洋公主', manstandpose2: '演讲之星',
431
+ };
432
+ AVATAR_TEMPLATES.forEach((t) => console.log(` ${t.padEnd(24)} ${descriptions[t] ?? ''}`));
433
+ process.exit(0);
434
+ }
435
+ if (!opts.profileImage) {
436
+ console.error('Error: --profile-image is required');
437
+ process.exit(1);
438
+ }
439
+ const client = new AgentApiClient();
440
+ const payload = {};
441
+ if (opts.template)
442
+ payload.template = opts.template;
443
+ const isUrl = opts.profileImage.startsWith('http://') || opts.profileImage.startsWith('https://');
444
+ if (isUrl) {
445
+ payload.profileImageUrl = opts.profileImage;
446
+ }
447
+ else {
448
+ payload.profileImageBase64 = readFileSync(opts.profileImage).toString('base64');
449
+ }
450
+ console.log('Generating 3D avatar...');
451
+ console.log(` Profile: ${opts.profileImage}`);
452
+ if (opts.template)
453
+ console.log(` Template: ${opts.template}`);
454
+ console.log(' (This may take 1–5 minutes)');
455
+ const t0 = Date.now();
456
+ const result = await client.post('/generate-avatar', payload);
457
+ const elapsed = ((Date.now() - t0) / 1000).toFixed(1);
458
+ if (!result.modelBase64) {
459
+ console.error('Error: No model data in response');
460
+ process.exit(1);
461
+ }
462
+ const buf = Buffer.from(result.modelBase64, 'base64');
463
+ const outDir = dirname(opts.output);
464
+ mkdirSync(outDir, { recursive: true });
465
+ writeFileSync(opts.output, buf);
466
+ const sizeMB = (buf.length / 1024 / 1024).toFixed(2);
467
+ console.log(`\n3D avatar saved: ${opts.output} (${sizeMB}MB, ${elapsed}s)`);
468
+ }
469
+ catch (e) {
470
+ handleError(e);
471
+ }
472
+ });
473
+ }