tripo-cli 0.7.3 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +29 -0
- package/README.md +9 -7
- package/dist/ai/experts.js +25 -19
- package/dist/ai/plan-card.js +71 -33
- package/dist/ai/wizard.js +13 -17
- package/dist/commands/account.js +2 -6
- package/dist/commands/ai.js +18 -16
- package/dist/commands/batch.js +58 -18
- package/dist/commands/config-cmd.js +1 -1
- package/dist/commands/doctor.js +1 -3
- package/dist/commands/generate.js +35 -4
- package/dist/commands/home.js +12 -13
- package/dist/commands/login.js +72 -187
- package/dist/commands/make.js +47 -25
- package/dist/commands/mcp.js +2 -1
- package/dist/commands/process-groups.js +3 -4
- package/dist/commands/shared.js +9 -14
- package/dist/commands/task.js +10 -11
- package/dist/commands/topup.js +6 -10
- package/dist/commands/update.js +6 -10
- package/dist/commands/view.js +3 -4
- package/dist/config/config.js +1 -1
- package/dist/core/client.js +4 -13
- package/dist/core/errors.js +16 -11
- package/dist/core/output.js +0 -4
- package/dist/core/requests.js +61 -17
- package/dist/core/task-service.js +19 -20
- package/dist/knowledge/chains.js +12 -12
- package/dist/knowledge/error-catalog.js +32 -32
- package/dist/knowledge/models.js +119 -20
- package/dist/knowledge/params.js +96 -8
- package/dist/knowledge/scenarios.js +32 -55
- package/dist/pipeline/make.js +36 -28
- package/dist/pipeline/runner.js +7 -12
- package/dist/pipeline/stdin.js +40 -3
- package/dist/program.js +3 -10
- package/package.json +3 -2
- package/skill/SKILL.md +34 -8
- package/skill/commands/account.md +1 -1
- package/skill/commands/batch.md +13 -3
- package/skill/commands/generate.md +35 -10
- package/skill/commands/make.md +39 -13
- package/skill/commands/process.md +2 -1
- package/skill/commands/task.md +2 -2
- package/skill/commands/view.md +1 -1
- package/skill/common-errors.md +6 -3
- package/skill/examples/animation.md +3 -1
- package/skill/examples/ar-web.md +6 -4
- package/skill/examples/game-asset.md +17 -6
- package/skill/examples/pipes.md +2 -2
- package/skill/examples/print.md +7 -3
- package/dist/i18n.js +0 -19
package/dist/knowledge/params.js
CHANGED
|
@@ -5,14 +5,96 @@
|
|
|
5
5
|
* Every rule here prevents a guaranteed 400 or a silently wasted credit.
|
|
6
6
|
* Rules marked [hard] mirror server enforcement; [soft] are CLI smart defaults.
|
|
7
7
|
*/
|
|
8
|
-
import { CONVERT_FORMATS, DECIMATE_MODELS, EXPORT_ORIENTATIONS, FBX_PRESETS, MODEL_P2, P1_FACE_LIMIT, P1_FORBIDDEN_PARAMS, P2_FACE_LIMIT, P2_FORBIDDEN_PARAMS, TEXTURE_FORMATS, TEXTURE_MODEL_V35, TEXTURE_QUALITIES, isP1, isP2, normalizeModel, } from './models.js';
|
|
8
|
+
import { CAPABILITY_DEFAULTS, IMAGE_TO_IMAGE_MODELS, TEXT_TO_IMAGE_MODELS, CONVERT_FORMATS, DECIMATE_MODELS, EXPORT_ORIENTATIONS, FBX_PRESETS, LEGACY_3D_MODELS, MODEL_P1, MODEL_P2, MODEL_V31, P1_FACE_LIMIT, P1_FORBIDDEN_PARAMS, P2_FACE_LIMIT, P2_FORBIDDEN_PARAMS, TEXTURE_FORMATS, TEXTURE_MODEL_V35, TEXTURE_QUALITIES, isP1, isP2, normalizeModel, } from './models.js';
|
|
9
9
|
const PROMPT_MAX = 1024;
|
|
10
10
|
const NEGATIVE_PROMPT_MAX = 255;
|
|
11
|
+
/** Image endpoint contracts; never silently substitute a paid model. */
|
|
12
|
+
export function validateImageParams(input, editing) {
|
|
13
|
+
const params = { ...input };
|
|
14
|
+
const warnings = [];
|
|
15
|
+
const errors = [];
|
|
16
|
+
const model = params.model === undefined
|
|
17
|
+
? (editing ? CAPABILITY_DEFAULTS.imageToImage : CAPABILITY_DEFAULTS.textToImage)
|
|
18
|
+
: params.model;
|
|
19
|
+
params.model = model;
|
|
20
|
+
const models = editing ? IMAGE_TO_IMAGE_MODELS : TEXT_TO_IMAGE_MODELS;
|
|
21
|
+
if (model === 'chat_image_1') {
|
|
22
|
+
errors.push('model chat_image_1 retired on 2026-10-23; explicitly choose chat_image_2 or chat_image_2.5_flare/sunburst (no automatic resubmission)');
|
|
23
|
+
}
|
|
24
|
+
else if (!models.includes(model)) {
|
|
25
|
+
errors.push(`unsupported image model "${String(model)}" for ${editing ? 'image-to-image' : 'text-to-image'}`);
|
|
26
|
+
}
|
|
27
|
+
const chat25 = model === 'chat_image_2.5_flare' || model === 'chat_image_2.5_sunburst';
|
|
28
|
+
if (params.size !== undefined && typeof model === 'string' && !validImageSize(model, params.size)) {
|
|
29
|
+
errors.push(`size is unsupported for model ${model}`);
|
|
30
|
+
}
|
|
31
|
+
if ((model === 'seedream_v4' || model === 'seedream_v5') && typeof params.size === 'string' && params.size.includes('x')) {
|
|
32
|
+
warnings.push('Seedream exact pixel-count limits unverified: official MP ranges are rounded; server validation still applies');
|
|
33
|
+
}
|
|
34
|
+
if (params.quality !== undefined) {
|
|
35
|
+
const qualities = chat25 ? ['low', 'medium', 'high', 'xhigh', 'max']
|
|
36
|
+
: model === 'chat_image_2' ? ['low', 'medium', 'high'] : [];
|
|
37
|
+
if (!qualities.includes(params.quality))
|
|
38
|
+
errors.push(`quality is unsupported for model ${String(model)} (auto is not supported)`);
|
|
39
|
+
}
|
|
40
|
+
if (params.output_format !== undefined && !['png', 'jpeg'].includes(params.output_format)) {
|
|
41
|
+
errors.push('output_format must be png or jpeg');
|
|
42
|
+
}
|
|
43
|
+
if (params.background !== undefined) {
|
|
44
|
+
if (!chat25) {
|
|
45
|
+
delete params.background;
|
|
46
|
+
warnings.push('background is ignored outside chat_image_2.5 models (removed)');
|
|
47
|
+
}
|
|
48
|
+
else if (!['auto', 'opaque', 'transparent'].includes(params.background)) {
|
|
49
|
+
errors.push('background must be auto, opaque or transparent');
|
|
50
|
+
}
|
|
51
|
+
else if (params.background === 'transparent' && params.output_format !== undefined && params.output_format !== 'png') {
|
|
52
|
+
errors.push('background=transparent requires output_format=png');
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return { params, warnings, errors };
|
|
56
|
+
}
|
|
57
|
+
/** Only documented size rules; rounded Seedream MP limits are not exact pixel thresholds. */
|
|
58
|
+
function validImageSize(model, size) {
|
|
59
|
+
if (typeof size !== 'string')
|
|
60
|
+
return false;
|
|
61
|
+
const match = /^([1-9]\d*)x([1-9]\d*)$/.exec(size);
|
|
62
|
+
const w = Number(match?.[1]);
|
|
63
|
+
const h = Number(match?.[2]);
|
|
64
|
+
const pixels = !!match && Number.isSafeInteger(w) && Number.isSafeInteger(h);
|
|
65
|
+
if (model === 'banana') {
|
|
66
|
+
return ['1024x1024', '832x1248', '1248x832', '864x1184', '1184x864', '896x1152', '1152x896', '768x1344', '1344x768', '1536x672'].includes(size);
|
|
67
|
+
}
|
|
68
|
+
if (model === 'banana2')
|
|
69
|
+
return ['0.5K', '1K', '2K', '4K'].includes(size);
|
|
70
|
+
if (model === 'banana_pro')
|
|
71
|
+
return ['1K', '2K', '4K'].includes(size) || pixels;
|
|
72
|
+
if (model === 'seedream_v4' || model === 'seedream_v5') {
|
|
73
|
+
return ['2K', model === 'seedream_v5' ? '3K' : '4K'].includes(size) || (pixels && w / h >= 1 / 16 && w / h <= 16);
|
|
74
|
+
}
|
|
75
|
+
if (model === 'chat_image_1.5')
|
|
76
|
+
return ['1024x1024', '1024x1536', '1536x1024', 'auto'].includes(size);
|
|
77
|
+
if (model === 'chat_image_2' || model.startsWith('chat_image_2.5_')) {
|
|
78
|
+
return size === 'auto' || (pixels && w <= 3840 && h <= 3840 && w % 16 === 0 && h % 16 === 0 &&
|
|
79
|
+
Math.max(w, h) <= 3 * Math.min(w, h) && w * h >= 655360 && w * h <= 8294400);
|
|
80
|
+
}
|
|
81
|
+
return true; // Unknown models are rejected separately.
|
|
82
|
+
}
|
|
83
|
+
function validateFaceLimit(params, errors) {
|
|
84
|
+
if (params.face_limit !== undefined &&
|
|
85
|
+
(typeof params.face_limit !== 'number' || !Number.isFinite(params.face_limit) || !Number.isInteger(params.face_limit))) {
|
|
86
|
+
errors.push('face_limit must be a finite integer');
|
|
87
|
+
}
|
|
88
|
+
}
|
|
11
89
|
/** Validate + auto-fix parameters for 3D generation (text/image/multiview-to-model). */
|
|
12
90
|
export function validateGenerationParams(model, input) {
|
|
13
91
|
const params = { ...input };
|
|
14
92
|
const warnings = [];
|
|
15
93
|
const errors = [];
|
|
94
|
+
if (![MODEL_V31, MODEL_P1, MODEL_P2, ...LEGACY_3D_MODELS].includes(normalizeModel(model))) {
|
|
95
|
+
errors.push(`unsupported 3D model "${model}"`);
|
|
96
|
+
}
|
|
97
|
+
validateFaceLimit(params, errors);
|
|
16
98
|
if (typeof params.prompt === 'string' && params.prompt.length > PROMPT_MAX) {
|
|
17
99
|
errors.push(`prompt exceeds ${PROMPT_MAX} characters (${params.prompt.length})`);
|
|
18
100
|
}
|
|
@@ -44,6 +126,11 @@ export function validateGenerationParams(model, input) {
|
|
|
44
126
|
params.texture = true;
|
|
45
127
|
warnings.push('pbr=true forces texture=true (adjusted)');
|
|
46
128
|
}
|
|
129
|
+
// [hard] pbr defaults to true server-side, so texture=false alone would still be textured.
|
|
130
|
+
if (params.texture === false && params.pbr === undefined) {
|
|
131
|
+
params.pbr = false;
|
|
132
|
+
warnings.push('texture=false implies pbr=false (adjusted)');
|
|
133
|
+
}
|
|
47
134
|
// [hard] generate_parts outputs bare geometry: texture/pbr/quad must be off
|
|
48
135
|
// (texture and pbr default to true server-side, so omitting them counts as on).
|
|
49
136
|
if (params.generate_parts === true) {
|
|
@@ -60,7 +147,7 @@ export function validateGenerationParams(model, input) {
|
|
|
60
147
|
if (typeof face === 'number') {
|
|
61
148
|
if (face < P1_FACE_LIMIT.min || face > P1_FACE_LIMIT.max) {
|
|
62
149
|
errors.push(`P1 face_limit must be within ${P1_FACE_LIMIT.min}–${P1_FACE_LIMIT.max} (got ${face}); ` +
|
|
63
|
-
`use tripo-v3.1 for
|
|
150
|
+
`use tripo-v3.1 for budgets outside this range`);
|
|
64
151
|
}
|
|
65
152
|
}
|
|
66
153
|
}
|
|
@@ -72,7 +159,7 @@ export function validateGenerationParams(model, input) {
|
|
|
72
159
|
const max = quad ? P2_FACE_LIMIT.quadMax : P2_FACE_LIMIT.triMax;
|
|
73
160
|
if (face < P2_FACE_LIMIT.min || face > max) {
|
|
74
161
|
errors.push(`P2 face_limit must be within ${P2_FACE_LIMIT.min}–${max}${quad ? ' with quad=true' : ''} (got ${face}); ` +
|
|
75
|
-
`use tripo-v3.1 for
|
|
162
|
+
`use tripo-v3.1 for budgets outside this range`);
|
|
76
163
|
}
|
|
77
164
|
}
|
|
78
165
|
}
|
|
@@ -85,10 +172,6 @@ export function validateGenerationParams(model, input) {
|
|
|
85
172
|
}
|
|
86
173
|
}
|
|
87
174
|
}
|
|
88
|
-
if (params.texture_quality !== undefined &&
|
|
89
|
-
!TEXTURE_QUALITIES.includes(params.texture_quality)) {
|
|
90
|
-
errors.push(`texture_quality must be one of ${TEXTURE_QUALITIES.join('/')}`);
|
|
91
|
-
}
|
|
92
175
|
applyTextureV35(params, 'texture_version', warnings, errors);
|
|
93
176
|
// [hard] quad topology cannot be stored in GLB → output will be FBX.
|
|
94
177
|
if (params.quad === true) {
|
|
@@ -103,6 +186,9 @@ export function validateGenerationParams(model, input) {
|
|
|
103
186
|
* generation endpoints and `model` on /v3/models/texture.
|
|
104
187
|
*/
|
|
105
188
|
export function applyTextureV35(params, key, warnings, errors) {
|
|
189
|
+
if (params.texture_quality !== undefined && !TEXTURE_QUALITIES.includes(params.texture_quality)) {
|
|
190
|
+
errors.push(`texture_quality must be one of ${TEXTURE_QUALITIES.join('/')}`);
|
|
191
|
+
}
|
|
106
192
|
const feature = params.texture_quality === 'fast' ? 'texture_quality=fast' : params.delight !== undefined ? 'delight' : undefined;
|
|
107
193
|
if (!feature)
|
|
108
194
|
return;
|
|
@@ -123,6 +209,7 @@ export function validateConvertParams(input) {
|
|
|
123
209
|
const params = { ...input };
|
|
124
210
|
const warnings = [];
|
|
125
211
|
const errors = [];
|
|
212
|
+
validateFaceLimit(params, errors);
|
|
126
213
|
const format = typeof params.format === 'string' ? params.format.toUpperCase() : undefined;
|
|
127
214
|
if (!format) {
|
|
128
215
|
errors.push(`convert requires format (${CONVERT_FORMATS.join('/')})`);
|
|
@@ -199,6 +286,7 @@ export function validateDecimateParams(input) {
|
|
|
199
286
|
const params = { ...input };
|
|
200
287
|
const warnings = [];
|
|
201
288
|
const errors = [];
|
|
289
|
+
validateFaceLimit(params, errors);
|
|
202
290
|
const model = typeof params.model === 'string' ? params.model.toLowerCase() : undefined;
|
|
203
291
|
if (model !== undefined && model !== 'default' && !DECIMATE_MODELS.includes(model)) {
|
|
204
292
|
errors.push(`decimate model must be ${DECIMATE_MODELS.join('/')} (got "${String(params.model)}")`);
|
|
@@ -207,7 +295,7 @@ export function validateDecimateParams(input) {
|
|
|
207
295
|
if (params.smart_uv === true) {
|
|
208
296
|
if (model === undefined) {
|
|
209
297
|
params.model = 'v3.0';
|
|
210
|
-
warnings.push('smart_uv requires decimate v3.0: using model=v3.0 (
|
|
298
|
+
warnings.push('smart_uv requires decimate v3.0: using model=v3.0 (cost not estimated; check current regional pricing)');
|
|
211
299
|
}
|
|
212
300
|
else if (model !== 'v3.0') {
|
|
213
301
|
errors.push(`smart_uv only works with decimate model v3.0 (got "${String(params.model)}")`);
|
|
@@ -2,105 +2,82 @@
|
|
|
2
2
|
* Scenario decision matrix: --for presets and the AI wizard both read this
|
|
3
3
|
* table. Changing production strategy means editing data here, not logic.
|
|
4
4
|
*/
|
|
5
|
-
import { CAPABILITY_DEFAULTS,
|
|
5
|
+
import { CAPABILITY_DEFAULTS, MODEL_P2, MODEL_V31 } from './models.js';
|
|
6
6
|
export const SCENARIOS = [
|
|
7
7
|
{
|
|
8
8
|
id: 'game-mobile',
|
|
9
|
-
label:
|
|
10
|
-
description:
|
|
11
|
-
model:
|
|
9
|
+
label: 'Mobile game / low-poly',
|
|
10
|
+
description: 'Mobile game assets with strict face budgets and clean topology',
|
|
11
|
+
model: MODEL_P2,
|
|
12
12
|
genParams: { texture: true, pbr: true, texture_quality: 'standard', face_limit: 15000 },
|
|
13
13
|
chain: [{ step: 'convert', args: { format: 'FBX', texture_size: 2048 } }],
|
|
14
14
|
deliverables: ['FBX', 'GLB'],
|
|
15
|
-
notes:
|
|
16
|
-
zh: ['P1 原生低模,拓扑干净', '2K 贴图适配移动端', '需要骨骼时追加 --then rig'],
|
|
17
|
-
en: ['P1 native low-poly with clean topology', '2K textures for mobile budgets', 'append --then rig if you need a skeleton'],
|
|
18
|
-
},
|
|
15
|
+
notes: ['P2 editable low-poly topology', '2K textures for mobile budgets', 'P2 costs more credits than H/P1; check credits before running', 'append --then rig if you need a skeleton'],
|
|
19
16
|
},
|
|
20
17
|
{
|
|
21
18
|
id: 'game-pc',
|
|
22
|
-
label:
|
|
23
|
-
description:
|
|
24
|
-
model:
|
|
19
|
+
label: 'PC / console game',
|
|
20
|
+
description: 'Editable real-time engine assets with full PBR',
|
|
21
|
+
model: MODEL_P2,
|
|
25
22
|
genParams: { texture: true, pbr: true, texture_quality: 'detailed' },
|
|
26
23
|
chain: [{ step: 'convert', args: { format: 'FBX' } }],
|
|
27
24
|
deliverables: ['FBX', 'GLTF'],
|
|
28
|
-
notes:
|
|
29
|
-
zh: ['v3.1 高精度 + detailed 贴图', '需要 LOD 时追加 --then decimate:5000', '进 Blender/3dsMax 用 fbx_preset 对齐'],
|
|
30
|
-
en: ['v3.1 high fidelity + detailed textures', 'append --then decimate:5000 for LODs', 'use fbx_preset to match Blender/3dsMax'],
|
|
31
|
-
},
|
|
25
|
+
notes: ['P2 editable topology + detailed textures', 'append --then decimate:5000 for LODs', 'P2 costs more credits than H/P1; check credits before running', 'use fbx_preset to match Blender/3dsMax'],
|
|
32
26
|
},
|
|
33
27
|
{
|
|
34
28
|
id: 'film',
|
|
35
|
-
label:
|
|
36
|
-
description:
|
|
29
|
+
label: 'Film / offline rendering',
|
|
30
|
+
description: 'Subdivision-friendly quads, maximum quality',
|
|
37
31
|
model: MODEL_V31,
|
|
38
32
|
genParams: { texture: true, pbr: true, quad: true, geometry_quality: 'detailed', texture_quality: 'detailed' },
|
|
39
33
|
chain: [],
|
|
40
34
|
deliverables: ['FBX (quad)', 'USDZ/OBJ via convert'],
|
|
41
|
-
notes:
|
|
42
|
-
zh: ['quad=true 输出 FBX(quad 进不了 GLB)', 'geometry detailed 加收 HD credit', '进 USD 管线追加 --then convert:usdz'],
|
|
43
|
-
en: ['quad=true outputs FBX (quads cannot be stored in GLB)', 'geometry detailed bills an extra HD credit', 'append --then convert:usdz for USD pipelines'],
|
|
44
|
-
},
|
|
35
|
+
notes: ['quad=true outputs FBX (quads cannot be stored in GLB)', 'geometry detailed bills an extra HD credit', 'append --then convert:usdz for USD pipelines'],
|
|
45
36
|
},
|
|
46
37
|
{
|
|
47
38
|
id: 'print',
|
|
48
|
-
label:
|
|
49
|
-
description:
|
|
39
|
+
label: '3D printing',
|
|
40
|
+
description: 'Watertight printable geometry, skipping textures saves credits',
|
|
50
41
|
model: MODEL_V31,
|
|
51
42
|
genParams: { texture: false, pbr: false },
|
|
52
43
|
chain: [{ step: 'convert', args: { format: 'STL', flatten_bottom: true } }],
|
|
53
|
-
deliverables: ['STL'
|
|
54
|
-
notes:
|
|
55
|
-
zh: ['白模生成,不花贴图积分', 'flatten_bottom 平底利于 FDM', '彩色打印改 --then convert:3mf,export_vertex_colors=true'],
|
|
56
|
-
en: ['bare geometry: no texture credits spent', 'flatten_bottom helps FDM adhesion', 'for color prints use --then convert:3mf,export_vertex_colors=true'],
|
|
57
|
-
},
|
|
44
|
+
deliverables: ['STL'],
|
|
45
|
+
notes: ['bare geometry: no texture credits spent', 'flatten_bottom helps FDM adhesion', 'for color prints add -p texture=true and --then convert:format=OBJ,export_vertex_colors=true (API 3MF output is single color)'],
|
|
58
46
|
},
|
|
59
47
|
{
|
|
60
48
|
id: 'ar-web',
|
|
61
|
-
label:
|
|
62
|
-
description:
|
|
63
|
-
model:
|
|
64
|
-
genParams: { texture: true, pbr: true },
|
|
65
|
-
chain: [
|
|
66
|
-
|
|
67
|
-
{ step: 'convert', args: { format: 'USDZ' } },
|
|
68
|
-
],
|
|
49
|
+
label: 'AR / Web / e-commerce',
|
|
50
|
+
description: 'Mobile performance budgets with dual glb+usdz delivery',
|
|
51
|
+
model: MODEL_P2,
|
|
52
|
+
genParams: { texture: true, pbr: true, face_limit: 20000 },
|
|
53
|
+
chain: [{ step: 'convert', args: { format: 'USDZ' } }],
|
|
54
|
+
downloadGeneration: true,
|
|
69
55
|
deliverables: ['GLB', 'USDZ'],
|
|
70
|
-
notes:
|
|
71
|
-
zh: ['v3.1 生成后减面到 2 万内', 'iOS QuickLook 需要 USDZ', 'decimate 默认 bake 烘焙贴图到低模'],
|
|
72
|
-
en: ['generate with v3.1, then decimate under 20k faces', 'iOS QuickLook requires USDZ', 'decimate bakes textures onto the low-poly by default'],
|
|
73
|
-
},
|
|
56
|
+
notes: ['P2 generates under 20k faces directly (no decimate step)', 'iOS QuickLook requires USDZ', 'P2 costs more credits than H/P1; check credits before running', 'downloads both the generated GLB and the USDZ'],
|
|
74
57
|
},
|
|
75
58
|
{
|
|
76
59
|
id: 'anim',
|
|
77
|
-
label:
|
|
78
|
-
description:
|
|
79
|
-
model:
|
|
60
|
+
label: 'Character animation',
|
|
61
|
+
description: 'Full rig + retarget pipeline',
|
|
62
|
+
model: MODEL_P2,
|
|
80
63
|
genParams: { texture: true, pbr: true },
|
|
81
64
|
chain: [
|
|
82
65
|
{ step: 'rig-check', args: {} },
|
|
83
|
-
{ step: 'rig', args: { model: CAPABILITY_DEFAULTS.rig,
|
|
66
|
+
{ step: 'rig', args: { model: CAPABILITY_DEFAULTS.rig, out_format: 'glb' } },
|
|
84
67
|
{ step: 'retarget', args: { animations: ['preset:idle', 'preset:walk'] } },
|
|
85
68
|
],
|
|
86
69
|
deliverables: ['GLB', 'FBX'],
|
|
87
|
-
notes:
|
|
88
|
-
zh: ['先 rig-check 预检,不可绑骨即止损', '接 Mixamo 用 rig:spec=mixamo,Unity Humanoid / Unreal 用 spec=unity / spec=unreal', '提示词建议 T-pose 站姿,或先 text-to-image template=t_pose'],
|
|
89
|
-
en: ['rig-check first: stop early if not riggable', 'use rig:spec=mixamo for Mixamo, spec=unity / spec=unreal for Unity Humanoid / Unreal', 'prompt for a T-pose, or pre-generate with text-to-image template=t_pose'],
|
|
90
|
-
},
|
|
70
|
+
notes: ['P2 costs more credits than H/P1; check credits before running', 'rig-check first: stop early if not riggable', 'use rig:spec=mixamo for Mixamo, spec=unity / spec=unreal for Unity Humanoid / Unreal', 'prompt for a T-pose, or pre-generate with text-to-image template=t_pose'],
|
|
91
71
|
},
|
|
92
72
|
{
|
|
93
73
|
id: 'toy',
|
|
94
|
-
label:
|
|
95
|
-
description:
|
|
96
|
-
model:
|
|
74
|
+
label: 'Stylized toy / desk piece',
|
|
75
|
+
description: 'Cute low-poly desk piece',
|
|
76
|
+
model: MODEL_P2,
|
|
97
77
|
genParams: { texture: true, pbr: true, face_limit: 20000 },
|
|
98
78
|
chain: [],
|
|
99
79
|
deliverables: ['GLB', 'STL via convert'],
|
|
100
|
-
notes:
|
|
101
|
-
zh: ['P1 出干净低模', '要打印实物追加 --then convert:stl'],
|
|
102
|
-
en: ['P1 produces clean low-poly', 'append --then convert:stl to print it'],
|
|
103
|
-
},
|
|
80
|
+
notes: ['P2 produces editable low-poly', 'P2 costs more credits than H/P1; check credits before running', 'append --then convert:stl to print it'],
|
|
104
81
|
},
|
|
105
82
|
];
|
|
106
83
|
const byId = new Map(SCENARIOS.map((s) => [s.id, s]));
|
package/dist/pipeline/make.js
CHANGED
|
@@ -1,22 +1,38 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The `make` orchestrator — one entry point that turns any input (text /
|
|
3
3
|
* image / multiview / model file / task ref) into finished artifacts,
|
|
4
|
-
* applying
|
|
4
|
+
* applying model routing, scenario presets, and the processing chain.
|
|
5
5
|
* Shared by `tripo make`, `tripo ai`, `tripo redo`, batch, and the MCP server.
|
|
6
6
|
*/
|
|
7
7
|
import { basename } from 'node:path';
|
|
8
8
|
import { UsageError } from '../core/errors.js';
|
|
9
9
|
import { log, warn } from '../core/output.js';
|
|
10
|
-
import { buildImageToModel, buildMultiviewToModel, buildStepRequest, buildTextToModel, } from '../core/requests.js';
|
|
10
|
+
import { buildImageToModel, buildMultiviewToModel, buildStepRequest, buildTextToModel, validateChainRequests, } from '../core/requests.js';
|
|
11
11
|
import { precheckBalance, resolveInputValue, runTask } from '../core/task-service.js';
|
|
12
|
-
import { t } from '../i18n.js';
|
|
13
12
|
import { validateChain, ChainError } from '../knowledge/chains.js';
|
|
14
|
-
import { chooseModel, detectLowPolyIntent } from '../knowledge/models.js';
|
|
13
|
+
import { chooseModel, detectDenseGeometryIntent, detectLowPolyIntent, detectRealtimeIntent } from '../knowledge/models.js';
|
|
15
14
|
import { getScenario } from '../knowledge/scenarios.js';
|
|
16
15
|
import { classifyInputs } from '../util/input-detect.js';
|
|
17
16
|
import { runChain, assertSourceReady } from './runner.js';
|
|
18
|
-
|
|
17
|
+
function chooseGenerationModel(options, scenario, genParams, prompt) {
|
|
18
|
+
return chooseModel({
|
|
19
|
+
explicit: options.model,
|
|
20
|
+
preferred: scenario?.model,
|
|
21
|
+
faceLimit: typeof genParams.face_limit === 'number' ? genParams.face_limit : undefined,
|
|
22
|
+
quad: genParams.quad === true,
|
|
23
|
+
lowPoly: prompt ? detectLowPolyIntent(prompt) : false,
|
|
24
|
+
realtime: prompt ? detectRealtimeIntent(prompt) : false,
|
|
25
|
+
denseGeometry: prompt ? detectDenseGeometryIntent(prompt) : false,
|
|
26
|
+
params: genParams,
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
/** Validate the entire plan without side effects before resolving uploads. */
|
|
19
30
|
export async function planMake(client, options) {
|
|
31
|
+
const preview = await prepareMake(client, { ...options, dryRun: true });
|
|
32
|
+
checkPlanChain(preview);
|
|
33
|
+
return options.dryRun ? preview : prepareMake(client, options);
|
|
34
|
+
}
|
|
35
|
+
async function prepareMake(client, options) {
|
|
20
36
|
const classified = classifyInputs(options.inputs);
|
|
21
37
|
const scenario = options.scenario ? getScenario(options.scenario) : undefined;
|
|
22
38
|
if (options.scenario && !scenario) {
|
|
@@ -25,25 +41,21 @@ export async function planMake(client, options) {
|
|
|
25
41
|
const userParams = options.params ?? {};
|
|
26
42
|
const genParams = { ...(scenario?.genParams ?? {}), ...userParams };
|
|
27
43
|
const chain = options.chain && options.chain.length > 0 ? options.chain : (scenario?.chain ?? []);
|
|
44
|
+
// --then replaces the scenario chain, so its dual-delivery promise no longer holds.
|
|
45
|
+
const downloadGeneration = scenario?.downloadGeneration === true && !(options.chain && options.chain.length > 0);
|
|
28
46
|
const warnings = [];
|
|
29
47
|
if (options.chain && options.chain.length > 0 && scenario && scenario.chain.length > 0) {
|
|
30
|
-
warnings.push(
|
|
31
|
-
zh: `--then 覆盖了 ${scenario.id} 场景默认加工链`,
|
|
32
|
-
en: `--then overrides the default ${scenario.id} processing chain`,
|
|
33
|
-
}));
|
|
48
|
+
warnings.push(`--then overrides the default ${scenario.id} processing chain`);
|
|
34
49
|
}
|
|
35
50
|
switch (classified.kind) {
|
|
36
51
|
case 'text': {
|
|
37
52
|
const prompt = classified.values[0].value;
|
|
38
|
-
const choice =
|
|
39
|
-
explicit: options.model ?? scenario?.model,
|
|
40
|
-
faceLimit: typeof genParams.face_limit === 'number' ? genParams.face_limit : undefined,
|
|
41
|
-
lowPoly: detectLowPolyIntent(prompt),
|
|
42
|
-
});
|
|
53
|
+
const choice = chooseGenerationModel(options, scenario, genParams, prompt);
|
|
43
54
|
const generation = buildTextToModel(prompt, choice.model, genParams);
|
|
44
55
|
return {
|
|
45
56
|
generation,
|
|
46
57
|
chain,
|
|
58
|
+
downloadGeneration,
|
|
47
59
|
model: choice.model,
|
|
48
60
|
modelReason: choice.reason,
|
|
49
61
|
name: options.name ?? prompt,
|
|
@@ -54,14 +66,12 @@ export async function planMake(client, options) {
|
|
|
54
66
|
case 'url-image': {
|
|
55
67
|
const raw = classified.values[0].value;
|
|
56
68
|
const { value } = await resolveInputValue(client, raw, { dryRun: options.dryRun });
|
|
57
|
-
const choice =
|
|
58
|
-
explicit: options.model ?? scenario?.model,
|
|
59
|
-
faceLimit: typeof genParams.face_limit === 'number' ? genParams.face_limit : undefined,
|
|
60
|
-
});
|
|
69
|
+
const choice = chooseGenerationModel(options, scenario, genParams);
|
|
61
70
|
const generation = buildImageToModel(value, choice.model, genParams);
|
|
62
71
|
return {
|
|
63
72
|
generation,
|
|
64
73
|
chain,
|
|
74
|
+
downloadGeneration,
|
|
65
75
|
model: choice.model,
|
|
66
76
|
modelReason: choice.reason,
|
|
67
77
|
name: options.name ?? basename(raw),
|
|
@@ -70,14 +80,12 @@ export async function planMake(client, options) {
|
|
|
70
80
|
}
|
|
71
81
|
case 'multiview': {
|
|
72
82
|
const views = await assignViews(client, classified.values.map((v) => v.value), { dryRun: options.dryRun });
|
|
73
|
-
const choice =
|
|
74
|
-
explicit: options.model ?? scenario?.model,
|
|
75
|
-
faceLimit: typeof genParams.face_limit === 'number' ? genParams.face_limit : undefined,
|
|
76
|
-
});
|
|
83
|
+
const choice = chooseGenerationModel(options, scenario, genParams);
|
|
77
84
|
const generation = buildMultiviewToModel(views, choice.model, genParams);
|
|
78
85
|
return {
|
|
79
86
|
generation,
|
|
80
87
|
chain,
|
|
88
|
+
downloadGeneration,
|
|
81
89
|
model: choice.model,
|
|
82
90
|
modelReason: choice.reason,
|
|
83
91
|
name: options.name ?? basename(classified.values[0].value),
|
|
@@ -89,10 +97,7 @@ export async function planMake(client, options) {
|
|
|
89
97
|
const { value } = await resolveInputValue(client, raw, { dryRun: options.dryRun });
|
|
90
98
|
const generation = buildStepRequest('import', value, {});
|
|
91
99
|
if (chain.length === 0) {
|
|
92
|
-
warnings.push(
|
|
93
|
-
zh: '导入模型后没有加工步骤;用 --then 指定(如 --then texture,rig)',
|
|
94
|
-
en: 'importing a model with no processing steps; add --then (e.g. --then texture,rig)',
|
|
95
|
-
}));
|
|
100
|
+
warnings.push('importing a model with no processing steps; add --then (e.g. --then texture,rig)');
|
|
96
101
|
}
|
|
97
102
|
return {
|
|
98
103
|
generation,
|
|
@@ -121,6 +126,7 @@ export async function planMake(client, options) {
|
|
|
121
126
|
* Skipped when the upstream type is unknown (raw task id without local history).
|
|
122
127
|
*/
|
|
123
128
|
export function checkPlanChain(plan) {
|
|
129
|
+
validateChainRequests(plan.chain);
|
|
124
130
|
if (plan.chain.length === 0)
|
|
125
131
|
return;
|
|
126
132
|
const upstream = plan.generation?.taskType ?? plan.sourceType;
|
|
@@ -147,7 +153,7 @@ export async function executeMake(client, plan, options) {
|
|
|
147
153
|
let primary;
|
|
148
154
|
if (plan.generation) {
|
|
149
155
|
if (plan.model && plan.modelReason) {
|
|
150
|
-
log(
|
|
156
|
+
log(`model: ${plan.model} (${plan.modelReason})`);
|
|
151
157
|
}
|
|
152
158
|
primary = await runTask({
|
|
153
159
|
client,
|
|
@@ -156,10 +162,11 @@ export async function executeMake(client, plan, options) {
|
|
|
156
162
|
name: plan.name,
|
|
157
163
|
outDir: options.outDir,
|
|
158
164
|
noWait: options.noWait,
|
|
159
|
-
noDownload: options.noDownload || plan.chain.length > 0,
|
|
165
|
+
noDownload: options.noDownload || (plan.chain.length > 0 && !plan.downloadGeneration),
|
|
160
166
|
timeoutMs: options.timeoutMs,
|
|
161
167
|
notify: options.notify && plan.chain.length === 0,
|
|
162
168
|
precheckBalance: false,
|
|
169
|
+
onCreated: options.onTaskCreated,
|
|
163
170
|
});
|
|
164
171
|
if (options.noWait)
|
|
165
172
|
return { primary, chainResults: [] };
|
|
@@ -183,6 +190,7 @@ export async function executeMake(client, plan, options) {
|
|
|
183
190
|
outDir: options.outDir,
|
|
184
191
|
notify: false,
|
|
185
192
|
timeoutMs: options.timeoutMs,
|
|
193
|
+
onTaskCreated: options.onTaskCreated,
|
|
186
194
|
});
|
|
187
195
|
// If the chain stopped early (not riggable) and no step downloaded a model,
|
|
188
196
|
// fetch the source artifacts so the user still gets their model.
|
package/dist/pipeline/runner.js
CHANGED
|
@@ -5,14 +5,14 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { UsageError } from '../core/errors.js';
|
|
7
7
|
import { log, warn } from '../core/output.js';
|
|
8
|
-
import { buildStepRequest } from '../core/requests.js';
|
|
8
|
+
import { buildStepRequest, validateChainRequests } from '../core/requests.js';
|
|
9
9
|
import { runTask } from '../core/task-service.js';
|
|
10
|
-
import { t } from '../i18n.js';
|
|
11
10
|
import { getStep, validateChain } from '../knowledge/chains.js';
|
|
12
11
|
import { NON_BIPED_PRESETS } from '../knowledge/models.js';
|
|
13
12
|
export async function runChain(options) {
|
|
14
13
|
const { client, chain } = options;
|
|
15
14
|
validateChain(chain, options.sourceType);
|
|
15
|
+
validateChainRequests(chain);
|
|
16
16
|
const results = [];
|
|
17
17
|
let currentTaskId = options.sourceTaskId;
|
|
18
18
|
let rigTypeRecommendation;
|
|
@@ -22,7 +22,7 @@ export async function runChain(options) {
|
|
|
22
22
|
const args = { ...spec.args };
|
|
23
23
|
if (spec.step === 'rig' && rigTypeRecommendation && args.rig_type === undefined) {
|
|
24
24
|
args.rig_type = rigTypeRecommendation;
|
|
25
|
-
log(
|
|
25
|
+
log(`using rig-check recommendation: ${rigTypeRecommendation}`);
|
|
26
26
|
}
|
|
27
27
|
if (spec.step === 'rig')
|
|
28
28
|
rigType = String(args.rig_type ?? 'biped');
|
|
@@ -33,7 +33,7 @@ export async function runChain(options) {
|
|
|
33
33
|
if (requested.length > 0 && requested.every((a) => /^preset:[^:]+$/.test(a))) {
|
|
34
34
|
delete args.animation;
|
|
35
35
|
args.animations = bodyPresets;
|
|
36
|
-
log(
|
|
36
|
+
log(`rig type is ${rigType}: biped presets swapped for ${bodyPresets.join(', ')}`);
|
|
37
37
|
}
|
|
38
38
|
}
|
|
39
39
|
// avian/others have no preset animations: the rigged model is the final output.
|
|
@@ -52,13 +52,11 @@ export async function runChain(options) {
|
|
|
52
52
|
// Intermediate steps skip download; final step downloads.
|
|
53
53
|
noDownload: i < chain.length - 1 && spec.step !== 'rig-check' && !noPresets,
|
|
54
54
|
precheckBalance: false, // checked once by the caller
|
|
55
|
+
onCreated: options.onTaskCreated,
|
|
55
56
|
});
|
|
56
57
|
results.push(result);
|
|
57
58
|
if (noPresets) {
|
|
58
|
-
const message =
|
|
59
|
-
zh: `体型 ${rigType} 没有预设动画,已跳过 retarget,产物为绑骨模型`,
|
|
60
|
-
en: `rig type ${rigType} has no preset animations; skipped retarget and kept the rigged model`,
|
|
61
|
-
});
|
|
59
|
+
const message = `rig type ${rigType} has no preset animations; skipped retarget and kept the rigged model`;
|
|
62
60
|
warn(message);
|
|
63
61
|
return { results, stoppedEarly: message };
|
|
64
62
|
}
|
|
@@ -68,10 +66,7 @@ export async function runChain(options) {
|
|
|
68
66
|
const riggable = output.riggable;
|
|
69
67
|
rigTypeRecommendation = typeof output.rig_type === 'string' ? output.rig_type : undefined;
|
|
70
68
|
if (riggable === false) {
|
|
71
|
-
const message =
|
|
72
|
-
zh: 'rig-check 判定该模型不可绑骨,动画链止损停止(后续步骤未执行,未扣积分)',
|
|
73
|
-
en: 'rig-check says this model is not riggable; stopping the chain early (no further credits spent)',
|
|
74
|
-
});
|
|
69
|
+
const message = 'rig-check says this model is not riggable; stopping the chain early (no further credits spent)';
|
|
75
70
|
warn(message);
|
|
76
71
|
return { results, stoppedEarly: message };
|
|
77
72
|
}
|
package/dist/pipeline/stdin.js
CHANGED
|
@@ -2,12 +2,48 @@
|
|
|
2
2
|
* NDJSON pipe support: downstream commands read the upstream task result from
|
|
3
3
|
* stdin, enabling `tripo make cat.png | tripo model texture | tripo anim rig`.
|
|
4
4
|
*/
|
|
5
|
-
|
|
5
|
+
import { CliError, UsageError } from '../core/errors.js';
|
|
6
|
+
import { EXIT } from '../knowledge/error-catalog.js';
|
|
7
|
+
/**
|
|
8
|
+
* Read piped NDJSON/JSON from stdin; returns undefined only on a TTY.
|
|
9
|
+
* Piped input that carries an upstream error or no task throws, so a broken
|
|
10
|
+
* pipeline never falls back to some other task and spends credits on it.
|
|
11
|
+
*/
|
|
6
12
|
export async function readPipedTask() {
|
|
7
13
|
if (process.stdin.isTTY)
|
|
8
14
|
return undefined;
|
|
9
15
|
const raw = await readAll(process.stdin);
|
|
10
|
-
|
|
16
|
+
const upstream = parsePipedError(raw);
|
|
17
|
+
if (upstream) {
|
|
18
|
+
const code = upstream.exit_code;
|
|
19
|
+
const exitCode = code !== undefined && Number.isInteger(code) && code >= 1 && code <= 9 ? code : EXIT.USAGE;
|
|
20
|
+
throw new CliError(`upstream command failed${code !== undefined ? ` (exit ${code})` : ''}: ${upstream.error}; nothing was submitted`, exitCode);
|
|
21
|
+
}
|
|
22
|
+
const task = parsePipedText(raw);
|
|
23
|
+
if (!task) {
|
|
24
|
+
throw new UsageError('no task on stdin (piped input was empty or carried no task_id); nothing was submitted', 'pass the input explicitly, e.g. "tripo model texture @last" or a task id');
|
|
25
|
+
}
|
|
26
|
+
return task;
|
|
27
|
+
}
|
|
28
|
+
/** The upstream error when the last JSON line is an error object (`{"error","exit_code"}`). */
|
|
29
|
+
export function parsePipedError(raw) {
|
|
30
|
+
const lines = raw.trim().split('\n').map((l) => l.trim()).filter(Boolean);
|
|
31
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
32
|
+
let obj;
|
|
33
|
+
try {
|
|
34
|
+
obj = JSON.parse(lines[i]);
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
if (typeof obj !== 'object' || obj === null)
|
|
40
|
+
continue;
|
|
41
|
+
const { error, exit_code: exitCode } = obj;
|
|
42
|
+
if (typeof error !== 'string')
|
|
43
|
+
return undefined;
|
|
44
|
+
return { error, exit_code: typeof exitCode === 'number' ? exitCode : undefined };
|
|
45
|
+
}
|
|
46
|
+
return undefined;
|
|
11
47
|
}
|
|
12
48
|
/** Parse piped text: JSON object, NDJSON (last task line wins), or a bare task id. */
|
|
13
49
|
export function parsePipedText(raw) {
|
|
@@ -18,7 +54,8 @@ export function parsePipedText(raw) {
|
|
|
18
54
|
for (let i = lines.length - 1; i >= 0; i--) {
|
|
19
55
|
try {
|
|
20
56
|
const obj = JSON.parse(lines[i]);
|
|
21
|
-
|
|
57
|
+
// Error JSON may name the failed task; it is never an input.
|
|
58
|
+
if (typeof obj.task_id === 'string' && obj.error === undefined) {
|
|
22
59
|
return { task_id: obj.task_id, type: typeof obj.type === 'string' ? obj.type : undefined };
|
|
23
60
|
}
|
|
24
61
|
}
|
package/dist/program.js
CHANGED
|
@@ -18,11 +18,9 @@ import { registerTaskCommands } from './commands/task.js';
|
|
|
18
18
|
import { registerTopupCommand } from './commands/topup.js';
|
|
19
19
|
import { notifyIfOutdated, registerUpdateCommand } from './commands/update.js';
|
|
20
20
|
import { registerViewCommands } from './commands/view.js';
|
|
21
|
-
import { loadConfig } from './config/config.js';
|
|
22
21
|
import { RefError } from './config/history.js';
|
|
23
|
-
import { ApiError, CliError, UsageError } from './core/errors.js';
|
|
22
|
+
import { ApiError, ArtifactDownloadError, CliError, TaskFailedError, UsageError } from './core/errors.js';
|
|
24
23
|
import { configureOutput, errorLine, isJsonMode } from './core/output.js';
|
|
25
|
-
import { setLocale, t } from './i18n.js';
|
|
26
24
|
import { EXIT } from './knowledge/error-catalog.js';
|
|
27
25
|
import { cliVersion } from './version.js';
|
|
28
26
|
import { beginOperation, finishOperation } from './core/analytics.js';
|
|
@@ -30,10 +28,7 @@ export function buildProgram() {
|
|
|
30
28
|
const program = new Command();
|
|
31
29
|
program
|
|
32
30
|
.name('tripo')
|
|
33
|
-
.description(
|
|
34
|
-
zh: 'Tripo 3D 智能 CLI — 一句话/一张图,直出可进引擎的 3D 资产\n高频: make(生成) ai(专家规划) view(预览) topup(充值)',
|
|
35
|
-
en: 'Tripo 3D intelligent CLI — turn a sentence or an image into engine-ready 3D assets\ntop commands: make · ai · view · topup',
|
|
36
|
-
}))
|
|
31
|
+
.description('Tripo 3D intelligent CLI — turn a sentence or an image into engine-ready 3D assets\ntop commands: make · ai · view · topup')
|
|
37
32
|
.version(cliVersion(), '-v, --version')
|
|
38
33
|
.option('--json', 'machine-readable output on stdout (auto-enabled when piped)')
|
|
39
34
|
.option('--yes', 'skip confirmations (auto-enabled when headless)')
|
|
@@ -54,9 +49,6 @@ export function buildProgram() {
|
|
|
54
49
|
const profile = resolveGlobalString(actionCommand, 'profile');
|
|
55
50
|
if (profile)
|
|
56
51
|
process.env.TRIPO_PROFILE = profile;
|
|
57
|
-
const lang = loadConfig().language;
|
|
58
|
-
if (lang)
|
|
59
|
-
setLocale(lang);
|
|
60
52
|
const path = [];
|
|
61
53
|
for (let command = actionCommand; command && command !== program; command = command.parent)
|
|
62
54
|
path.unshift(command.name());
|
|
@@ -158,6 +150,7 @@ export function handleError(err) {
|
|
|
158
150
|
...(err instanceof ApiError
|
|
159
151
|
? { api_code: err.apiCode, request_id: err.requestId, http_status: err.httpStatus }
|
|
160
152
|
: {}),
|
|
153
|
+
...(err instanceof TaskFailedError || err instanceof ArtifactDownloadError ? { task_id: err.taskId } : {}),
|
|
161
154
|
...(err.suggestion ? { suggestion: err.suggestion } : {}),
|
|
162
155
|
}) + '\n');
|
|
163
156
|
}
|