tripo-cli 0.1.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/README.md +105 -0
- package/dist/ai/experts.js +115 -0
- package/dist/ai/llm.js +88 -0
- package/dist/ai/plan-card.js +79 -0
- package/dist/ai/wizard.js +105 -0
- package/dist/cli.js +7 -0
- package/dist/commands/account.js +27 -0
- package/dist/commands/ai.js +114 -0
- package/dist/commands/batch.js +148 -0
- package/dist/commands/completion.js +84 -0
- package/dist/commands/config-cmd.js +84 -0
- package/dist/commands/docs.js +30 -0
- package/dist/commands/doctor.js +102 -0
- package/dist/commands/generate.js +81 -0
- package/dist/commands/home.js +68 -0
- package/dist/commands/login.js +309 -0
- package/dist/commands/make.js +142 -0
- package/dist/commands/mcp.js +194 -0
- package/dist/commands/process-groups.js +166 -0
- package/dist/commands/shared.js +75 -0
- package/dist/commands/task.js +128 -0
- package/dist/commands/topup.js +61 -0
- package/dist/commands/view.js +97 -0
- package/dist/config/config.js +99 -0
- package/dist/config/context.js +44 -0
- package/dist/config/history.js +104 -0
- package/dist/core/client.js +129 -0
- package/dist/core/device-auth.js +141 -0
- package/dist/core/download.js +107 -0
- package/dist/core/errors.js +64 -0
- package/dist/core/output.js +72 -0
- package/dist/core/region.js +45 -0
- package/dist/core/requests.js +197 -0
- package/dist/core/task-service.js +167 -0
- package/dist/core/upload.js +42 -0
- package/dist/core/viewer-assets.js +24 -0
- package/dist/core/viewer.js +59 -0
- package/dist/core/watch.js +42 -0
- package/dist/i18n.js +19 -0
- package/dist/knowledge/chains.js +157 -0
- package/dist/knowledge/error-catalog.js +91 -0
- package/dist/knowledge/models.js +158 -0
- package/dist/knowledge/params.js +203 -0
- package/dist/knowledge/scenarios.js +112 -0
- package/dist/pipeline/make.js +233 -0
- package/dist/pipeline/runner.js +73 -0
- package/dist/pipeline/stdin.js +40 -0
- package/dist/program.js +147 -0
- package/dist/src/ai/experts.js +115 -0
- package/dist/src/ai/llm.js +88 -0
- package/dist/src/ai/plan-card.js +79 -0
- package/dist/src/ai/wizard.js +104 -0
- package/dist/src/cli.js +7 -0
- package/dist/src/commands/account.js +27 -0
- package/dist/src/commands/ai.js +113 -0
- package/dist/src/commands/batch.js +148 -0
- package/dist/src/commands/completion.js +84 -0
- package/dist/src/commands/config-cmd.js +84 -0
- package/dist/src/commands/docs.js +30 -0
- package/dist/src/commands/doctor.js +94 -0
- package/dist/src/commands/generate.js +81 -0
- package/dist/src/commands/home.js +68 -0
- package/dist/src/commands/login.js +225 -0
- package/dist/src/commands/make.js +142 -0
- package/dist/src/commands/mcp.js +194 -0
- package/dist/src/commands/process-groups.js +164 -0
- package/dist/src/commands/shared.js +75 -0
- package/dist/src/commands/task.js +127 -0
- package/dist/src/commands/topup.js +61 -0
- package/dist/src/commands/view.js +97 -0
- package/dist/src/config/config.js +79 -0
- package/dist/src/config/context.js +44 -0
- package/dist/src/config/history.js +104 -0
- package/dist/src/core/client.js +120 -0
- package/dist/src/core/download.js +107 -0
- package/dist/src/core/errors.js +64 -0
- package/dist/src/core/output.js +72 -0
- package/dist/src/core/requests.js +169 -0
- package/dist/src/core/task-service.js +161 -0
- package/dist/src/core/upload.js +36 -0
- package/dist/src/core/viewer-assets.js +24 -0
- package/dist/src/core/viewer.js +59 -0
- package/dist/src/core/watch.js +41 -0
- package/dist/src/i18n.js +19 -0
- package/dist/src/knowledge/chains.js +152 -0
- package/dist/src/knowledge/error-catalog.js +91 -0
- package/dist/src/knowledge/models.js +110 -0
- package/dist/src/knowledge/params.js +174 -0
- package/dist/src/knowledge/scenarios.js +112 -0
- package/dist/src/pipeline/make.js +233 -0
- package/dist/src/pipeline/runner.js +73 -0
- package/dist/src/pipeline/stdin.js +40 -0
- package/dist/src/program.js +147 -0
- package/dist/src/types.js +2 -0
- package/dist/src/util/input-detect.js +92 -0
- package/dist/src/util/notify.js +17 -0
- package/dist/src/util/open.js +18 -0
- package/dist/src/util/spinner.js +62 -0
- package/dist/src/util/term-image.js +28 -0
- package/dist/src/version.js +10 -0
- package/dist/types.js +2 -0
- package/dist/util/input-detect.js +93 -0
- package/dist/util/notify.js +17 -0
- package/dist/util/open.js +18 -0
- package/dist/util/spinner.js +62 -0
- package/dist/util/term-image.js +28 -0
- package/dist/version.js +10 -0
- package/package.json +63 -0
- package/skill/SKILL.md +70 -0
- package/skill/commands/account.md +41 -0
- package/skill/commands/batch.md +33 -0
- package/skill/commands/generate.md +34 -0
- package/skill/commands/make.md +47 -0
- package/skill/commands/process.md +50 -0
- package/skill/commands/task.md +27 -0
- package/skill/commands/view.md +25 -0
- package/skill/common-errors.md +38 -0
- package/skill/examples/animation.md +33 -0
- package/skill/examples/ar-web.md +23 -0
- package/skill/examples/film.md +22 -0
- package/skill/examples/game-asset.md +35 -0
- package/skill/examples/pipes.md +39 -0
- package/skill/examples/print.md +31 -0
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Task-chain legality — which processing step may consume which upstream task.
|
|
3
|
+
* Mirrors server-side handler constraints; violations are rejected locally
|
|
4
|
+
* before any credits are spent.
|
|
5
|
+
*/
|
|
6
|
+
/** Task types that produce a downloadable 3D model. */
|
|
7
|
+
export const MODEL_PRODUCING_TYPES = new Set([
|
|
8
|
+
'text_to_model',
|
|
9
|
+
'image_to_model',
|
|
10
|
+
'multiview_to_model',
|
|
11
|
+
'refine_model',
|
|
12
|
+
'texture_model',
|
|
13
|
+
'stylize_model',
|
|
14
|
+
'convert_model',
|
|
15
|
+
'import_model',
|
|
16
|
+
'highpoly_to_lowpoly',
|
|
17
|
+
'mesh_segmentation',
|
|
18
|
+
'mesh_completion',
|
|
19
|
+
'animate_rig',
|
|
20
|
+
'animate_retarget',
|
|
21
|
+
]);
|
|
22
|
+
export const STEPS = [
|
|
23
|
+
{ name: 'refine', endpoint: '/v3/models/refine', produces: 'refine_model', accepts: ['text_to_model'], acceptsFile: false },
|
|
24
|
+
{ name: 'texture', endpoint: '/v3/models/texture', produces: 'texture_model', accepts: 'any-model', acceptsFile: true },
|
|
25
|
+
{ name: 'stylize', endpoint: '/v3/models/stylize', produces: 'stylize_model', accepts: 'any-model', acceptsFile: true },
|
|
26
|
+
{ name: 'convert', endpoint: '/v3/models/convert', produces: 'convert_model', accepts: 'any-model', acceptsFile: true },
|
|
27
|
+
{ name: 'import', endpoint: '/v3/models/import', produces: 'import_model', accepts: [], acceptsFile: true },
|
|
28
|
+
{ name: 'rig-check', endpoint: '/v3/animations/rig-check', produces: 'animate_prerigcheck', accepts: 'any-model', acceptsFile: true, passthrough: true, aliases: ['check', 'rigcheck'] },
|
|
29
|
+
{ name: 'rig', endpoint: '/v3/animations/rig', produces: 'animate_rig', accepts: 'any-model', acceptsFile: true },
|
|
30
|
+
{ name: 'retarget', endpoint: '/v3/animations/retarget', produces: 'animate_retarget', accepts: ['animate_rig'], acceptsFile: false, aliases: ['animate'] },
|
|
31
|
+
{ name: 'segment', endpoint: '/v3/mesh/segment', produces: 'mesh_segmentation', accepts: 'any-model', acceptsFile: true },
|
|
32
|
+
{ name: 'complete', endpoint: '/v3/mesh/complete', produces: 'mesh_completion', accepts: ['mesh_segmentation'], acceptsFile: false },
|
|
33
|
+
{ name: 'decimate', endpoint: '/v3/mesh/decimate', produces: 'highpoly_to_lowpoly', accepts: 'any-model', acceptsFile: true, aliases: ['lowpoly'] },
|
|
34
|
+
// smartsegment's `input` is file_token/URL only (the server does not resolve
|
|
35
|
+
// task ids here), so it cannot chain after a task. Type is refined to
|
|
36
|
+
// smartsegment_image|smartsegment_model at request-build time per seg_type.
|
|
37
|
+
{ name: 'smartsegment', endpoint: '/v3/mesh/smartsegment', produces: 'smartsegment_model', accepts: [], acceptsFile: true },
|
|
38
|
+
];
|
|
39
|
+
const stepIndex = new Map();
|
|
40
|
+
for (const s of STEPS) {
|
|
41
|
+
stepIndex.set(s.name, s);
|
|
42
|
+
for (const a of s.aliases ?? [])
|
|
43
|
+
stepIndex.set(a, s);
|
|
44
|
+
}
|
|
45
|
+
export function getStep(name) {
|
|
46
|
+
return stepIndex.get(name);
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Parse a --then expression: "texture,rig,convert:fbx" or
|
|
50
|
+
* "convert:format=fbx,texture_size=2048". A bare value after ":" is sugar for
|
|
51
|
+
* the step's primary argument (convert→format, retarget→animation,
|
|
52
|
+
* stylize→style, decimate→face_limit).
|
|
53
|
+
*/
|
|
54
|
+
export function parseThenExpression(expr) {
|
|
55
|
+
const out = [];
|
|
56
|
+
// Split on commas that are not inside a step's arg list (args use "=", so a
|
|
57
|
+
// comma always separates either steps or key=value pairs; a segment with "="
|
|
58
|
+
// and no known step prefix belongs to the previous step).
|
|
59
|
+
const rawParts = expr.split(',').map((p) => p.trim()).filter(Boolean);
|
|
60
|
+
for (const part of rawParts) {
|
|
61
|
+
const [head, ...rest] = part.split(':');
|
|
62
|
+
const stepName = (head ?? '').trim().toLowerCase();
|
|
63
|
+
const def = getStep(stepName);
|
|
64
|
+
if (!def) {
|
|
65
|
+
if (part.includes('=') && out.length > 0) {
|
|
66
|
+
// continuation of previous step's args: "k=v"
|
|
67
|
+
const prev = out[out.length - 1];
|
|
68
|
+
applyArg(prev.args, part);
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
throw new ChainError(`unknown step "${stepName}" (valid: ${STEPS.map((s) => s.name).join(', ')})`);
|
|
72
|
+
}
|
|
73
|
+
const args = {};
|
|
74
|
+
const argStr = rest.join(':').trim();
|
|
75
|
+
if (argStr) {
|
|
76
|
+
for (const token of argStr.split(/[,;]/).map((t) => t.trim()).filter(Boolean)) {
|
|
77
|
+
if (token.includes('='))
|
|
78
|
+
applyArg(args, token);
|
|
79
|
+
else
|
|
80
|
+
applyPrimaryArg(def.name, args, token);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
out.push({ step: def.name, args });
|
|
84
|
+
}
|
|
85
|
+
return out;
|
|
86
|
+
}
|
|
87
|
+
function applyArg(args, token) {
|
|
88
|
+
const eq = token.indexOf('=');
|
|
89
|
+
const key = token.slice(0, eq).trim();
|
|
90
|
+
const raw = token.slice(eq + 1).trim();
|
|
91
|
+
args[key] = coerce(raw);
|
|
92
|
+
}
|
|
93
|
+
function applyPrimaryArg(step, args, value) {
|
|
94
|
+
const primary = {
|
|
95
|
+
convert: 'format',
|
|
96
|
+
retarget: 'animation',
|
|
97
|
+
stylize: 'style',
|
|
98
|
+
decimate: 'face_limit',
|
|
99
|
+
texture: 'texture_quality',
|
|
100
|
+
rig: 'rig_type',
|
|
101
|
+
segment: 'model',
|
|
102
|
+
smartsegment: 'granularity',
|
|
103
|
+
};
|
|
104
|
+
const key = primary[step];
|
|
105
|
+
if (!key)
|
|
106
|
+
throw new ChainError(`step "${step}" does not take a bare argument ("${value}")`);
|
|
107
|
+
args[key] = coerce(value);
|
|
108
|
+
}
|
|
109
|
+
function coerce(raw) {
|
|
110
|
+
if (raw === 'true')
|
|
111
|
+
return true;
|
|
112
|
+
if (raw === 'false')
|
|
113
|
+
return false;
|
|
114
|
+
if (/^-?\d+$/.test(raw))
|
|
115
|
+
return Number(raw);
|
|
116
|
+
if (/^-?\d*\.\d+$/.test(raw))
|
|
117
|
+
return Number(raw);
|
|
118
|
+
return raw;
|
|
119
|
+
}
|
|
120
|
+
export class ChainError extends Error {
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Validate that a chain is legal given the type of the first (source) task.
|
|
124
|
+
* Returns the list of task types the chain will produce, in order.
|
|
125
|
+
*/
|
|
126
|
+
export function validateChain(chain, sourceType) {
|
|
127
|
+
let currentType = sourceType;
|
|
128
|
+
const produced = [];
|
|
129
|
+
for (const { step } of chain) {
|
|
130
|
+
const def = getStep(step);
|
|
131
|
+
if (!def)
|
|
132
|
+
throw new ChainError(`unknown step "${step}"`);
|
|
133
|
+
if (def.accepts === 'any-model') {
|
|
134
|
+
if (!MODEL_PRODUCING_TYPES.has(currentType)) {
|
|
135
|
+
throw new ChainError(`step "${def.name}" needs a model-producing upstream task, but the previous step produces "${currentType}"`);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
else if (!def.accepts.includes(currentType)) {
|
|
139
|
+
throw new ChainError(def.accepts.length === 0
|
|
140
|
+
? `step "${def.name}" takes a file/URL input only and cannot follow another task in a chain`
|
|
141
|
+
: `step "${def.name}" only accepts ${def.accepts.join('/')} tasks, but the previous step produces "${currentType}"` +
|
|
142
|
+
hintFor(def.name));
|
|
143
|
+
}
|
|
144
|
+
if (!def.passthrough)
|
|
145
|
+
currentType = def.produces;
|
|
146
|
+
produced.push(def.produces);
|
|
147
|
+
}
|
|
148
|
+
return produced;
|
|
149
|
+
}
|
|
150
|
+
function hintFor(step) {
|
|
151
|
+
const hints = {
|
|
152
|
+
refine: ' (refine only accepts a successful text-to-model task)',
|
|
153
|
+
complete: ' (run "segment" first: complete only accepts mesh segmentation tasks)',
|
|
154
|
+
retarget: ' (run "rig" first: retarget only accepts rigged tasks)',
|
|
155
|
+
};
|
|
156
|
+
return hints[step] ?? '';
|
|
157
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* V3 error-code catalog: human translations (zh/en), retryability, and the
|
|
3
|
+
* CLI exit code each error maps to.
|
|
4
|
+
*
|
|
5
|
+
* Source of truth: backend ErrorCode enum (platform-common .../error/ErrorCode.java,
|
|
6
|
+
* itself aligned with openapi-server errors.gen.go). Note the public docs page
|
|
7
|
+
* is stale for 1000/1001 — the server actually sends 1002 for auth failures;
|
|
8
|
+
* 1000/1001 are 5xx server-side errors.
|
|
9
|
+
*/
|
|
10
|
+
export const EXIT = {
|
|
11
|
+
OK: 0,
|
|
12
|
+
FATAL: 1,
|
|
13
|
+
USAGE: 2,
|
|
14
|
+
AUTH: 3,
|
|
15
|
+
BALANCE: 4,
|
|
16
|
+
CONTENT_POLICY: 5,
|
|
17
|
+
TASK_FAILED: 6,
|
|
18
|
+
NETWORK: 7,
|
|
19
|
+
NOT_FOUND: 8,
|
|
20
|
+
RATE_LIMIT: 9,
|
|
21
|
+
};
|
|
22
|
+
export const ERROR_CATALOG = [
|
|
23
|
+
{ code: 1000, zh: '服务端未知错误,请携带 request_id 联系支持', en: 'Unknown server-side error; contact support with the request id', retryable: true, exitCode: EXIT.NETWORK },
|
|
24
|
+
{ code: 1001, zh: '服务端严重错误,请携带 request_id 联系支持', en: 'Fatal server-side error; contact support with the request id', retryable: false, exitCode: EXIT.NETWORK },
|
|
25
|
+
{ code: 1002, zh: '认证失败:API Key 无效或区域不匹配(国内 Key 打了海外 API?),运行 tripo doctor 自动诊断', en: '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, zh: '请求体格式不合法', en: 'The request body is malformed', retryable: false, exitCode: EXIT.USAGE },
|
|
27
|
+
{ code: 1004, zh: '一个或多个参数不合法,请对照文档检查', en: 'One or more parameters are invalid (check the docs)', retryable: false, exitCode: EXIT.USAGE },
|
|
28
|
+
{ code: 1005, zh: '无权访问该资源,请检查账号权限', en: 'Not allowed to access this resource (check permissions)', retryable: false, exitCode: EXIT.AUTH },
|
|
29
|
+
{ code: 1007, zh: '请求过于频繁,触发全局速率限制', en: 'Rate limit exceeded — too many requests in a short time', retryable: true, exitCode: EXIT.RATE_LIMIT },
|
|
30
|
+
{ code: 2000, zh: '触发生成并发上限,同系列任务共享并发池', en: 'Generation concurrency limit hit (task families share a pool)', retryable: true, exitCode: EXIT.RATE_LIMIT },
|
|
31
|
+
{ code: 2001, zh: '任务不存在(确认任务属于当前 Key、区域正确)', en: 'Task not found (same key that created it? right region?)', retryable: false, exitCode: EXIT.NOT_FOUND },
|
|
32
|
+
{ code: 2002, zh: '该任务类型不受支持', en: 'This task type is not supported', retryable: false, exitCode: EXIT.USAGE },
|
|
33
|
+
{ code: 2003, zh: '输入图片文件为空,请检查文件是否损坏', en: 'The image file is empty (corrupted upload?)', retryable: false, exitCode: EXIT.USAGE },
|
|
34
|
+
{ code: 2004, zh: '不支持的图片文件类型', en: 'Unsupported image file type', retryable: false, exitCode: EXIT.USAGE },
|
|
35
|
+
{ code: 2005, zh: 'draft 任务不是成功状态,请用成功的任务重试', en: 'The draft task is not a success; retry with a successful one', retryable: false, exitCode: EXIT.USAGE },
|
|
36
|
+
{ code: 2006, zh: '上游任务类型不受支持,请检查任务链', en: 'The original task type is not supported for this operation', retryable: false, exitCode: EXIT.USAGE },
|
|
37
|
+
{ code: 2007, zh: '上游任务不是成功状态', en: 'The original task has not succeeded', retryable: false, exitCode: EXIT.USAGE },
|
|
38
|
+
{ code: 2008, zh: '输入违反内容政策,请更换输入', en: 'Input violates the content policy; change the input', retryable: false, exitCode: EXIT.CONTENT_POLICY },
|
|
39
|
+
{ code: 2009, zh: 'prompt 含有非法字符,请修改后重试', en: 'The prompt contains invalid characters', retryable: false, exitCode: EXIT.USAGE },
|
|
40
|
+
{ code: 2010, zh: '积分不足', en: 'Insufficient credits', retryable: false, exitCode: EXIT.BALANCE },
|
|
41
|
+
{ code: 2011, zh: 'rig-check 的输入任务没有模型产物', en: 'The rig-check input task has no model output', retryable: false, exitCode: EXIT.USAGE },
|
|
42
|
+
{ code: 2012, zh: 'retarget 只接受 rig 任务作为输入', en: 'retarget only accepts a rig task as input', retryable: false, exitCode: EXIT.USAGE },
|
|
43
|
+
{ code: 2013, zh: 'priority 取值无效(更高优先级需联系支持)', en: 'Invalid priority (contact support for higher priority)', retryable: false, exitCode: EXIT.USAGE },
|
|
44
|
+
{ code: 2014, zh: '内容审核过程出错,请联系支持', en: 'Error occurred during content auditing; contact support', retryable: false, exitCode: EXIT.FATAL },
|
|
45
|
+
{ code: 2015, zh: '模型版本已弃用,请升级 model 参数', en: 'Model version deprecated; upgrade the model parameter', retryable: false, exitCode: EXIT.USAGE },
|
|
46
|
+
{ code: 2016, zh: '该请求类型已弃用,请改用其他类型', en: 'This request type is deprecated; use an alternative', retryable: false, exitCode: EXIT.USAGE },
|
|
47
|
+
{ code: 2017, zh: 'model 版本取值无效', en: 'The model version value is invalid', retryable: false, exitCode: EXIT.USAGE },
|
|
48
|
+
{ code: 2018, zh: '模型太复杂无法重网格化,降低 face_limit 或简化输入', en: 'Model too complex to remesh; lower face_limit or simplify input', retryable: false, exitCode: EXIT.USAGE },
|
|
49
|
+
{ code: 2019, zh: '文件不存在,请确认上传是否成功、file_token 是否过期', en: 'File not found (upload succeeded? token expired?)', retryable: false, exitCode: EXIT.NOT_FOUND },
|
|
50
|
+
{ code: 2020, zh: '图片 URL 无效,请确认可公开访问', en: 'The image URL is invalid (must be publicly reachable)', retryable: false, exitCode: EXIT.USAGE },
|
|
51
|
+
{ code: 2021, zh: '文件大小超过上限,请压缩后重试', en: 'File size exceeds the limit; reduce and retry', retryable: false, exitCode: EXIT.USAGE },
|
|
52
|
+
{ code: 2022, zh: '图片尺寸超过上限,请缩小后重试', en: 'Image size exceeds the limit; reduce and retry', retryable: false, exitCode: EXIT.USAGE },
|
|
53
|
+
{ code: 4001, zh: '请求的接口不存在,请检查 API 路径', en: 'The requested endpoint does not exist (check the API path)', retryable: false, exitCode: EXIT.NOT_FOUND },
|
|
54
|
+
];
|
|
55
|
+
const byCode = new Map(ERROR_CATALOG.map((e) => [e.code, e]));
|
|
56
|
+
export function lookupError(code) {
|
|
57
|
+
return byCode.get(code);
|
|
58
|
+
}
|
|
59
|
+
/** Map an API error code + HTTP status to a stable CLI exit code. */
|
|
60
|
+
export function exitCodeFor(apiCode, httpStatus) {
|
|
61
|
+
if (apiCode !== undefined) {
|
|
62
|
+
const info = byCode.get(apiCode);
|
|
63
|
+
if (info)
|
|
64
|
+
return info.exitCode;
|
|
65
|
+
}
|
|
66
|
+
if (httpStatus === 401)
|
|
67
|
+
return EXIT.AUTH;
|
|
68
|
+
if (httpStatus === 403)
|
|
69
|
+
return EXIT.AUTH;
|
|
70
|
+
if (httpStatus === 404)
|
|
71
|
+
return EXIT.NOT_FOUND;
|
|
72
|
+
if (httpStatus === 429)
|
|
73
|
+
return EXIT.RATE_LIMIT;
|
|
74
|
+
if (httpStatus !== undefined && httpStatus >= 500)
|
|
75
|
+
return EXIT.NETWORK;
|
|
76
|
+
if (httpStatus !== undefined && httpStatus >= 400)
|
|
77
|
+
return EXIT.USAGE;
|
|
78
|
+
return EXIT.FATAL;
|
|
79
|
+
}
|
|
80
|
+
export function isRetryable(apiCode, httpStatus) {
|
|
81
|
+
if (apiCode !== undefined) {
|
|
82
|
+
const info = byCode.get(apiCode);
|
|
83
|
+
if (info)
|
|
84
|
+
return info.retryable;
|
|
85
|
+
}
|
|
86
|
+
if (httpStatus === 429)
|
|
87
|
+
return true;
|
|
88
|
+
if (httpStatus !== undefined && httpStatus >= 500)
|
|
89
|
+
return true;
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dual-model strategy — the single source of truth for 3D model selection.
|
|
3
|
+
*
|
|
4
|
+
* Rule (one branch, no exceptions): explicit low-poly intent or a face budget
|
|
5
|
+
* <= 20000 selects P1; everything else selects v3.1. Explicit --model wins but
|
|
6
|
+
* still goes through constraint validation.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Wire values are what the server actually accepts (backend whitelist:
|
|
10
|
+
* v3.1-20260211, v3.0-20250812, v2.5-20250123, P1-20260311). The friendly
|
|
11
|
+
* `tripo-*` names from marketing docs are CLI-side aliases only — sending
|
|
12
|
+
* them raw returns 400 "invalid model".
|
|
13
|
+
*/
|
|
14
|
+
export const MODEL_V31 = 'v3.1-20260211';
|
|
15
|
+
export const MODEL_P1 = 'P1-20260311';
|
|
16
|
+
/** User-facing aliases normalized to the wire values above. */
|
|
17
|
+
export const MODEL_ALIASES = {
|
|
18
|
+
'v3.1': MODEL_V31,
|
|
19
|
+
'v3.1-20260211': MODEL_V31,
|
|
20
|
+
'tripo-v3.1': MODEL_V31,
|
|
21
|
+
p1: MODEL_P1,
|
|
22
|
+
P1: MODEL_P1,
|
|
23
|
+
'P1-20260311': MODEL_P1,
|
|
24
|
+
'tripo-p1': MODEL_P1,
|
|
25
|
+
// Older whitelisted versions, alias → wire.
|
|
26
|
+
'v3.0': 'v3.0-20250812',
|
|
27
|
+
'tripo-v3.0': 'v3.0-20250812',
|
|
28
|
+
'v2.5': 'v2.5-20250123',
|
|
29
|
+
'tripo-v2.5': 'v2.5-20250123',
|
|
30
|
+
};
|
|
31
|
+
/** Older versions still on the server whitelist (explicit --model only). */
|
|
32
|
+
export const LEGACY_3D_MODELS = ['v3.0-20250812', 'v2.5-20250123'];
|
|
33
|
+
/** CN backend enforces min 50 (OV historically 48); 50 is safe in both regions. */
|
|
34
|
+
export const P1_FACE_LIMIT = { min: 50, max: 20000 };
|
|
35
|
+
/** Parameters the server rejects for P1 — stripped locally with a warning. */
|
|
36
|
+
export const P1_FORBIDDEN_PARAMS = [
|
|
37
|
+
'quad',
|
|
38
|
+
'smart_low_poly',
|
|
39
|
+
'generate_parts',
|
|
40
|
+
'geometry_quality',
|
|
41
|
+
];
|
|
42
|
+
/** Rig wire versions (backend whitelist; marketing names rig-v1.0/rig-v2.0 are rejected). */
|
|
43
|
+
export const RIG_MODEL_V1 = 'v1.0-20240301';
|
|
44
|
+
export const RIG_MODEL_V25 = 'v2.5-20260210';
|
|
45
|
+
/** Marketing rig names → wire versions the server actually accepts. */
|
|
46
|
+
export const RIG_MODEL_ALIASES = {
|
|
47
|
+
'rig-v1.0': RIG_MODEL_V1,
|
|
48
|
+
'v1.0-20240301': RIG_MODEL_V1,
|
|
49
|
+
'rig-v2.0': RIG_MODEL_V25,
|
|
50
|
+
'rig-v2.5': RIG_MODEL_V25,
|
|
51
|
+
'v2.5-20260210': RIG_MODEL_V25,
|
|
52
|
+
};
|
|
53
|
+
export function normalizeRigModel(input) {
|
|
54
|
+
return RIG_MODEL_ALIASES[input] ?? input;
|
|
55
|
+
}
|
|
56
|
+
/** Default versions for non-generation capabilities. */
|
|
57
|
+
export const CAPABILITY_DEFAULTS = {
|
|
58
|
+
texture: 'v3.0-20250812',
|
|
59
|
+
/** Server default is v1.0 (biped-only presets); the CLI defaults to the
|
|
60
|
+
* v2.5 rig which has presets for all seven body types. */
|
|
61
|
+
rig: RIG_MODEL_V25,
|
|
62
|
+
segment: 'v1.0-20250506',
|
|
63
|
+
textToImage: 'seedream_v4',
|
|
64
|
+
imageToImage: 'seedream_v5',
|
|
65
|
+
};
|
|
66
|
+
export const RIG_TYPES = [
|
|
67
|
+
'biped',
|
|
68
|
+
'quadruped',
|
|
69
|
+
'hexapod',
|
|
70
|
+
'octopod',
|
|
71
|
+
'avian',
|
|
72
|
+
'serpentine',
|
|
73
|
+
'aquatic',
|
|
74
|
+
];
|
|
75
|
+
export const RIG_SPECS = ['tripo', 'mixamo'];
|
|
76
|
+
/** Per-endpoint image model whitelists (ImageGenerationValidation):
|
|
77
|
+
* image-to-image does NOT accept seedream_v4. */
|
|
78
|
+
export const TEXT_TO_IMAGE_MODELS = [
|
|
79
|
+
'seedream_v4',
|
|
80
|
+
'seedream_v5',
|
|
81
|
+
'banana',
|
|
82
|
+
'banana_pro',
|
|
83
|
+
'banana2',
|
|
84
|
+
'chat_image_1',
|
|
85
|
+
'chat_image_1.5',
|
|
86
|
+
'chat_image_2',
|
|
87
|
+
];
|
|
88
|
+
export const IMAGE_TO_IMAGE_MODELS = [
|
|
89
|
+
'seedream_v5',
|
|
90
|
+
'banana',
|
|
91
|
+
'banana_pro',
|
|
92
|
+
'banana2',
|
|
93
|
+
'chat_image_1',
|
|
94
|
+
'chat_image_1.5',
|
|
95
|
+
'chat_image_2',
|
|
96
|
+
];
|
|
97
|
+
/** texture_model whitelist (ModelPostProcessValidation.ALLOWED_TEXTURE_MODELS). */
|
|
98
|
+
export const TEXTURE_MODELS = ['v3.0-20250812', 'v2.5-20250123'];
|
|
99
|
+
/** mesh_segmentation whitelist (ALLOWED_SEGMENT_MODELS). */
|
|
100
|
+
export const SEGMENT_MODELS = ['v1.0-20250506', 'v2.0-20260430'];
|
|
101
|
+
/** mesh/decimate whitelist (ALLOWED_DECIMATE_MODELS; blank/"default" → server default v2.0). */
|
|
102
|
+
export const DECIMATE_MODELS = ['v2.0', 'v1.0'];
|
|
103
|
+
/** Server whitelists differ per endpoint (ImageGenerationValidation). */
|
|
104
|
+
export const TEXT_TO_IMAGE_TEMPLATES = [
|
|
105
|
+
'asset_extraction',
|
|
106
|
+
'character_completion',
|
|
107
|
+
't_pose',
|
|
108
|
+
'variants',
|
|
109
|
+
'figure',
|
|
110
|
+
];
|
|
111
|
+
export const IMAGE_TO_IMAGE_TEMPLATES = [
|
|
112
|
+
't_pose',
|
|
113
|
+
'character_completion',
|
|
114
|
+
'3d_enhance',
|
|
115
|
+
'variants',
|
|
116
|
+
'figure',
|
|
117
|
+
];
|
|
118
|
+
export const CONVERT_FORMATS = ['GLTF', 'USDZ', 'FBX', 'OBJ', 'STL', '3MF'];
|
|
119
|
+
export const TEXTURE_FORMATS = [
|
|
120
|
+
'BMP',
|
|
121
|
+
'DPX',
|
|
122
|
+
'HDR',
|
|
123
|
+
'JPEG',
|
|
124
|
+
'OPEN_EXR',
|
|
125
|
+
'PNG',
|
|
126
|
+
'TARGA',
|
|
127
|
+
'TIFF',
|
|
128
|
+
'WEBP',
|
|
129
|
+
];
|
|
130
|
+
export const FBX_PRESETS = ['blender', '3dsmax', 'mixamo'];
|
|
131
|
+
export const EXPORT_ORIENTATIONS = ['+x', '-x', '+y', '-y'];
|
|
132
|
+
export const STYLIZE_STYLES = ['lego', 'voxel', 'voronoi', 'minecraft'];
|
|
133
|
+
export const TEXTURE_QUALITIES = ['standard', 'detailed', 'extreme'];
|
|
134
|
+
/** Normalize any accepted alias to the canonical wire value; unknown values pass through. */
|
|
135
|
+
export function normalizeModel(input) {
|
|
136
|
+
return MODEL_ALIASES[input] ?? input;
|
|
137
|
+
}
|
|
138
|
+
export function isP1(model) {
|
|
139
|
+
return normalizeModel(model) === MODEL_P1;
|
|
140
|
+
}
|
|
141
|
+
/** The one rule. Returns the model plus a human-readable reason. */
|
|
142
|
+
export function chooseModel(intent) {
|
|
143
|
+
if (intent.explicit) {
|
|
144
|
+
return { model: normalizeModel(intent.explicit), reason: 'explicit --model' };
|
|
145
|
+
}
|
|
146
|
+
if (intent.lowPoly) {
|
|
147
|
+
return { model: MODEL_P1, reason: 'low-poly intent → P1' };
|
|
148
|
+
}
|
|
149
|
+
if (intent.faceLimit !== undefined && intent.faceLimit <= P1_FACE_LIMIT.max) {
|
|
150
|
+
return { model: MODEL_P1, reason: `face budget ${intent.faceLimit} ≤ ${P1_FACE_LIMIT.max} → P1` };
|
|
151
|
+
}
|
|
152
|
+
return { model: MODEL_V31, reason: 'default high-fidelity → v3.1' };
|
|
153
|
+
}
|
|
154
|
+
/** Keywords that signal low-poly intent in a prompt (used by make/ai). */
|
|
155
|
+
const LOW_POLY_HINTS = [/low[\s_-]?poly/i, /低模/, /低面/, /手游/, /voxel/i, /像素风/];
|
|
156
|
+
export function detectLowPolyIntent(text) {
|
|
157
|
+
return LOW_POLY_HINTS.some((re) => re.test(text));
|
|
158
|
+
}
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parameter linkage rules distilled from server-side validation
|
|
3
|
+
* (TextImageModelValidation / ModelPostProcessValidation / handlers).
|
|
4
|
+
*
|
|
5
|
+
* Every rule here prevents a guaranteed 400 or a silently wasted credit.
|
|
6
|
+
* Rules marked [hard] mirror server enforcement; [soft] are CLI smart defaults.
|
|
7
|
+
*/
|
|
8
|
+
import { CONVERT_FORMATS, EXPORT_ORIENTATIONS, FBX_PRESETS, P1_FACE_LIMIT, P1_FORBIDDEN_PARAMS, TEXTURE_FORMATS, TEXTURE_QUALITIES, isP1, } from './models.js';
|
|
9
|
+
const PROMPT_MAX = 1024;
|
|
10
|
+
const NEGATIVE_PROMPT_MAX = 255;
|
|
11
|
+
/** Validate + auto-fix parameters for 3D generation (text/image/multiview-to-model). */
|
|
12
|
+
export function validateGenerationParams(model, input) {
|
|
13
|
+
const params = { ...input };
|
|
14
|
+
const warnings = [];
|
|
15
|
+
const errors = [];
|
|
16
|
+
if (typeof params.prompt === 'string' && params.prompt.length > PROMPT_MAX) {
|
|
17
|
+
errors.push(`prompt exceeds ${PROMPT_MAX} characters (${params.prompt.length})`);
|
|
18
|
+
}
|
|
19
|
+
if (typeof params.negative_prompt === 'string' && params.negative_prompt.length > NEGATIVE_PROMPT_MAX) {
|
|
20
|
+
errors.push(`negative_prompt exceeds ${NEGATIVE_PROMPT_MAX} characters`);
|
|
21
|
+
}
|
|
22
|
+
// [hard] pbr=true forces texture=true (PBR is a superset of texturing).
|
|
23
|
+
if (params.pbr === true && params.texture === false) {
|
|
24
|
+
params.texture = true;
|
|
25
|
+
warnings.push('pbr=true forces texture=true (adjusted)');
|
|
26
|
+
}
|
|
27
|
+
// [hard] generate_parts outputs bare geometry: texture/pbr/quad must be off.
|
|
28
|
+
if (params.generate_parts === true) {
|
|
29
|
+
for (const key of ['texture', 'pbr', 'quad']) {
|
|
30
|
+
if (params[key] === true) {
|
|
31
|
+
params[key] = false;
|
|
32
|
+
warnings.push(`generate_parts=true disables ${key} (adjusted; run "tripo model texture" afterwards)`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
// [hard] P1 constraints: strip forbidden params, enforce face_limit range.
|
|
37
|
+
if (isP1(model)) {
|
|
38
|
+
for (const key of P1_FORBIDDEN_PARAMS) {
|
|
39
|
+
if (params[key] !== undefined) {
|
|
40
|
+
delete params[key];
|
|
41
|
+
warnings.push(`P1 does not support ${key} (removed)`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
const face = params.face_limit;
|
|
45
|
+
if (typeof face === 'number') {
|
|
46
|
+
if (face < P1_FACE_LIMIT.min || face > P1_FACE_LIMIT.max) {
|
|
47
|
+
errors.push(`P1 face_limit must be within ${P1_FACE_LIMIT.min}–${P1_FACE_LIMIT.max} (got ${face}); ` +
|
|
48
|
+
`use tripo-v3.1 for higher budgets`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
// [hard] smart_low_poly face_limit windows: 500–20000 tri / 500–10000 quad.
|
|
54
|
+
if (params.smart_low_poly === true && typeof params.face_limit === 'number') {
|
|
55
|
+
const max = params.quad === true ? 10000 : 20000;
|
|
56
|
+
if (params.face_limit < 500 || params.face_limit > max) {
|
|
57
|
+
errors.push(`smart_low_poly requires face_limit 500–${max}${params.quad === true ? ' (quad halves the budget)' : ''}`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
if (params.texture_quality !== undefined &&
|
|
62
|
+
!TEXTURE_QUALITIES.includes(params.texture_quality)) {
|
|
63
|
+
errors.push(`texture_quality must be one of ${TEXTURE_QUALITIES.join('/')}`);
|
|
64
|
+
}
|
|
65
|
+
// [hard] quad topology cannot be stored in GLB → output will be FBX.
|
|
66
|
+
if (params.quad === true) {
|
|
67
|
+
warnings.push('quad=true forces FBX output (quad topology cannot be stored in GLB)');
|
|
68
|
+
}
|
|
69
|
+
return { params, warnings, errors };
|
|
70
|
+
}
|
|
71
|
+
/** Validate parameters for models/convert. */
|
|
72
|
+
export function validateConvertParams(input) {
|
|
73
|
+
const params = { ...input };
|
|
74
|
+
const warnings = [];
|
|
75
|
+
const errors = [];
|
|
76
|
+
const format = typeof params.format === 'string' ? params.format.toUpperCase() : undefined;
|
|
77
|
+
if (!format) {
|
|
78
|
+
errors.push(`convert requires format (${CONVERT_FORMATS.join('/')})`);
|
|
79
|
+
}
|
|
80
|
+
else if (!CONVERT_FORMATS.includes(format)) {
|
|
81
|
+
errors.push(`unsupported format "${params.format}" (${CONVERT_FORMATS.join('/')})`);
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
params.format = format;
|
|
85
|
+
}
|
|
86
|
+
// [hard] quad topology cannot live in GLTF/GLB.
|
|
87
|
+
if (params.quad === true && format === 'GLTF') {
|
|
88
|
+
errors.push('quad=true cannot export GLTF (quad topology is not storable in glTF); use FBX/OBJ/USDZ');
|
|
89
|
+
}
|
|
90
|
+
if (params.force_symmetry === true && params.quad !== true) {
|
|
91
|
+
delete params.force_symmetry;
|
|
92
|
+
warnings.push('force_symmetry only applies with quad=true (removed)');
|
|
93
|
+
}
|
|
94
|
+
if (params.texture_format !== undefined &&
|
|
95
|
+
!TEXTURE_FORMATS.includes(String(params.texture_format).toUpperCase())) {
|
|
96
|
+
errors.push(`texture_format must be one of ${TEXTURE_FORMATS.join('/')}`);
|
|
97
|
+
}
|
|
98
|
+
else if (typeof params.texture_format === 'string') {
|
|
99
|
+
params.texture_format = params.texture_format.toUpperCase();
|
|
100
|
+
}
|
|
101
|
+
// [hard] vertex colors only survive in OBJ/GLTF exports.
|
|
102
|
+
if (params.export_vertex_colors === true && format && !['OBJ', 'GLTF'].includes(format)) {
|
|
103
|
+
delete params.export_vertex_colors;
|
|
104
|
+
warnings.push(`export_vertex_colors only applies to OBJ/GLTF (removed for ${format})`);
|
|
105
|
+
}
|
|
106
|
+
if (params.fbx_preset !== undefined) {
|
|
107
|
+
if (!FBX_PRESETS.includes(params.fbx_preset)) {
|
|
108
|
+
errors.push(`fbx_preset must be one of ${FBX_PRESETS.join('/')}`);
|
|
109
|
+
}
|
|
110
|
+
else if (format !== 'FBX') {
|
|
111
|
+
delete params.fbx_preset;
|
|
112
|
+
warnings.push('fbx_preset only applies to FBX exports (removed)');
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
if (params.export_orientation !== undefined &&
|
|
116
|
+
!EXPORT_ORIENTATIONS.includes(params.export_orientation)) {
|
|
117
|
+
errors.push(`export_orientation must be one of ${EXPORT_ORIENTATIONS.join('/')}`);
|
|
118
|
+
}
|
|
119
|
+
// [soft] cost transparency: any non-default option upgrades billing to
|
|
120
|
+
// convert_model_complex.
|
|
121
|
+
const defaults = {
|
|
122
|
+
texture_size: 4096,
|
|
123
|
+
texture_format: 'JPEG',
|
|
124
|
+
bake: true,
|
|
125
|
+
pack_uv: false,
|
|
126
|
+
export_vertex_colors: false,
|
|
127
|
+
pivot_to_center_bottom: false,
|
|
128
|
+
scale_factor: 1,
|
|
129
|
+
with_animation: true,
|
|
130
|
+
animate_in_place: false,
|
|
131
|
+
export_orientation: '+x',
|
|
132
|
+
fbx_preset: 'blender',
|
|
133
|
+
quad: false,
|
|
134
|
+
flatten_bottom: false,
|
|
135
|
+
force_symmetry: false,
|
|
136
|
+
};
|
|
137
|
+
const complex = Object.entries(defaults).some(([k, dv]) => params[k] !== undefined && params[k] !== dv) || params.face_limit !== undefined || params.part_names !== undefined;
|
|
138
|
+
if (complex) {
|
|
139
|
+
warnings.push('non-default convert options use the "complex convert" price tier');
|
|
140
|
+
}
|
|
141
|
+
return { params, warnings, errors };
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Validate parameters for mesh/decimate (ModelPostProcessValidation):
|
|
145
|
+
* v2.0 (default): face_limit 500–20000 tri / 500–10000 quad.
|
|
146
|
+
* v1.0: face_limit REQUIRED, 500–2,000,000 tri / 500–150,000 quad; no bake/part_names.
|
|
147
|
+
*/
|
|
148
|
+
export function validateDecimateParams(input) {
|
|
149
|
+
const params = { ...input };
|
|
150
|
+
const warnings = [];
|
|
151
|
+
const errors = [];
|
|
152
|
+
const model = typeof params.model === 'string' ? params.model.toLowerCase() : undefined;
|
|
153
|
+
if (model !== undefined && !['v1.0', 'v2.0', 'default'].includes(model)) {
|
|
154
|
+
errors.push(`decimate model must be v2.0 or v1.0 (got "${String(params.model)}")`);
|
|
155
|
+
}
|
|
156
|
+
const isV1 = model === 'v1.0';
|
|
157
|
+
const quad = params.quad === true;
|
|
158
|
+
const face = params.face_limit;
|
|
159
|
+
if (isV1) {
|
|
160
|
+
const max = quad ? 150_000 : 2_000_000;
|
|
161
|
+
if (typeof face !== 'number') {
|
|
162
|
+
errors.push(`decimate model v1.0 requires face_limit (500–${max}${quad ? ', quad' : ''})`);
|
|
163
|
+
}
|
|
164
|
+
else if (face < 500 || face > max) {
|
|
165
|
+
errors.push(`decimate v1.0 face_limit must be within 500–${max}${quad ? ' (quad mesh)' : ''}`);
|
|
166
|
+
}
|
|
167
|
+
for (const key of ['bake', 'part_names']) {
|
|
168
|
+
if (params[key] !== undefined) {
|
|
169
|
+
delete params[key];
|
|
170
|
+
warnings.push(`decimate v1.0 does not support ${key} (removed)`);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
else {
|
|
175
|
+
const max = quad ? 10_000 : 20_000;
|
|
176
|
+
if (typeof face === 'number' && (face < 500 || face > max)) {
|
|
177
|
+
errors.push(`decimate face_limit must be within 500–${max}${quad ? ' (quad halves the budget)' : ''}`);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return { params, warnings, errors };
|
|
181
|
+
}
|
|
182
|
+
/** Validate retarget animations list. */
|
|
183
|
+
export function validateRetargetParams(input) {
|
|
184
|
+
const params = { ...input };
|
|
185
|
+
const warnings = [];
|
|
186
|
+
const errors = [];
|
|
187
|
+
const anims = params.animations;
|
|
188
|
+
if (Array.isArray(anims)) {
|
|
189
|
+
if (anims.length === 0)
|
|
190
|
+
errors.push('animations must not be empty');
|
|
191
|
+
if (anims.length > 5)
|
|
192
|
+
errors.push(`retarget accepts at most 5 animations per task (got ${anims.length}); billing is per animation`);
|
|
193
|
+
if (anims.length === 1) {
|
|
194
|
+
// Server accepts either `animation` (single) or `animations` (list).
|
|
195
|
+
params.animation = anims[0];
|
|
196
|
+
delete params.animations;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
else if (typeof params.animation !== 'string') {
|
|
200
|
+
errors.push('retarget requires animation (single) or animations (list)');
|
|
201
|
+
}
|
|
202
|
+
return { params, warnings, errors };
|
|
203
|
+
}
|