tripo-cli 0.7.2 → 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 +16 -14
- package/dist/ai/experts.js +26 -20
- package/dist/ai/plan-card.js +73 -34
- package/dist/ai/wizard.js +19 -19
- package/dist/commands/account.js +2 -6
- package/dist/commands/ai.js +18 -16
- package/dist/commands/batch.js +58 -18
- package/dist/commands/completion.js +1 -1
- package/dist/commands/config-cmd.js +1 -1
- package/dist/commands/doctor.js +1 -3
- package/dist/commands/generate.js +50 -8
- package/dist/commands/home.js +12 -13
- package/dist/commands/login.js +125 -182
- package/dist/commands/make.js +50 -25
- package/dist/commands/mcp.js +8 -3
- package/dist/commands/process-groups.js +29 -18
- package/dist/commands/shared.js +22 -13
- package/dist/commands/task.js +12 -11
- package/dist/commands/topup.js +6 -10
- package/dist/commands/update.js +23 -11
- package/dist/commands/view.js +3 -4
- package/dist/config/config.js +1 -1
- package/dist/core/analytics.js +39 -16
- package/dist/core/client.js +53 -21
- package/dist/core/device-auth.js +49 -1
- package/dist/core/download.js +2 -1
- package/dist/core/errors.js +24 -14
- package/dist/core/output.js +0 -4
- package/dist/core/requests.js +94 -31
- package/dist/core/task-service.js +21 -20
- package/dist/core/upload.js +23 -5
- package/dist/core/watch.js +3 -0
- package/dist/knowledge/chains.js +16 -17
- package/dist/knowledge/error-catalog.js +39 -37
- package/dist/knowledge/models.js +147 -27
- package/dist/knowledge/params.js +143 -32
- package/dist/knowledge/scenarios.js +32 -55
- package/dist/pipeline/make.js +38 -30
- package/dist/pipeline/runner.js +27 -8
- package/dist/pipeline/stdin.js +40 -3
- package/dist/program.js +3 -10
- package/dist/util/input-detect.js +10 -1
- package/package.json +3 -2
- package/skill/SKILL.md +52 -15
- package/skill/commands/account.md +8 -3
- package/skill/commands/batch.md +13 -3
- package/skill/commands/generate.md +57 -23
- package/skill/commands/make.md +43 -16
- package/skill/commands/process.md +29 -24
- package/skill/commands/task.md +4 -4
- package/skill/commands/view.md +1 -1
- package/skill/common-errors.md +14 -10
- package/skill/examples/animation.md +4 -2
- package/skill/examples/ar-web.md +6 -4
- package/skill/examples/game-asset.md +18 -7
- package/skill/examples/pipes.md +2 -2
- package/skill/examples/print.md +7 -3
- package/dist/i18n.js +0 -19
package/dist/core/requests.js
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
* `parameters`), so new API options work without a CLI release.
|
|
6
6
|
*/
|
|
7
7
|
import { getStep } from '../knowledge/chains.js';
|
|
8
|
-
import { validateConvertParams, validateDecimateParams, validateGenerationParams, validateMeshEditParams, validateRetargetParams, } from '../knowledge/params.js';
|
|
9
|
-
import { CAPABILITY_DEFAULTS, SEGMENT_MODELS, TEXTURE_MODELS, normalizeModel, normalizeRigModel, } from '../knowledge/models.js';
|
|
8
|
+
import { applyTextureV35, validateConvertParams, validateDecimateParams, validateGenerationParams, validateImageParams, validateMeshEditParams, validateRetargetParams, } from '../knowledge/params.js';
|
|
9
|
+
import { CAPABILITY_DEFAULTS, RIG_MODEL_V1, RIG_MODEL_V25, RIG_MODEL_V30, RIG_SPECS, RIG_TYPES, SEGMENT_MODELS, TEXTURE_MODELS, normalizeModel, normalizeRigModel, } from '../knowledge/models.js';
|
|
10
10
|
import { UsageError } from './errors.js';
|
|
11
11
|
function ensure(result) {
|
|
12
12
|
if (result.errors.length > 0) {
|
|
@@ -15,12 +15,12 @@ function ensure(result) {
|
|
|
15
15
|
}
|
|
16
16
|
/** 3D generation: text-to-model. */
|
|
17
17
|
export function buildTextToModel(prompt, model, params) {
|
|
18
|
-
const validated = validateGenerationParams(model, params);
|
|
18
|
+
const validated = validateGenerationParams(model, { prompt, ...params });
|
|
19
19
|
ensure(validated);
|
|
20
20
|
return {
|
|
21
21
|
request: {
|
|
22
22
|
endpoint: '/v3/generation/text-to-model',
|
|
23
|
-
payload: { prompt, model: normalizeModel(model)
|
|
23
|
+
payload: { prompt, ...validated.params, model: normalizeModel(model) },
|
|
24
24
|
},
|
|
25
25
|
taskType: 'text_to_model',
|
|
26
26
|
warnings: validated.warnings,
|
|
@@ -33,27 +33,36 @@ export function buildImageToModel(input, model, params) {
|
|
|
33
33
|
return {
|
|
34
34
|
request: {
|
|
35
35
|
endpoint: '/v3/generation/image-to-model',
|
|
36
|
-
payload: { input, model: normalizeModel(model)
|
|
36
|
+
payload: { input, ...validated.params, model: normalizeModel(model) },
|
|
37
37
|
},
|
|
38
38
|
taskType: 'image_to_model',
|
|
39
39
|
warnings: validated.warnings,
|
|
40
40
|
};
|
|
41
41
|
}
|
|
42
|
-
/**
|
|
42
|
+
/**
|
|
43
|
+
* 3D generation: multiview-to-model. Views ordered [front, left, back, right],
|
|
44
|
+
* or `{task_id}` of an image-to-multiview / edit-multiview task to reuse its 4 views.
|
|
45
|
+
*/
|
|
43
46
|
export function buildMultiviewToModel(views, model, params) {
|
|
44
47
|
const validated = validateGenerationParams(model, params);
|
|
45
48
|
ensure(validated);
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
49
|
+
let inputs;
|
|
50
|
+
if ('task_id' in views) {
|
|
51
|
+
inputs = [{ task_id: views.task_id }];
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
inputs = ['front', 'left', 'back', 'right']
|
|
55
|
+
.filter((v) => views[v])
|
|
56
|
+
.map((v) => ({ [v]: views[v] }));
|
|
57
|
+
if (!views.front)
|
|
58
|
+
throw new UsageError('multiview requires a front view');
|
|
59
|
+
if (inputs.length < 2)
|
|
60
|
+
throw new UsageError('multiview requires at least 2 views');
|
|
61
|
+
}
|
|
53
62
|
return {
|
|
54
63
|
request: {
|
|
55
64
|
endpoint: '/v3/generation/multiview-to-model',
|
|
56
|
-
payload: { inputs, model: normalizeModel(model)
|
|
65
|
+
payload: { inputs, ...validated.params, model: normalizeModel(model) },
|
|
57
66
|
},
|
|
58
67
|
taskType: 'multiview_to_model',
|
|
59
68
|
warnings: validated.warnings,
|
|
@@ -63,23 +72,38 @@ export function buildMultiviewToModel(views, model, params) {
|
|
|
63
72
|
* Server task types (TaskType enum): text-to-image → `text_to_image`,
|
|
64
73
|
* image-to-image → `generate_image`. */
|
|
65
74
|
export function buildTextToImage(prompt, params) {
|
|
75
|
+
const validated = validateImageParams(params, false);
|
|
76
|
+
ensure(validated);
|
|
66
77
|
return {
|
|
67
78
|
request: {
|
|
68
79
|
endpoint: '/v3/generation/text-to-image',
|
|
69
|
-
payload: { prompt,
|
|
80
|
+
payload: { prompt, ...validated.params },
|
|
70
81
|
},
|
|
71
82
|
taskType: 'text_to_image',
|
|
72
|
-
warnings:
|
|
83
|
+
warnings: validated.warnings,
|
|
73
84
|
};
|
|
74
85
|
}
|
|
75
|
-
|
|
86
|
+
/** One reference image goes as `input`, several as `inputs` (prompt cites them as image[1], image[2], ...). */
|
|
87
|
+
export function buildImageToImage(inputs, params) {
|
|
88
|
+
const validated = validateImageParams(params, true);
|
|
89
|
+
ensure(validated);
|
|
90
|
+
const model = String(validated.params.model);
|
|
91
|
+
const maxInputs = model.startsWith('seedream') ? 4 : model.startsWith('banana') ? 10 : 16;
|
|
92
|
+
if (inputs.length === 0)
|
|
93
|
+
throw new UsageError('image-to-image requires at least one reference image');
|
|
94
|
+
if (inputs.length > maxInputs) {
|
|
95
|
+
throw new UsageError(`${model} accepts at most ${maxInputs} reference images (got ${inputs.length})`);
|
|
96
|
+
}
|
|
76
97
|
return {
|
|
77
98
|
request: {
|
|
78
99
|
endpoint: '/v3/generation/image-to-image',
|
|
79
|
-
payload: {
|
|
100
|
+
payload: {
|
|
101
|
+
...(inputs.length > 1 ? { inputs } : { input: inputs[0] }),
|
|
102
|
+
...validated.params,
|
|
103
|
+
},
|
|
80
104
|
},
|
|
81
105
|
taskType: 'generate_image',
|
|
82
|
-
warnings:
|
|
106
|
+
warnings: validated.warnings,
|
|
83
107
|
};
|
|
84
108
|
}
|
|
85
109
|
export function buildImageToMultiview(input, params) {
|
|
@@ -103,6 +127,27 @@ export function buildImageToSplat(input, params) {
|
|
|
103
127
|
warnings: [],
|
|
104
128
|
};
|
|
105
129
|
}
|
|
130
|
+
/** Validate deterministic request and rig/preset constraints before any earlier task runs. */
|
|
131
|
+
export function validateChainRequests(chain) {
|
|
132
|
+
let rigModel;
|
|
133
|
+
for (const spec of chain) {
|
|
134
|
+
const built = buildStepRequest(spec.step, '<upstream>', spec.args);
|
|
135
|
+
if (built.taskType === 'animate_retarget' && rigModel) {
|
|
136
|
+
const { animation, animations } = built.request.payload;
|
|
137
|
+
const requested = Array.isArray(animations) ? animations : [animation];
|
|
138
|
+
for (const preset of requested) {
|
|
139
|
+
if (typeof preset !== 'string' || !preset.startsWith('preset:'))
|
|
140
|
+
continue;
|
|
141
|
+
if ((rigModel === RIG_MODEL_V1) !== preset.startsWith('preset:biped:')) {
|
|
142
|
+
throw new UsageError(`preset ${preset} is incompatible with rig model ${rigModel}; v1.0 uses preset:biped:*, v2.5/v3.0 use generic or body-type presets`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
if (!getStep(spec.step)?.passthrough) {
|
|
147
|
+
rigModel = built.taskType === 'animate_rig' ? String(built.request.payload.model) : undefined;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
106
151
|
/**
|
|
107
152
|
* Processing step (texture/convert/rig/...): validate per step, then build.
|
|
108
153
|
* `input` must already be a task_id / file_token / URL.
|
|
@@ -147,16 +192,40 @@ export function buildStepRequest(stepName, input, args) {
|
|
|
147
192
|
params.model = params.model === undefined
|
|
148
193
|
? CAPABILITY_DEFAULTS.rig
|
|
149
194
|
: normalizeRigModel(String(params.model));
|
|
195
|
+
if (![RIG_MODEL_V1, RIG_MODEL_V25, RIG_MODEL_V30].includes(String(params.model))) {
|
|
196
|
+
throw new UsageError('unsupported rig model');
|
|
197
|
+
}
|
|
198
|
+
const rigType = params.rig_type === undefined ? 'biped' : params.rig_type;
|
|
199
|
+
if (!RIG_TYPES.includes(rigType))
|
|
200
|
+
throw new UsageError('unsupported rig_type');
|
|
201
|
+
if (params.model === RIG_MODEL_V1 && rigType !== 'biped')
|
|
202
|
+
throw new UsageError('rig v1.0 only supports biped');
|
|
203
|
+
if (rigType === 'others' && params.model !== RIG_MODEL_V30)
|
|
204
|
+
throw new UsageError('rig_type others requires rig v3.0');
|
|
205
|
+
if (params.spec !== undefined) {
|
|
206
|
+
if (!RIG_SPECS.includes(params.spec))
|
|
207
|
+
throw new UsageError('unsupported rig spec');
|
|
208
|
+
if (rigType !== 'biped')
|
|
209
|
+
throw new UsageError('rig spec only applies to biped');
|
|
210
|
+
if (!['tripo', 'mixamo'].includes(String(params.spec)) && params.model !== RIG_MODEL_V30) {
|
|
211
|
+
throw new UsageError('spec actorcore/unity/unreal/vrm requires rig v3.0');
|
|
212
|
+
}
|
|
213
|
+
}
|
|
150
214
|
break;
|
|
151
215
|
}
|
|
152
216
|
case 'texture': {
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
217
|
+
if (params.model !== undefined) {
|
|
218
|
+
const model = normalizeModel(String(params.model));
|
|
219
|
+
if (!TEXTURE_MODELS.includes(model)) {
|
|
220
|
+
throw new UsageError(`texture model must be one of ${TEXTURE_MODELS.join('/')} (got "${String(params.model)}")`);
|
|
221
|
+
}
|
|
222
|
+
params.model = model;
|
|
158
223
|
}
|
|
159
|
-
|
|
224
|
+
const errors = [];
|
|
225
|
+
applyTextureV35(params, 'model', warnings, errors);
|
|
226
|
+
ensure({ params, warnings, errors });
|
|
227
|
+
if (params.model === undefined)
|
|
228
|
+
params.model = CAPABILITY_DEFAULTS.texture;
|
|
160
229
|
break;
|
|
161
230
|
}
|
|
162
231
|
case 'segment': {
|
|
@@ -190,9 +259,3 @@ export function buildStepRequest(stepName, input, args) {
|
|
|
190
259
|
warnings,
|
|
191
260
|
};
|
|
192
261
|
}
|
|
193
|
-
function omit(obj, keys) {
|
|
194
|
-
const out = { ...obj };
|
|
195
|
-
for (const k of keys)
|
|
196
|
-
delete out[k];
|
|
197
|
-
return out;
|
|
198
|
-
}
|
|
@@ -8,13 +8,12 @@ import { basename } from 'node:path';
|
|
|
8
8
|
import { resolvePlatformBaseUrl, resolveRegion } from '../config/config.js';
|
|
9
9
|
import { loadContext, updateContext } from '../config/context.js';
|
|
10
10
|
import { RefError, appendHistory, readHistory, resolveReference, slugify, updateHistoryStatus } from '../config/history.js';
|
|
11
|
-
import { t } from '../i18n.js';
|
|
12
11
|
import { detectInput } from '../util/input-detect.js';
|
|
13
12
|
import { notifyDone } from '../util/notify.js';
|
|
14
13
|
import { createSpinner } from '../util/spinner.js';
|
|
15
14
|
import { defaultOutputDir, downloadArtifacts } from './download.js';
|
|
16
15
|
import { taskOrigin } from './analytics.js';
|
|
17
|
-
import { ApiError, CliError, TaskFailedError, UsageError, balanceSuggestion } from './errors.js';
|
|
16
|
+
import { ApiError, ArtifactDownloadError, CliError, TaskFailedError, UsageError, balanceSuggestion } from './errors.js';
|
|
18
17
|
import { isQuiet, log, warn } from './output.js';
|
|
19
18
|
import { uploadLocalFile } from './upload.js';
|
|
20
19
|
/** Placeholder `input` value for a local file that a dry run would upload. */
|
|
@@ -75,10 +74,7 @@ export async function precheckBalance(client) {
|
|
|
75
74
|
throw new ApiError({ apiCode: 2010, suggestion: balanceSuggestion(undefined, balance) });
|
|
76
75
|
}
|
|
77
76
|
if (balance < 30) {
|
|
78
|
-
warn(
|
|
79
|
-
zh: `余额仅剩 ${balance} 积分,任务可能因余额不足失败(tripo topup 充值)`,
|
|
80
|
-
en: `balance is down to ${balance} credits; tasks may fail (run "tripo topup")`,
|
|
81
|
-
}));
|
|
77
|
+
warn(`balance is down to ${balance} credits; tasks may fail (run "tripo topup")`);
|
|
82
78
|
}
|
|
83
79
|
}
|
|
84
80
|
/** Submit one task and (by default) watch it to completion + download. */
|
|
@@ -87,6 +83,8 @@ export async function runTask(options) {
|
|
|
87
83
|
if (options.precheckBalance !== false)
|
|
88
84
|
await precheckBalance(client);
|
|
89
85
|
const taskId = await client.createTask(request.endpoint, request.payload, taskType);
|
|
86
|
+
// Report the task before any local bookkeeping that can throw: batch decides retry safety from it.
|
|
87
|
+
options.onCreated?.({ task_id: taskId, type: taskType });
|
|
90
88
|
const name = uniqueName(slugify(options.name));
|
|
91
89
|
const entry = {
|
|
92
90
|
analytics_origin: taskOrigin(request.payload.model),
|
|
@@ -99,8 +97,13 @@ export async function runTask(options) {
|
|
|
99
97
|
payload: request.payload,
|
|
100
98
|
};
|
|
101
99
|
appendHistory(entry);
|
|
102
|
-
|
|
103
|
-
|
|
100
|
+
try {
|
|
101
|
+
updateContext({ last_task_id: taskId });
|
|
102
|
+
}
|
|
103
|
+
catch (err) {
|
|
104
|
+
warn(`could not update .tripo/context.json (${err instanceof Error ? err.message : String(err)})`);
|
|
105
|
+
}
|
|
106
|
+
log(`task created ${taskId} (@${name})`);
|
|
104
107
|
if (options.noWait) {
|
|
105
108
|
return { task_id: taskId, type: taskType, status: 'queued' };
|
|
106
109
|
}
|
|
@@ -121,17 +124,14 @@ export async function runTask(options) {
|
|
|
121
124
|
updateHistoryStatus(taskId, 'failed');
|
|
122
125
|
}
|
|
123
126
|
else if (err instanceof CliError) {
|
|
124
|
-
err.suggestion =
|
|
125
|
-
zh: `任务仍在服务端运行,成功后照常扣费;用 tripo task watch ${taskId} --download 继续等待,不要重新提交`,
|
|
126
|
-
en: `the task keeps running server-side and is billed if it succeeds; resume with "tripo task watch ${taskId} --download" instead of resubmitting`,
|
|
127
|
-
});
|
|
127
|
+
err.suggestion = `the task keeps running server-side and is billed if it succeeds; resume with "tripo task watch ${taskId} --download" instead of resubmitting`;
|
|
128
128
|
}
|
|
129
129
|
throw err;
|
|
130
130
|
}
|
|
131
131
|
updateHistoryStatus(taskId, 'success', { credits_consumed: task.credits_consumed });
|
|
132
132
|
spinner.succeed(`${taskType} · ${taskId}` +
|
|
133
133
|
(task.credits_consumed !== undefined
|
|
134
|
-
?
|
|
134
|
+
? ` · ${task.credits_consumed} credits`
|
|
135
135
|
: ''));
|
|
136
136
|
const result = {
|
|
137
137
|
task_id: taskId,
|
|
@@ -139,15 +139,17 @@ export async function runTask(options) {
|
|
|
139
139
|
status: task.status,
|
|
140
140
|
credits_consumed: task.credits_consumed,
|
|
141
141
|
};
|
|
142
|
+
if (typeof task.output?.normalize_scale === 'number')
|
|
143
|
+
result.normalize_scale = task.output.normalize_scale;
|
|
142
144
|
if (!options.noDownload) {
|
|
143
145
|
const baseDir = options.outDir ?? loadContext().output_dir ?? process.cwd();
|
|
144
146
|
const dir = defaultOutputDir(baseDir, name, taskId);
|
|
145
|
-
const dlSpinner = createSpinner(
|
|
147
|
+
const dlSpinner = createSpinner('downloading artifacts…', isQuiet());
|
|
146
148
|
try {
|
|
147
149
|
const artifacts = await downloadArtifacts(task, dir, entry, {
|
|
148
|
-
onFile: (f) => dlSpinner.update(
|
|
150
|
+
onFile: (f) => dlSpinner.update(`downloading ${f}`),
|
|
149
151
|
});
|
|
150
|
-
dlSpinner.succeed(
|
|
152
|
+
dlSpinner.succeed(`artifacts saved to ${artifacts.outputDir}`);
|
|
151
153
|
result.output_dir = artifacts.outputDir;
|
|
152
154
|
result.files = artifacts.files.map((f) => basename(f));
|
|
153
155
|
result.model_file = artifacts.modelFile;
|
|
@@ -155,13 +157,12 @@ export async function runTask(options) {
|
|
|
155
157
|
updateHistoryStatus(taskId, 'success', { output_dir: artifacts.outputDir });
|
|
156
158
|
}
|
|
157
159
|
catch (err) {
|
|
158
|
-
dlSpinner.fail(
|
|
159
|
-
|
|
160
|
-
warn(t({ zh: `可重试:tripo task get ${taskId} --download`, en: `retry with: tripo task get ${taskId} --download` }));
|
|
160
|
+
dlSpinner.fail('artifact download failed (the task itself succeeded)');
|
|
161
|
+
throw new ArtifactDownloadError(taskId, result, err instanceof Error ? err.message : String(err));
|
|
161
162
|
}
|
|
162
163
|
}
|
|
163
164
|
if (options.notify) {
|
|
164
|
-
notifyDone('Tripo',
|
|
165
|
+
notifyDone('Tripo', `${taskType} finished`);
|
|
165
166
|
}
|
|
166
167
|
return result;
|
|
167
168
|
}
|
package/dist/core/upload.js
CHANGED
|
@@ -5,6 +5,8 @@ import { UsageError } from './errors.js';
|
|
|
5
5
|
import { IMAGE_EXTS, MODEL_EXTS } from '../util/input-detect.js';
|
|
6
6
|
const IMAGE_MAX = 20 * 1024 * 1024;
|
|
7
7
|
const MODEL_MAX = 150 * 1024 * 1024;
|
|
8
|
+
/** Above this size the docs recommend the presigned direct upload. */
|
|
9
|
+
const PRESIGN_THRESHOLD = 60 * 1024 * 1024;
|
|
8
10
|
const MIME = {
|
|
9
11
|
'.png': 'image/png',
|
|
10
12
|
'.jpg': 'image/jpeg',
|
|
@@ -25,18 +27,34 @@ export async function uploadLocalFile(client, filePath) {
|
|
|
25
27
|
if (!isImage && !isModel) {
|
|
26
28
|
throw new UsageError(`unsupported file type "${ext}" for upload`);
|
|
27
29
|
}
|
|
28
|
-
// The server's upload whitelist has no gltf: a .gltf references external
|
|
29
|
-
// .bin/texture files, so a single-file upload would be incomplete anyway.
|
|
30
|
-
if (ext === '.gltf') {
|
|
31
|
-
throw new UsageError('the upload API does not accept .gltf (it references external files) — export as .glb, or host it and pass the URL');
|
|
32
|
-
}
|
|
33
30
|
const info = await stat(filePath);
|
|
34
31
|
const limit = isImage ? IMAGE_MAX : MODEL_MAX;
|
|
35
32
|
if (info.size > limit) {
|
|
36
33
|
throw new UsageError(`${basename(filePath)} is ${(info.size / 1024 / 1024).toFixed(1)}MB, exceeding the ${limit / 1024 / 1024}MB ${isImage ? 'image' : 'model'} limit`);
|
|
37
34
|
}
|
|
38
35
|
const bytes = await readFile(filePath);
|
|
36
|
+
// .gltf goes through presign: the multipart whitelist has rejected it, the presign one lists it.
|
|
37
|
+
if (ext === '.gltf')
|
|
38
|
+
assertSelfContainedGltf(filePath, bytes);
|
|
39
|
+
if (ext === '.gltf' || info.size > PRESIGN_THRESHOLD) {
|
|
40
|
+
const { file_token } = await client.uploadFileViaPresign(ext.slice(1), new Blob([bytes]));
|
|
41
|
+
return file_token;
|
|
42
|
+
}
|
|
39
43
|
const blob = new Blob([bytes], { type: MIME[ext] ?? 'application/octet-stream' });
|
|
40
44
|
const { file_token } = await client.uploadFile(basename(filePath), blob);
|
|
41
45
|
return file_token;
|
|
42
46
|
}
|
|
47
|
+
/** A single-file upload cannot carry a .gltf's external .bin/texture files. */
|
|
48
|
+
function assertSelfContainedGltf(filePath, bytes) {
|
|
49
|
+
let doc;
|
|
50
|
+
try {
|
|
51
|
+
doc = JSON.parse(bytes.toString('utf8'));
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
throw new UsageError(`${basename(filePath)} is not valid glTF JSON`);
|
|
55
|
+
}
|
|
56
|
+
const external = [...(doc.buffers ?? []), ...(doc.images ?? [])].some((ref) => typeof ref.uri === 'string' && !ref.uri.startsWith('data:'));
|
|
57
|
+
if (external) {
|
|
58
|
+
throw new UsageError(`${basename(filePath)} references external files (.bin/textures) — export as .glb, or host it and pass the URL`);
|
|
59
|
+
}
|
|
60
|
+
}
|
package/dist/core/watch.js
CHANGED
|
@@ -36,6 +36,9 @@ export async function watchTask(client, taskId, options = {}) {
|
|
|
36
36
|
}
|
|
37
37
|
}
|
|
38
38
|
function extractFailureReason(task) {
|
|
39
|
+
if (task.error_message) {
|
|
40
|
+
return task.error_code !== undefined ? `${task.error_message} (code ${task.error_code})` : task.error_message;
|
|
41
|
+
}
|
|
39
42
|
const out = task.output ?? {};
|
|
40
43
|
const candidate = out.message ?? out.error ?? out.reason;
|
|
41
44
|
return typeof candidate === 'string' ? candidate : undefined;
|
package/dist/knowledge/chains.js
CHANGED
|
@@ -22,23 +22,22 @@ export const MODEL_PRODUCING_TYPES = new Set([
|
|
|
22
22
|
'animate_retarget',
|
|
23
23
|
]);
|
|
24
24
|
export const STEPS = [
|
|
25
|
-
{ name: '
|
|
26
|
-
{ name: '
|
|
27
|
-
{ name: '
|
|
28
|
-
{ name: '
|
|
29
|
-
{ name: 'rig
|
|
30
|
-
{ name: '
|
|
31
|
-
{ name: '
|
|
32
|
-
{ name: '
|
|
33
|
-
{ name: '
|
|
34
|
-
{ name: '
|
|
35
|
-
{ name: 'uv', endpoint: '/v3/mesh/uv', produces: 'mesh_uv', accepts: 'any-model', acceptsFile: true, describe: { zh: '智能展 UV,原贴图/骨骼/动画映射到新布局', en: 'smart UV unwrap; remaps textures/rig/animation onto the new layout' } },
|
|
25
|
+
{ name: 'texture', endpoint: '/v3/models/texture', produces: 'texture_model', accepts: 'any-model', acceptsFile: true, describe: 'regenerate textures / PBR materials' },
|
|
26
|
+
{ name: 'convert', endpoint: '/v3/models/convert', produces: 'convert_model', accepts: 'any-model', acceptsFile: true, describe: 'convert format / texture size / orientation' },
|
|
27
|
+
{ name: 'import', endpoint: '/v3/models/import', produces: 'import_model', accepts: [], acceptsFile: true, describe: 'import a local model file as the chain source' },
|
|
28
|
+
{ name: 'rig-check', endpoint: '/v3/animations/rig-check', produces: 'animate_prerigcheck', accepts: 'any-model', acceptsFile: true, passthrough: true, aliases: ['check', 'rigcheck'], describe: 'check riggability (no model output; stops the chain if not riggable)' },
|
|
29
|
+
{ name: 'rig', endpoint: '/v3/animations/rig', produces: 'animate_rig', accepts: 'any-model', acceptsFile: true, describe: 'auto-rig a skeleton' },
|
|
30
|
+
{ name: 'retarget', endpoint: '/v3/animations/retarget', produces: 'animate_retarget', accepts: ['animate_rig'], acceptsFile: false, aliases: ['animate'], describe: 'retarget preset animations onto the rigged model' },
|
|
31
|
+
{ name: 'segment', endpoint: '/v3/mesh/segment', produces: 'mesh_segmentation', accepts: 'any-model', acceptsFile: true, describe: 'segment the mesh into parts' },
|
|
32
|
+
{ name: 'complete', endpoint: '/v3/mesh/complete', produces: 'mesh_completion', accepts: ['mesh_segmentation'], acceptsFile: false, describe: 'complete the segmented parts' },
|
|
33
|
+
{ name: 'decimate', endpoint: '/v3/mesh/decimate', produces: 'highpoly_to_lowpoly', accepts: 'any-model', acceptsFile: true, aliases: ['lowpoly'], describe: 'decimate to a target face count' },
|
|
34
|
+
{ name: 'uv', endpoint: '/v3/mesh/uv', produces: 'mesh_uv', accepts: 'any-model', acceptsFile: true, describe: 'smart UV unwrap; remaps textures/rig/animation onto the new layout' },
|
|
36
35
|
// mesh/edit: local retry/refine inside a bbox, P2 pipeline (model fixed to P2-20260801).
|
|
37
|
-
{ name: 'edit', endpoint: '/v3/mesh/edit', produces: 'mesh_edit', accepts: 'any-model', acceptsFile: true, describe:
|
|
36
|
+
{ name: 'edit', endpoint: '/v3/mesh/edit', produces: 'mesh_edit', accepts: 'any-model', acceptsFile: true, describe: 'redo one bbox region locally (P2 pipeline)' },
|
|
38
37
|
// smartsegment's `input` is file_token/URL only (the server does not resolve
|
|
39
38
|
// task ids here), so it cannot chain after a task. Type is refined to
|
|
40
39
|
// smartsegment_image|smartsegment_model at request-build time per seg_type.
|
|
41
|
-
{ name: 'smartsegment', endpoint: '/v3/mesh/smartsegment', produces: 'smartsegment_model', accepts: [], acceptsFile: true, describe:
|
|
40
|
+
{ name: 'smartsegment', endpoint: '/v3/mesh/smartsegment', produces: 'smartsegment_model', accepts: [], acceptsFile: true, describe: 'smart semantic segmentation' },
|
|
42
41
|
];
|
|
43
42
|
const stepIndex = new Map();
|
|
44
43
|
for (const s of STEPS) {
|
|
@@ -47,9 +46,10 @@ for (const s of STEPS) {
|
|
|
47
46
|
stepIndex.set(a, s);
|
|
48
47
|
}
|
|
49
48
|
/**
|
|
50
|
-
* `/v3/mesh/edit`
|
|
51
|
-
*
|
|
52
|
-
* to opt in
|
|
49
|
+
* `/v3/mesh/edit` has a reference page that is still left out of the docs
|
|
50
|
+
* index, and cn returned 4001 when last checked. The step stays hidden; set
|
|
51
|
+
* TRIPO_EXPERIMENTAL_MESH_EDIT=1 to opt in. Flip this default once the
|
|
52
|
+
* endpoint is listed and live.
|
|
53
53
|
*/
|
|
54
54
|
export function isMeshEditEnabled() {
|
|
55
55
|
return process.env.TRIPO_EXPERIMENTAL_MESH_EDIT === '1';
|
|
@@ -171,7 +171,6 @@ export function validateChain(chain, sourceType) {
|
|
|
171
171
|
}
|
|
172
172
|
function hintFor(step) {
|
|
173
173
|
const hints = {
|
|
174
|
-
refine: ' (refine only accepts a successful text-to-model task)',
|
|
175
174
|
complete: ' (run "segment" first: complete only accepts mesh segmentation tasks)',
|
|
176
175
|
retarget: ' (run "rig" first: retarget only accepts rigged tasks)',
|
|
177
176
|
};
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* V3 error-code catalog: human
|
|
2
|
+
* V3 error-code catalog: human-readable messages, retryability, and the
|
|
3
3
|
* CLI exit code each error maps to.
|
|
4
4
|
*
|
|
5
5
|
* Source of truth: backend ErrorCode enum (platform-common .../error/ErrorCode.java,
|
|
6
|
-
* itself aligned with openapi-server errors.gen.go).
|
|
7
|
-
*
|
|
8
|
-
* 1000/1001
|
|
6
|
+
* itself aligned with openapi-server errors.gen.go). The public docs call
|
|
7
|
+
* 1000/1001 auth errors while the backend sends 1002 for auth failures and
|
|
8
|
+
* 1000/1001 for 5xx server errors — so HTTP 401 wins over the code table.
|
|
9
9
|
*/
|
|
10
10
|
export const EXIT = {
|
|
11
11
|
OK: 0,
|
|
@@ -20,37 +20,37 @@ export const EXIT = {
|
|
|
20
20
|
RATE_LIMIT: 9,
|
|
21
21
|
};
|
|
22
22
|
export const ERROR_CATALOG = [
|
|
23
|
-
{ code: 1000,
|
|
24
|
-
{ code: 1001,
|
|
25
|
-
{ code: 1002,
|
|
26
|
-
{ code: 1003,
|
|
27
|
-
{ code: 1004,
|
|
28
|
-
{ code: 1005,
|
|
29
|
-
{ code: 1007,
|
|
30
|
-
{ code: 2000,
|
|
31
|
-
{ code: 2001,
|
|
32
|
-
{ code: 2002,
|
|
33
|
-
{ code: 2003,
|
|
34
|
-
{ code: 2004,
|
|
35
|
-
{ code: 2005,
|
|
36
|
-
{ code: 2006,
|
|
37
|
-
{ code: 2007,
|
|
38
|
-
{ code: 2008,
|
|
39
|
-
{ code: 2009,
|
|
40
|
-
{ code: 2010,
|
|
41
|
-
{ code: 2011,
|
|
42
|
-
{ code: 2012,
|
|
43
|
-
{ code: 2013,
|
|
44
|
-
{ code: 2014,
|
|
45
|
-
{ code: 2015,
|
|
46
|
-
{ code: 2016,
|
|
47
|
-
{ code: 2017,
|
|
48
|
-
{ code: 2018,
|
|
49
|
-
{ code: 2019,
|
|
50
|
-
{ code: 2020,
|
|
51
|
-
{ code: 2021,
|
|
52
|
-
{ code: 2022,
|
|
53
|
-
{ code: 4001,
|
|
23
|
+
{ code: 1000, message: 'Unknown server-side error; contact support with the request id', retryable: true, exitCode: EXIT.NETWORK },
|
|
24
|
+
{ code: 1001, message: 'Fatal server-side error; contact support with the request id', retryable: false, exitCode: EXIT.NETWORK },
|
|
25
|
+
{ code: 1002, message: 'Authentication failed: invalid key or wrong region (cn key against the ov API?) — run "tripo doctor" to auto-diagnose', retryable: false, exitCode: EXIT.AUTH },
|
|
26
|
+
{ code: 1003, message: 'The request body is malformed', retryable: false, exitCode: EXIT.USAGE },
|
|
27
|
+
{ code: 1004, message: 'One or more parameters are invalid (check the docs)', retryable: false, exitCode: EXIT.USAGE },
|
|
28
|
+
{ code: 1005, message: 'Not allowed to access this resource (check permissions)', retryable: false, exitCode: EXIT.AUTH },
|
|
29
|
+
{ code: 1007, message: 'Rate limit exceeded — too many requests in a short time', retryable: true, exitCode: EXIT.RATE_LIMIT },
|
|
30
|
+
{ code: 2000, message: 'Generation concurrency limit hit (task families share a pool)', retryable: true, exitCode: EXIT.RATE_LIMIT },
|
|
31
|
+
{ code: 2001, message: 'Task not found (same key that created it? right region?)', retryable: false, exitCode: EXIT.NOT_FOUND },
|
|
32
|
+
{ code: 2002, message: 'Unsupported request parameter (check field names and values)', retryable: false, exitCode: EXIT.USAGE },
|
|
33
|
+
{ code: 2003, message: 'The input file is empty (corrupted upload?)', retryable: false, exitCode: EXIT.USAGE },
|
|
34
|
+
{ code: 2004, message: 'Unsupported file type', retryable: false, exitCode: EXIT.USAGE },
|
|
35
|
+
{ code: 2005, message: 'The draft task is not a success; retry with a successful one', retryable: false, exitCode: EXIT.USAGE },
|
|
36
|
+
{ code: 2006, message: 'The original task type is not supported for this operation', retryable: false, exitCode: EXIT.USAGE },
|
|
37
|
+
{ code: 2007, message: 'The original task has not succeeded', retryable: false, exitCode: EXIT.USAGE },
|
|
38
|
+
{ code: 2008, message: 'Input violates the content policy; change the input', retryable: false, exitCode: EXIT.CONTENT_POLICY },
|
|
39
|
+
{ code: 2009, message: 'The prompt contains invalid characters', retryable: false, exitCode: EXIT.USAGE },
|
|
40
|
+
{ code: 2010, message: 'Insufficient credits', retryable: false, exitCode: EXIT.BALANCE },
|
|
41
|
+
{ code: 2011, message: 'The rig-check input task has no model output', retryable: false, exitCode: EXIT.USAGE },
|
|
42
|
+
{ code: 2012, message: 'retarget only accepts a rig task as input', retryable: false, exitCode: EXIT.USAGE },
|
|
43
|
+
{ code: 2013, message: 'Invalid priority (contact support for higher priority)', retryable: false, exitCode: EXIT.USAGE },
|
|
44
|
+
{ code: 2014, message: 'Error occurred during content auditing; contact support', retryable: false, exitCode: EXIT.FATAL },
|
|
45
|
+
{ code: 2015, message: 'Model version deprecated; upgrade the model parameter', retryable: false, exitCode: EXIT.USAGE },
|
|
46
|
+
{ code: 2016, message: 'This request type is deprecated; use an alternative', retryable: false, exitCode: EXIT.USAGE },
|
|
47
|
+
{ code: 2017, message: 'The model version value is invalid', retryable: false, exitCode: EXIT.USAGE },
|
|
48
|
+
{ code: 2018, message: 'Model too complex to remesh; lower face_limit or simplify input', retryable: false, exitCode: EXIT.USAGE },
|
|
49
|
+
{ code: 2019, message: 'File not found (upload succeeded? token expired?)', retryable: false, exitCode: EXIT.NOT_FOUND },
|
|
50
|
+
{ code: 2020, message: 'The image URL is invalid (must be publicly reachable)', retryable: false, exitCode: EXIT.USAGE },
|
|
51
|
+
{ code: 2021, message: 'File size exceeds the limit; reduce and retry', retryable: false, exitCode: EXIT.USAGE },
|
|
52
|
+
{ code: 2022, message: 'Image size exceeds the limit; reduce and retry', retryable: false, exitCode: EXIT.USAGE },
|
|
53
|
+
{ code: 4001, message: 'The requested endpoint does not exist (check the API path)', retryable: false, exitCode: EXIT.NOT_FOUND },
|
|
54
54
|
];
|
|
55
55
|
const byCode = new Map(ERROR_CATALOG.map((e) => [e.code, e]));
|
|
56
56
|
export function lookupError(code) {
|
|
@@ -58,13 +58,13 @@ export function lookupError(code) {
|
|
|
58
58
|
}
|
|
59
59
|
/** Map an API error code + HTTP status to a stable CLI exit code. */
|
|
60
60
|
export function exitCodeFor(apiCode, httpStatus) {
|
|
61
|
+
if (httpStatus === 401)
|
|
62
|
+
return EXIT.AUTH;
|
|
61
63
|
if (apiCode !== undefined) {
|
|
62
64
|
const info = byCode.get(apiCode);
|
|
63
65
|
if (info)
|
|
64
66
|
return info.exitCode;
|
|
65
67
|
}
|
|
66
|
-
if (httpStatus === 401)
|
|
67
|
-
return EXIT.AUTH;
|
|
68
68
|
if (httpStatus === 403)
|
|
69
69
|
return EXIT.AUTH;
|
|
70
70
|
if (httpStatus === 404)
|
|
@@ -78,6 +78,8 @@ export function exitCodeFor(apiCode, httpStatus) {
|
|
|
78
78
|
return EXIT.FATAL;
|
|
79
79
|
}
|
|
80
80
|
export function isRetryable(apiCode, httpStatus) {
|
|
81
|
+
if (httpStatus === 401)
|
|
82
|
+
return false;
|
|
81
83
|
if (apiCode !== undefined) {
|
|
82
84
|
const info = byCode.get(apiCode);
|
|
83
85
|
if (info)
|