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/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 { applyTextureV35, 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,7 +33,7 @@ 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,
|
|
@@ -62,7 +62,7 @@ export function buildMultiviewToModel(views, model, params) {
|
|
|
62
62
|
return {
|
|
63
63
|
request: {
|
|
64
64
|
endpoint: '/v3/generation/multiview-to-model',
|
|
65
|
-
payload: { inputs, model: normalizeModel(model)
|
|
65
|
+
payload: { inputs, ...validated.params, model: normalizeModel(model) },
|
|
66
66
|
},
|
|
67
67
|
taskType: 'multiview_to_model',
|
|
68
68
|
warnings: validated.warnings,
|
|
@@ -72,28 +72,38 @@ export function buildMultiviewToModel(views, model, params) {
|
|
|
72
72
|
* Server task types (TaskType enum): text-to-image → `text_to_image`,
|
|
73
73
|
* image-to-image → `generate_image`. */
|
|
74
74
|
export function buildTextToImage(prompt, params) {
|
|
75
|
+
const validated = validateImageParams(params, false);
|
|
76
|
+
ensure(validated);
|
|
75
77
|
return {
|
|
76
78
|
request: {
|
|
77
79
|
endpoint: '/v3/generation/text-to-image',
|
|
78
|
-
payload: { prompt,
|
|
80
|
+
payload: { prompt, ...validated.params },
|
|
79
81
|
},
|
|
80
82
|
taskType: 'text_to_image',
|
|
81
|
-
warnings:
|
|
83
|
+
warnings: validated.warnings,
|
|
82
84
|
};
|
|
83
85
|
}
|
|
84
86
|
/** One reference image goes as `input`, several as `inputs` (prompt cites them as image[1], image[2], ...). */
|
|
85
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
|
+
}
|
|
86
97
|
return {
|
|
87
98
|
request: {
|
|
88
99
|
endpoint: '/v3/generation/image-to-image',
|
|
89
100
|
payload: {
|
|
90
101
|
...(inputs.length > 1 ? { inputs } : { input: inputs[0] }),
|
|
91
|
-
|
|
92
|
-
...omit(params, ['model']),
|
|
102
|
+
...validated.params,
|
|
93
103
|
},
|
|
94
104
|
},
|
|
95
105
|
taskType: 'generate_image',
|
|
96
|
-
warnings:
|
|
106
|
+
warnings: validated.warnings,
|
|
97
107
|
};
|
|
98
108
|
}
|
|
99
109
|
export function buildImageToMultiview(input, params) {
|
|
@@ -117,6 +127,27 @@ export function buildImageToSplat(input, params) {
|
|
|
117
127
|
warnings: [],
|
|
118
128
|
};
|
|
119
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
|
+
}
|
|
120
151
|
/**
|
|
121
152
|
* Processing step (texture/convert/rig/...): validate per step, then build.
|
|
122
153
|
* `input` must already be a task_id / file_token / URL.
|
|
@@ -161,6 +192,25 @@ export function buildStepRequest(stepName, input, args) {
|
|
|
161
192
|
params.model = params.model === undefined
|
|
162
193
|
? CAPABILITY_DEFAULTS.rig
|
|
163
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
|
+
}
|
|
164
214
|
break;
|
|
165
215
|
}
|
|
166
216
|
case 'texture': {
|
|
@@ -209,9 +259,3 @@ export function buildStepRequest(stepName, input, args) {
|
|
|
209
259
|
warnings,
|
|
210
260
|
};
|
|
211
261
|
}
|
|
212
|
-
function omit(obj, keys) {
|
|
213
|
-
const out = { ...obj };
|
|
214
|
-
for (const k of keys)
|
|
215
|
-
delete out[k];
|
|
216
|
-
return out;
|
|
217
|
-
}
|
|
@@ -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,
|
|
@@ -144,12 +144,12 @@ export async function runTask(options) {
|
|
|
144
144
|
if (!options.noDownload) {
|
|
145
145
|
const baseDir = options.outDir ?? loadContext().output_dir ?? process.cwd();
|
|
146
146
|
const dir = defaultOutputDir(baseDir, name, taskId);
|
|
147
|
-
const dlSpinner = createSpinner(
|
|
147
|
+
const dlSpinner = createSpinner('downloading artifacts…', isQuiet());
|
|
148
148
|
try {
|
|
149
149
|
const artifacts = await downloadArtifacts(task, dir, entry, {
|
|
150
|
-
onFile: (f) => dlSpinner.update(
|
|
150
|
+
onFile: (f) => dlSpinner.update(`downloading ${f}`),
|
|
151
151
|
});
|
|
152
|
-
dlSpinner.succeed(
|
|
152
|
+
dlSpinner.succeed(`artifacts saved to ${artifacts.outputDir}`);
|
|
153
153
|
result.output_dir = artifacts.outputDir;
|
|
154
154
|
result.files = artifacts.files.map((f) => basename(f));
|
|
155
155
|
result.model_file = artifacts.modelFile;
|
|
@@ -157,13 +157,12 @@ export async function runTask(options) {
|
|
|
157
157
|
updateHistoryStatus(taskId, 'success', { output_dir: artifacts.outputDir });
|
|
158
158
|
}
|
|
159
159
|
catch (err) {
|
|
160
|
-
dlSpinner.fail(
|
|
161
|
-
|
|
162
|
-
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));
|
|
163
162
|
}
|
|
164
163
|
}
|
|
165
164
|
if (options.notify) {
|
|
166
|
-
notifyDone('Tripo',
|
|
165
|
+
notifyDone('Tripo', `${taskType} finished`);
|
|
167
166
|
}
|
|
168
167
|
return result;
|
|
169
168
|
}
|
package/dist/knowledge/chains.js
CHANGED
|
@@ -22,22 +22,22 @@ export const MODEL_PRODUCING_TYPES = new Set([
|
|
|
22
22
|
'animate_retarget',
|
|
23
23
|
]);
|
|
24
24
|
export const STEPS = [
|
|
25
|
-
{ name: 'texture', endpoint: '/v3/models/texture', produces: 'texture_model', accepts: 'any-model', acceptsFile: true, describe:
|
|
26
|
-
{ name: 'convert', endpoint: '/v3/models/convert', produces: 'convert_model', accepts: 'any-model', acceptsFile: true, describe:
|
|
27
|
-
{ name: 'import', endpoint: '/v3/models/import', produces: 'import_model', accepts: [], acceptsFile: true, describe:
|
|
28
|
-
{ name: 'rig-check', endpoint: '/v3/animations/rig-check', produces: 'animate_prerigcheck', accepts: 'any-model', acceptsFile: true, passthrough: true, aliases: ['check', 'rigcheck'], describe:
|
|
29
|
-
{ name: 'rig', endpoint: '/v3/animations/rig', produces: 'animate_rig', accepts: 'any-model', acceptsFile: true, describe:
|
|
30
|
-
{ name: 'retarget', endpoint: '/v3/animations/retarget', produces: 'animate_retarget', accepts: ['animate_rig'], acceptsFile: false, aliases: ['animate'], describe:
|
|
31
|
-
{ name: 'segment', endpoint: '/v3/mesh/segment', produces: 'mesh_segmentation', accepts: 'any-model', acceptsFile: true, describe:
|
|
32
|
-
{ name: 'complete', endpoint: '/v3/mesh/complete', produces: 'mesh_completion', accepts: ['mesh_segmentation'], acceptsFile: false, describe:
|
|
33
|
-
{ name: 'decimate', endpoint: '/v3/mesh/decimate', produces: 'highpoly_to_lowpoly', accepts: 'any-model', acceptsFile: true, aliases: ['lowpoly'], describe:
|
|
34
|
-
{ name: 'uv', endpoint: '/v3/mesh/uv', produces: 'mesh_uv', accepts: 'any-model', acceptsFile: true, describe:
|
|
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' },
|
|
35
35
|
// mesh/edit: local retry/refine inside a bbox, P2 pipeline (model fixed to P2-20260801).
|
|
36
|
-
{ 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)' },
|
|
37
37
|
// smartsegment's `input` is file_token/URL only (the server does not resolve
|
|
38
38
|
// task ids here), so it cannot chain after a task. Type is refined to
|
|
39
39
|
// smartsegment_image|smartsegment_model at request-build time per seg_type.
|
|
40
|
-
{ 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' },
|
|
41
41
|
];
|
|
42
42
|
const stepIndex = new Map();
|
|
43
43
|
for (const s of STEPS) {
|
|
@@ -1,5 +1,5 @@
|
|
|
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,
|
|
@@ -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) {
|
package/dist/knowledge/models.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Model routing — the single source of truth for 3D generation selection.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* H3.1 is the official default (flagship, highest fidelity) and also covers dense
|
|
5
|
+
* geometry and H-only parameters. Real-time, editable and topology-constrained
|
|
6
|
+
* assets route to P2.
|
|
7
7
|
*/
|
|
8
8
|
/**
|
|
9
9
|
* Wire values are what the server actually accepts (backend whitelist:
|
|
@@ -13,24 +13,28 @@
|
|
|
13
13
|
*/
|
|
14
14
|
export const MODEL_V31 = 'v3.1-20260211';
|
|
15
15
|
export const MODEL_P1 = 'P1-20260311';
|
|
16
|
-
/** P-series
|
|
16
|
+
/** P-series model: editable low-poly topology and quad output. */
|
|
17
17
|
export const MODEL_P2 = 'P2-20260801';
|
|
18
|
-
/** User-facing aliases normalized to the wire values above. */
|
|
18
|
+
/** User-facing aliases normalized to the wire values above (matched case-insensitively). */
|
|
19
19
|
export const MODEL_ALIASES = {
|
|
20
|
+
'H3.1': MODEL_V31,
|
|
21
|
+
'P1.0': MODEL_P1,
|
|
22
|
+
'P2.0': MODEL_P2,
|
|
23
|
+
'P2.0 Preview': MODEL_P2,
|
|
20
24
|
'v3.1': MODEL_V31,
|
|
21
25
|
'v3.1-20260211': MODEL_V31,
|
|
22
26
|
'tripo-v3.1': MODEL_V31,
|
|
23
|
-
p1: MODEL_P1,
|
|
24
27
|
P1: MODEL_P1,
|
|
25
28
|
'P1-20260311': MODEL_P1,
|
|
26
29
|
'tripo-p1': MODEL_P1,
|
|
27
|
-
p2: MODEL_P2,
|
|
28
30
|
P2: MODEL_P2,
|
|
29
31
|
'P2-20260801': MODEL_P2,
|
|
30
32
|
'tripo-p2': MODEL_P2,
|
|
31
33
|
// Older whitelisted versions, alias → wire.
|
|
34
|
+
'H3.0': 'v3.0-20250812',
|
|
32
35
|
'v3.0': 'v3.0-20250812',
|
|
33
36
|
'tripo-v3.0': 'v3.0-20250812',
|
|
37
|
+
'H2.5': 'v2.5-20250123',
|
|
34
38
|
'v2.5': 'v2.5-20250123',
|
|
35
39
|
'tripo-v2.5': 'v2.5-20250123',
|
|
36
40
|
};
|
|
@@ -105,7 +109,6 @@ export const TEXT_TO_IMAGE_MODELS = [
|
|
|
105
109
|
'banana',
|
|
106
110
|
'banana_pro',
|
|
107
111
|
'banana2',
|
|
108
|
-
'chat_image_1',
|
|
109
112
|
'chat_image_1.5',
|
|
110
113
|
'chat_image_2',
|
|
111
114
|
'chat_image_2.5_flare',
|
|
@@ -116,7 +119,6 @@ export const IMAGE_TO_IMAGE_MODELS = [
|
|
|
116
119
|
'banana',
|
|
117
120
|
'banana_pro',
|
|
118
121
|
'banana2',
|
|
119
|
-
'chat_image_1',
|
|
120
122
|
'chat_image_1.5',
|
|
121
123
|
'chat_image_2',
|
|
122
124
|
'chat_image_2.5_flare',
|
|
@@ -161,9 +163,10 @@ export const FBX_PRESETS = ['blender', '3dsmax', 'mixamo', 'bake_scale'];
|
|
|
161
163
|
export const EXPORT_ORIENTATIONS = ['+x', '-x', '+y', '-y'];
|
|
162
164
|
/** `fast` only works with texture model v3.5 (other versions → 1004). */
|
|
163
165
|
export const TEXTURE_QUALITIES = ['fast', 'standard', 'detailed', 'extreme'];
|
|
164
|
-
|
|
166
|
+
const MODEL_ALIAS_LOOKUP = new Map(Object.entries(MODEL_ALIASES).map(([alias, wire]) => [alias.toLowerCase(), wire]));
|
|
167
|
+
/** Normalize any accepted alias to the canonical wire value; unknown values pass through for validation to reject. */
|
|
165
168
|
export function normalizeModel(input) {
|
|
166
|
-
return
|
|
169
|
+
return MODEL_ALIAS_LOOKUP.get(input.trim().toLowerCase()) ?? input;
|
|
167
170
|
}
|
|
168
171
|
export function isP1(model) {
|
|
169
172
|
return normalizeModel(model) === MODEL_P1;
|
|
@@ -171,21 +174,117 @@ export function isP1(model) {
|
|
|
171
174
|
export function isP2(model) {
|
|
172
175
|
return normalizeModel(model) === MODEL_P2;
|
|
173
176
|
}
|
|
174
|
-
|
|
177
|
+
function hRequirement(intent) {
|
|
178
|
+
const params = intent.params ?? {};
|
|
179
|
+
const quad = intent.quad === true || params.quad === true;
|
|
180
|
+
if (intent.denseGeometry)
|
|
181
|
+
return 'dense geometry';
|
|
182
|
+
if (params.geometry_quality !== undefined)
|
|
183
|
+
return 'geometry_quality';
|
|
184
|
+
if (params.smart_low_poly === true)
|
|
185
|
+
return 'smart_low_poly';
|
|
186
|
+
if (params.generate_parts === true)
|
|
187
|
+
return 'generate_parts';
|
|
188
|
+
if (typeof intent.faceLimit === 'number' && intent.faceLimit > (quad ? P2_FACE_LIMIT.quadMax : P2_FACE_LIMIT.triMax)) {
|
|
189
|
+
return `face budget exceeds P2 ${quad ? 'quad' : 'triangle'} limit`;
|
|
190
|
+
}
|
|
191
|
+
if (typeof intent.faceLimit === 'number' && intent.faceLimit > 0 && intent.faceLimit < P2_FACE_LIMIT.min) {
|
|
192
|
+
return 'face budget below P2 minimum';
|
|
193
|
+
}
|
|
194
|
+
return undefined;
|
|
195
|
+
}
|
|
196
|
+
/** Select a generation model from explicit choice, geometry requirements and intended use. */
|
|
175
197
|
export function chooseModel(intent) {
|
|
176
|
-
|
|
177
|
-
|
|
198
|
+
const paramModel = typeof intent.params?.model === 'string' ? intent.params.model : undefined;
|
|
199
|
+
const explicit = intent.explicit ?? paramModel;
|
|
200
|
+
if (explicit) {
|
|
201
|
+
return {
|
|
202
|
+
model: normalizeModel(explicit),
|
|
203
|
+
reason: intent.explicit ? 'explicit --model' : 'explicit model parameter',
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
const hOnly = hRequirement(intent);
|
|
207
|
+
if (hOnly) {
|
|
208
|
+
return { model: MODEL_V31, reason: `${hOnly} → v3.1` };
|
|
178
209
|
}
|
|
179
|
-
if (intent.
|
|
180
|
-
|
|
210
|
+
if (intent.preferred) {
|
|
211
|
+
const preferred = normalizeModel(intent.preferred);
|
|
212
|
+
if (preferred !== MODEL_P1)
|
|
213
|
+
return { model: preferred, reason: 'scenario preference' };
|
|
181
214
|
}
|
|
182
|
-
|
|
183
|
-
|
|
215
|
+
const params = intent.params ?? {};
|
|
216
|
+
if (intent.lowPoly || intent.realtime || intent.editable || intent.quad === true || params.quad === true || intent.faceLimit !== undefined) {
|
|
217
|
+
return { model: MODEL_P2, reason: 'real-time or editable topology → P2' };
|
|
184
218
|
}
|
|
185
|
-
return { model: MODEL_V31, reason: 'default
|
|
219
|
+
return { model: MODEL_V31, reason: 'official default → v3.1' };
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Official list price per generation task in credits, identical on the ov and cn
|
|
223
|
+
* model pages. Tiers: [no texture, standard/fast, detailed, extreme]. P1 and the
|
|
224
|
+
* legacy models publish no price, so they are never estimated.
|
|
225
|
+
*/
|
|
226
|
+
const GENERATION_CREDITS = {
|
|
227
|
+
[MODEL_V31]: {
|
|
228
|
+
text_to_model: [10, 20, 30, 40],
|
|
229
|
+
image_to_model: [20, 30, 40, 50],
|
|
230
|
+
multiview_to_model: [20, 30, 40, 50],
|
|
231
|
+
},
|
|
232
|
+
[MODEL_P2]: {
|
|
233
|
+
text_to_model: [100, 110, 120, 130],
|
|
234
|
+
image_to_model: [100, 110, 120, 130],
|
|
235
|
+
multiview_to_model: [100, 110, 120, 130],
|
|
236
|
+
},
|
|
237
|
+
};
|
|
238
|
+
/** List-price credits for one generation task from its validated payload; undefined when no price is published. */
|
|
239
|
+
export function estimateGenerationCredits(model, taskType, params) {
|
|
240
|
+
const wire = normalizeModel(model);
|
|
241
|
+
const tiers = GENERATION_CREDITS[wire]?.[taskType];
|
|
242
|
+
if (!tiers)
|
|
243
|
+
return undefined;
|
|
244
|
+
let credits = tiers[params.texture === false ? 0 : params.texture_quality === 'extreme' ? 3 : params.texture_quality === 'detailed' ? 2 : 1];
|
|
245
|
+
if (wire === MODEL_V31) {
|
|
246
|
+
if (params.geometry_quality === 'detailed')
|
|
247
|
+
credits += 20;
|
|
248
|
+
if (params.quad === true)
|
|
249
|
+
credits += 5;
|
|
250
|
+
if (params.smart_low_poly === true)
|
|
251
|
+
credits += 10;
|
|
252
|
+
if (params.generate_parts === true)
|
|
253
|
+
credits += 20;
|
|
254
|
+
}
|
|
255
|
+
return credits;
|
|
186
256
|
}
|
|
187
257
|
/** Keywords that signal low-poly intent in a prompt (used by make/ai). */
|
|
188
258
|
const LOW_POLY_HINTS = [/low[\s_-]?poly/i, /低模/, /低面/, /低多边形/, /手游/, /voxel/i, /像素风/];
|
|
259
|
+
/** Physical games, product nouns (gaming PC, console) and animation *styles* are not real-time intent. */
|
|
260
|
+
const REALTIME_HINTS = [
|
|
261
|
+
/\b(?:(?<!\b(?:board|card|tabletop|dice|party)[\s_-]?)games?|gameplay|real[\s_-]?time|unity|unreal(?:\s+engine)?|godot|three\.?js|webgl)\b/i,
|
|
262
|
+
/\b(?:editable|edit[\s_-]?friendly|topology|retopo(?:logy)?|quad(?:s)?|rig(?:[\s_-]?ready|ged|ging)?|animat(?:e|ions?)(?![\s_-]?styl))\b/i,
|
|
263
|
+
/(?<!棋盘|桌面)游戏|手游|端游|实时|引擎|可编辑|可修改|拓扑|重拓扑|四边面|四边形|绑定|骨骼|动画(?![风片])|虚幻/,
|
|
264
|
+
];
|
|
265
|
+
const DENSE_GEOMETRY_HINTS = [
|
|
266
|
+
/\b(?:dense|high[\s_-]?density|high[\s_-]?detail(?:ed)?)\s+(?:mesh|geometry|topology)\b/i,
|
|
267
|
+
/\bhigh[\s_-]?poly(?:gon(?:al)?)?\b/i,
|
|
268
|
+
/\bnanite\b/i,
|
|
269
|
+
/\b(?:million|millions)\s+of\s+(?:faces|triangles|polygons)\b/i,
|
|
270
|
+
/高模|高面数|高多边形|高密度(?:网格|几何)|密集(?:网格|几何)|稠密(?:网格|几何)|高精度(?:网格|几何)|百万(?:面|三角面)/,
|
|
271
|
+
];
|
|
189
272
|
export function detectLowPolyIntent(text) {
|
|
190
273
|
return LOW_POLY_HINTS.some((re) => re.test(text));
|
|
191
274
|
}
|
|
275
|
+
/** Detect uses that benefit from P-series topology without treating visual quality as mesh density. */
|
|
276
|
+
export function detectRealtimeIntent(text) {
|
|
277
|
+
return REALTIME_HINTS.some((re) => re.test(text));
|
|
278
|
+
}
|
|
279
|
+
/** Detect only explicit dense-geometry requirements; phrases such as "high quality" do not match. */
|
|
280
|
+
export function detectDenseGeometryIntent(text) {
|
|
281
|
+
// Match each mention independently: negating one must not hide a later positive requirement.
|
|
282
|
+
return DENSE_GEOMETRY_HINTS.some((re) => Array.from(text.matchAll(new RegExp(re.source, `${re.flags}g`))).some((match) => {
|
|
283
|
+
const before = text.slice(0, match.index);
|
|
284
|
+
const after = text.slice(match.index + match[0].length);
|
|
285
|
+
const negatedBefore = /(?:\b(?:no|not|without|avoid|exclude)\s+(?:(?:a|an|any|the)\s+)?|\b(?:do\s+not|don't|does\s+not|doesn't)\s+(?:need|want|require|use)\s+(?:(?:a|an|any|the)\s+)?|\bno\s+need\s+for\s+(?:(?:a|an|any|the)\s+)?|(?:不要|不需要|无需|不用|不使用|避免|非)\s*)$/i.test(before);
|
|
286
|
+
const negatedAfter = /^\s*(?:(?:mesh|geometry|topology)\s+)?(?:is\s+not|isn't|not)\s+(?:needed|required|wanted)\b/i.test(after)
|
|
287
|
+
|| /^\s*(?:不需要|不要|无需)/.test(after);
|
|
288
|
+
return !negatedBefore && !negatedAfter;
|
|
289
|
+
}));
|
|
290
|
+
}
|