tripo-cli 0.7.3 → 0.8.1
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 +33 -0
- package/README.md +10 -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/completion.js +1 -1
- 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/models.js +47 -0
- 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 +6 -11
- package/package.json +3 -2
- package/skill/SKILL.md +35 -8
- package/skill/commands/account.md +1 -1
- package/skill/commands/batch.md +13 -3
- package/skill/commands/generate.md +35 -10
- package/skill/commands/make.md +39 -13
- package/skill/commands/process.md +2 -1
- package/skill/commands/task.md +2 -2
- package/skill/commands/view.md +1 -1
- package/skill/common-errors.md +6 -3
- package/skill/examples/animation.md +3 -1
- package/skill/examples/ar-web.md +6 -4
- package/skill/examples/game-asset.md +17 -6
- package/skill/examples/pipes.md +2 -2
- package/skill/examples/print.md +7 -3
- package/dist/i18n.js +0 -19
package/dist/knowledge/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
|
+
}
|
package/dist/knowledge/params.js
CHANGED
|
@@ -5,14 +5,96 @@
|
|
|
5
5
|
* Every rule here prevents a guaranteed 400 or a silently wasted credit.
|
|
6
6
|
* Rules marked [hard] mirror server enforcement; [soft] are CLI smart defaults.
|
|
7
7
|
*/
|
|
8
|
-
import { CONVERT_FORMATS, DECIMATE_MODELS, EXPORT_ORIENTATIONS, FBX_PRESETS, MODEL_P2, P1_FACE_LIMIT, P1_FORBIDDEN_PARAMS, P2_FACE_LIMIT, P2_FORBIDDEN_PARAMS, TEXTURE_FORMATS, TEXTURE_MODEL_V35, TEXTURE_QUALITIES, isP1, isP2, normalizeModel, } from './models.js';
|
|
8
|
+
import { CAPABILITY_DEFAULTS, IMAGE_TO_IMAGE_MODELS, TEXT_TO_IMAGE_MODELS, CONVERT_FORMATS, DECIMATE_MODELS, EXPORT_ORIENTATIONS, FBX_PRESETS, LEGACY_3D_MODELS, MODEL_P1, MODEL_P2, MODEL_V31, P1_FACE_LIMIT, P1_FORBIDDEN_PARAMS, P2_FACE_LIMIT, P2_FORBIDDEN_PARAMS, TEXTURE_FORMATS, TEXTURE_MODEL_V35, TEXTURE_QUALITIES, isP1, isP2, normalizeModel, } from './models.js';
|
|
9
9
|
const PROMPT_MAX = 1024;
|
|
10
10
|
const NEGATIVE_PROMPT_MAX = 255;
|
|
11
|
+
/** Image endpoint contracts; never silently substitute a paid model. */
|
|
12
|
+
export function validateImageParams(input, editing) {
|
|
13
|
+
const params = { ...input };
|
|
14
|
+
const warnings = [];
|
|
15
|
+
const errors = [];
|
|
16
|
+
const model = params.model === undefined
|
|
17
|
+
? (editing ? CAPABILITY_DEFAULTS.imageToImage : CAPABILITY_DEFAULTS.textToImage)
|
|
18
|
+
: params.model;
|
|
19
|
+
params.model = model;
|
|
20
|
+
const models = editing ? IMAGE_TO_IMAGE_MODELS : TEXT_TO_IMAGE_MODELS;
|
|
21
|
+
if (model === 'chat_image_1') {
|
|
22
|
+
errors.push('model chat_image_1 retired on 2026-10-23; explicitly choose chat_image_2 or chat_image_2.5_flare/sunburst (no automatic resubmission)');
|
|
23
|
+
}
|
|
24
|
+
else if (!models.includes(model)) {
|
|
25
|
+
errors.push(`unsupported image model "${String(model)}" for ${editing ? 'image-to-image' : 'text-to-image'}`);
|
|
26
|
+
}
|
|
27
|
+
const chat25 = model === 'chat_image_2.5_flare' || model === 'chat_image_2.5_sunburst';
|
|
28
|
+
if (params.size !== undefined && typeof model === 'string' && !validImageSize(model, params.size)) {
|
|
29
|
+
errors.push(`size is unsupported for model ${model}`);
|
|
30
|
+
}
|
|
31
|
+
if ((model === 'seedream_v4' || model === 'seedream_v5') && typeof params.size === 'string' && params.size.includes('x')) {
|
|
32
|
+
warnings.push('Seedream exact pixel-count limits unverified: official MP ranges are rounded; server validation still applies');
|
|
33
|
+
}
|
|
34
|
+
if (params.quality !== undefined) {
|
|
35
|
+
const qualities = chat25 ? ['low', 'medium', 'high', 'xhigh', 'max']
|
|
36
|
+
: model === 'chat_image_2' ? ['low', 'medium', 'high'] : [];
|
|
37
|
+
if (!qualities.includes(params.quality))
|
|
38
|
+
errors.push(`quality is unsupported for model ${String(model)} (auto is not supported)`);
|
|
39
|
+
}
|
|
40
|
+
if (params.output_format !== undefined && !['png', 'jpeg'].includes(params.output_format)) {
|
|
41
|
+
errors.push('output_format must be png or jpeg');
|
|
42
|
+
}
|
|
43
|
+
if (params.background !== undefined) {
|
|
44
|
+
if (!chat25) {
|
|
45
|
+
delete params.background;
|
|
46
|
+
warnings.push('background is ignored outside chat_image_2.5 models (removed)');
|
|
47
|
+
}
|
|
48
|
+
else if (!['auto', 'opaque', 'transparent'].includes(params.background)) {
|
|
49
|
+
errors.push('background must be auto, opaque or transparent');
|
|
50
|
+
}
|
|
51
|
+
else if (params.background === 'transparent' && params.output_format !== undefined && params.output_format !== 'png') {
|
|
52
|
+
errors.push('background=transparent requires output_format=png');
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return { params, warnings, errors };
|
|
56
|
+
}
|
|
57
|
+
/** Only documented size rules; rounded Seedream MP limits are not exact pixel thresholds. */
|
|
58
|
+
function validImageSize(model, size) {
|
|
59
|
+
if (typeof size !== 'string')
|
|
60
|
+
return false;
|
|
61
|
+
const match = /^([1-9]\d*)x([1-9]\d*)$/.exec(size);
|
|
62
|
+
const w = Number(match?.[1]);
|
|
63
|
+
const h = Number(match?.[2]);
|
|
64
|
+
const pixels = !!match && Number.isSafeInteger(w) && Number.isSafeInteger(h);
|
|
65
|
+
if (model === 'banana') {
|
|
66
|
+
return ['1024x1024', '832x1248', '1248x832', '864x1184', '1184x864', '896x1152', '1152x896', '768x1344', '1344x768', '1536x672'].includes(size);
|
|
67
|
+
}
|
|
68
|
+
if (model === 'banana2')
|
|
69
|
+
return ['0.5K', '1K', '2K', '4K'].includes(size);
|
|
70
|
+
if (model === 'banana_pro')
|
|
71
|
+
return ['1K', '2K', '4K'].includes(size) || pixels;
|
|
72
|
+
if (model === 'seedream_v4' || model === 'seedream_v5') {
|
|
73
|
+
return ['2K', model === 'seedream_v5' ? '3K' : '4K'].includes(size) || (pixels && w / h >= 1 / 16 && w / h <= 16);
|
|
74
|
+
}
|
|
75
|
+
if (model === 'chat_image_1.5')
|
|
76
|
+
return ['1024x1024', '1024x1536', '1536x1024', 'auto'].includes(size);
|
|
77
|
+
if (model === 'chat_image_2' || model.startsWith('chat_image_2.5_')) {
|
|
78
|
+
return size === 'auto' || (pixels && w <= 3840 && h <= 3840 && w % 16 === 0 && h % 16 === 0 &&
|
|
79
|
+
Math.max(w, h) <= 3 * Math.min(w, h) && w * h >= 655360 && w * h <= 8294400);
|
|
80
|
+
}
|
|
81
|
+
return true; // Unknown models are rejected separately.
|
|
82
|
+
}
|
|
83
|
+
function validateFaceLimit(params, errors) {
|
|
84
|
+
if (params.face_limit !== undefined &&
|
|
85
|
+
(typeof params.face_limit !== 'number' || !Number.isFinite(params.face_limit) || !Number.isInteger(params.face_limit))) {
|
|
86
|
+
errors.push('face_limit must be a finite integer');
|
|
87
|
+
}
|
|
88
|
+
}
|
|
11
89
|
/** Validate + auto-fix parameters for 3D generation (text/image/multiview-to-model). */
|
|
12
90
|
export function validateGenerationParams(model, input) {
|
|
13
91
|
const params = { ...input };
|
|
14
92
|
const warnings = [];
|
|
15
93
|
const errors = [];
|
|
94
|
+
if (![MODEL_V31, MODEL_P1, MODEL_P2, ...LEGACY_3D_MODELS].includes(normalizeModel(model))) {
|
|
95
|
+
errors.push(`unsupported 3D model "${model}"`);
|
|
96
|
+
}
|
|
97
|
+
validateFaceLimit(params, errors);
|
|
16
98
|
if (typeof params.prompt === 'string' && params.prompt.length > PROMPT_MAX) {
|
|
17
99
|
errors.push(`prompt exceeds ${PROMPT_MAX} characters (${params.prompt.length})`);
|
|
18
100
|
}
|
|
@@ -44,6 +126,11 @@ export function validateGenerationParams(model, input) {
|
|
|
44
126
|
params.texture = true;
|
|
45
127
|
warnings.push('pbr=true forces texture=true (adjusted)');
|
|
46
128
|
}
|
|
129
|
+
// [hard] pbr defaults to true server-side, so texture=false alone would still be textured.
|
|
130
|
+
if (params.texture === false && params.pbr === undefined) {
|
|
131
|
+
params.pbr = false;
|
|
132
|
+
warnings.push('texture=false implies pbr=false (adjusted)');
|
|
133
|
+
}
|
|
47
134
|
// [hard] generate_parts outputs bare geometry: texture/pbr/quad must be off
|
|
48
135
|
// (texture and pbr default to true server-side, so omitting them counts as on).
|
|
49
136
|
if (params.generate_parts === true) {
|
|
@@ -60,7 +147,7 @@ export function validateGenerationParams(model, input) {
|
|
|
60
147
|
if (typeof face === 'number') {
|
|
61
148
|
if (face < P1_FACE_LIMIT.min || face > P1_FACE_LIMIT.max) {
|
|
62
149
|
errors.push(`P1 face_limit must be within ${P1_FACE_LIMIT.min}–${P1_FACE_LIMIT.max} (got ${face}); ` +
|
|
63
|
-
`use tripo-v3.1 for
|
|
150
|
+
`use tripo-v3.1 for budgets outside this range`);
|
|
64
151
|
}
|
|
65
152
|
}
|
|
66
153
|
}
|
|
@@ -72,7 +159,7 @@ export function validateGenerationParams(model, input) {
|
|
|
72
159
|
const max = quad ? P2_FACE_LIMIT.quadMax : P2_FACE_LIMIT.triMax;
|
|
73
160
|
if (face < P2_FACE_LIMIT.min || face > max) {
|
|
74
161
|
errors.push(`P2 face_limit must be within ${P2_FACE_LIMIT.min}–${max}${quad ? ' with quad=true' : ''} (got ${face}); ` +
|
|
75
|
-
`use tripo-v3.1 for
|
|
162
|
+
`use tripo-v3.1 for budgets outside this range`);
|
|
76
163
|
}
|
|
77
164
|
}
|
|
78
165
|
}
|
|
@@ -85,10 +172,6 @@ export function validateGenerationParams(model, input) {
|
|
|
85
172
|
}
|
|
86
173
|
}
|
|
87
174
|
}
|
|
88
|
-
if (params.texture_quality !== undefined &&
|
|
89
|
-
!TEXTURE_QUALITIES.includes(params.texture_quality)) {
|
|
90
|
-
errors.push(`texture_quality must be one of ${TEXTURE_QUALITIES.join('/')}`);
|
|
91
|
-
}
|
|
92
175
|
applyTextureV35(params, 'texture_version', warnings, errors);
|
|
93
176
|
// [hard] quad topology cannot be stored in GLB → output will be FBX.
|
|
94
177
|
if (params.quad === true) {
|
|
@@ -103,6 +186,9 @@ export function validateGenerationParams(model, input) {
|
|
|
103
186
|
* generation endpoints and `model` on /v3/models/texture.
|
|
104
187
|
*/
|
|
105
188
|
export function applyTextureV35(params, key, warnings, errors) {
|
|
189
|
+
if (params.texture_quality !== undefined && !TEXTURE_QUALITIES.includes(params.texture_quality)) {
|
|
190
|
+
errors.push(`texture_quality must be one of ${TEXTURE_QUALITIES.join('/')}`);
|
|
191
|
+
}
|
|
106
192
|
const feature = params.texture_quality === 'fast' ? 'texture_quality=fast' : params.delight !== undefined ? 'delight' : undefined;
|
|
107
193
|
if (!feature)
|
|
108
194
|
return;
|
|
@@ -123,6 +209,7 @@ export function validateConvertParams(input) {
|
|
|
123
209
|
const params = { ...input };
|
|
124
210
|
const warnings = [];
|
|
125
211
|
const errors = [];
|
|
212
|
+
validateFaceLimit(params, errors);
|
|
126
213
|
const format = typeof params.format === 'string' ? params.format.toUpperCase() : undefined;
|
|
127
214
|
if (!format) {
|
|
128
215
|
errors.push(`convert requires format (${CONVERT_FORMATS.join('/')})`);
|
|
@@ -199,6 +286,7 @@ export function validateDecimateParams(input) {
|
|
|
199
286
|
const params = { ...input };
|
|
200
287
|
const warnings = [];
|
|
201
288
|
const errors = [];
|
|
289
|
+
validateFaceLimit(params, errors);
|
|
202
290
|
const model = typeof params.model === 'string' ? params.model.toLowerCase() : undefined;
|
|
203
291
|
if (model !== undefined && model !== 'default' && !DECIMATE_MODELS.includes(model)) {
|
|
204
292
|
errors.push(`decimate model must be ${DECIMATE_MODELS.join('/')} (got "${String(params.model)}")`);
|
|
@@ -207,7 +295,7 @@ export function validateDecimateParams(input) {
|
|
|
207
295
|
if (params.smart_uv === true) {
|
|
208
296
|
if (model === undefined) {
|
|
209
297
|
params.model = 'v3.0';
|
|
210
|
-
warnings.push('smart_uv requires decimate v3.0: using model=v3.0 (
|
|
298
|
+
warnings.push('smart_uv requires decimate v3.0: using model=v3.0 (cost not estimated; check current regional pricing)');
|
|
211
299
|
}
|
|
212
300
|
else if (model !== 'v3.0') {
|
|
213
301
|
errors.push(`smart_uv only works with decimate model v3.0 (got "${String(params.model)}")`);
|
|
@@ -2,105 +2,82 @@
|
|
|
2
2
|
* Scenario decision matrix: --for presets and the AI wizard both read this
|
|
3
3
|
* table. Changing production strategy means editing data here, not logic.
|
|
4
4
|
*/
|
|
5
|
-
import { CAPABILITY_DEFAULTS,
|
|
5
|
+
import { CAPABILITY_DEFAULTS, MODEL_P2, MODEL_V31 } from './models.js';
|
|
6
6
|
export const SCENARIOS = [
|
|
7
7
|
{
|
|
8
8
|
id: 'game-mobile',
|
|
9
|
-
label:
|
|
10
|
-
description:
|
|
11
|
-
model:
|
|
9
|
+
label: 'Mobile game / low-poly',
|
|
10
|
+
description: 'Mobile game assets with strict face budgets and clean topology',
|
|
11
|
+
model: MODEL_P2,
|
|
12
12
|
genParams: { texture: true, pbr: true, texture_quality: 'standard', face_limit: 15000 },
|
|
13
13
|
chain: [{ step: 'convert', args: { format: 'FBX', texture_size: 2048 } }],
|
|
14
14
|
deliverables: ['FBX', 'GLB'],
|
|
15
|
-
notes:
|
|
16
|
-
zh: ['P1 原生低模,拓扑干净', '2K 贴图适配移动端', '需要骨骼时追加 --then rig'],
|
|
17
|
-
en: ['P1 native low-poly with clean topology', '2K textures for mobile budgets', 'append --then rig if you need a skeleton'],
|
|
18
|
-
},
|
|
15
|
+
notes: ['P2 editable low-poly topology', '2K textures for mobile budgets', 'P2 costs more credits than H/P1; check credits before running', 'append --then rig if you need a skeleton'],
|
|
19
16
|
},
|
|
20
17
|
{
|
|
21
18
|
id: 'game-pc',
|
|
22
|
-
label:
|
|
23
|
-
description:
|
|
24
|
-
model:
|
|
19
|
+
label: 'PC / console game',
|
|
20
|
+
description: 'Editable real-time engine assets with full PBR',
|
|
21
|
+
model: MODEL_P2,
|
|
25
22
|
genParams: { texture: true, pbr: true, texture_quality: 'detailed' },
|
|
26
23
|
chain: [{ step: 'convert', args: { format: 'FBX' } }],
|
|
27
24
|
deliverables: ['FBX', 'GLTF'],
|
|
28
|
-
notes:
|
|
29
|
-
zh: ['v3.1 高精度 + detailed 贴图', '需要 LOD 时追加 --then decimate:5000', '进 Blender/3dsMax 用 fbx_preset 对齐'],
|
|
30
|
-
en: ['v3.1 high fidelity + detailed textures', 'append --then decimate:5000 for LODs', 'use fbx_preset to match Blender/3dsMax'],
|
|
31
|
-
},
|
|
25
|
+
notes: ['P2 editable topology + detailed textures', 'append --then decimate:5000 for LODs', 'P2 costs more credits than H/P1; check credits before running', 'use fbx_preset to match Blender/3dsMax'],
|
|
32
26
|
},
|
|
33
27
|
{
|
|
34
28
|
id: 'film',
|
|
35
|
-
label:
|
|
36
|
-
description:
|
|
29
|
+
label: 'Film / offline rendering',
|
|
30
|
+
description: 'Subdivision-friendly quads, maximum quality',
|
|
37
31
|
model: MODEL_V31,
|
|
38
32
|
genParams: { texture: true, pbr: true, quad: true, geometry_quality: 'detailed', texture_quality: 'detailed' },
|
|
39
33
|
chain: [],
|
|
40
34
|
deliverables: ['FBX (quad)', 'USDZ/OBJ via convert'],
|
|
41
|
-
notes:
|
|
42
|
-
zh: ['quad=true 输出 FBX(quad 进不了 GLB)', 'geometry detailed 加收 HD credit', '进 USD 管线追加 --then convert:usdz'],
|
|
43
|
-
en: ['quad=true outputs FBX (quads cannot be stored in GLB)', 'geometry detailed bills an extra HD credit', 'append --then convert:usdz for USD pipelines'],
|
|
44
|
-
},
|
|
35
|
+
notes: ['quad=true outputs FBX (quads cannot be stored in GLB)', 'geometry detailed bills an extra HD credit', 'append --then convert:usdz for USD pipelines'],
|
|
45
36
|
},
|
|
46
37
|
{
|
|
47
38
|
id: 'print',
|
|
48
|
-
label:
|
|
49
|
-
description:
|
|
39
|
+
label: '3D printing',
|
|
40
|
+
description: 'Watertight printable geometry, skipping textures saves credits',
|
|
50
41
|
model: MODEL_V31,
|
|
51
42
|
genParams: { texture: false, pbr: false },
|
|
52
43
|
chain: [{ step: 'convert', args: { format: 'STL', flatten_bottom: true } }],
|
|
53
|
-
deliverables: ['STL'
|
|
54
|
-
notes:
|
|
55
|
-
zh: ['白模生成,不花贴图积分', 'flatten_bottom 平底利于 FDM', '彩色打印改 --then convert:3mf,export_vertex_colors=true'],
|
|
56
|
-
en: ['bare geometry: no texture credits spent', 'flatten_bottom helps FDM adhesion', 'for color prints use --then convert:3mf,export_vertex_colors=true'],
|
|
57
|
-
},
|
|
44
|
+
deliverables: ['STL'],
|
|
45
|
+
notes: ['bare geometry: no texture credits spent', 'flatten_bottom helps FDM adhesion', 'for color prints add -p texture=true and --then convert:format=OBJ,export_vertex_colors=true (API 3MF output is single color)'],
|
|
58
46
|
},
|
|
59
47
|
{
|
|
60
48
|
id: 'ar-web',
|
|
61
|
-
label:
|
|
62
|
-
description:
|
|
63
|
-
model:
|
|
64
|
-
genParams: { texture: true, pbr: true },
|
|
65
|
-
chain: [
|
|
66
|
-
|
|
67
|
-
{ step: 'convert', args: { format: 'USDZ' } },
|
|
68
|
-
],
|
|
49
|
+
label: 'AR / Web / e-commerce',
|
|
50
|
+
description: 'Mobile performance budgets with dual glb+usdz delivery',
|
|
51
|
+
model: MODEL_P2,
|
|
52
|
+
genParams: { texture: true, pbr: true, face_limit: 20000 },
|
|
53
|
+
chain: [{ step: 'convert', args: { format: 'USDZ' } }],
|
|
54
|
+
downloadGeneration: true,
|
|
69
55
|
deliverables: ['GLB', 'USDZ'],
|
|
70
|
-
notes:
|
|
71
|
-
zh: ['v3.1 生成后减面到 2 万内', 'iOS QuickLook 需要 USDZ', 'decimate 默认 bake 烘焙贴图到低模'],
|
|
72
|
-
en: ['generate with v3.1, then decimate under 20k faces', 'iOS QuickLook requires USDZ', 'decimate bakes textures onto the low-poly by default'],
|
|
73
|
-
},
|
|
56
|
+
notes: ['P2 generates under 20k faces directly (no decimate step)', 'iOS QuickLook requires USDZ', 'P2 costs more credits than H/P1; check credits before running', 'downloads both the generated GLB and the USDZ'],
|
|
74
57
|
},
|
|
75
58
|
{
|
|
76
59
|
id: 'anim',
|
|
77
|
-
label:
|
|
78
|
-
description:
|
|
79
|
-
model:
|
|
60
|
+
label: 'Character animation',
|
|
61
|
+
description: 'Full rig + retarget pipeline',
|
|
62
|
+
model: MODEL_P2,
|
|
80
63
|
genParams: { texture: true, pbr: true },
|
|
81
64
|
chain: [
|
|
82
65
|
{ step: 'rig-check', args: {} },
|
|
83
|
-
{ step: 'rig', args: { model: CAPABILITY_DEFAULTS.rig,
|
|
66
|
+
{ step: 'rig', args: { model: CAPABILITY_DEFAULTS.rig, out_format: 'glb' } },
|
|
84
67
|
{ step: 'retarget', args: { animations: ['preset:idle', 'preset:walk'] } },
|
|
85
68
|
],
|
|
86
69
|
deliverables: ['GLB', 'FBX'],
|
|
87
|
-
notes:
|
|
88
|
-
zh: ['先 rig-check 预检,不可绑骨即止损', '接 Mixamo 用 rig:spec=mixamo,Unity Humanoid / Unreal 用 spec=unity / spec=unreal', '提示词建议 T-pose 站姿,或先 text-to-image template=t_pose'],
|
|
89
|
-
en: ['rig-check first: stop early if not riggable', 'use rig:spec=mixamo for Mixamo, spec=unity / spec=unreal for Unity Humanoid / Unreal', 'prompt for a T-pose, or pre-generate with text-to-image template=t_pose'],
|
|
90
|
-
},
|
|
70
|
+
notes: ['P2 costs more credits than H/P1; check credits before running', 'rig-check first: stop early if not riggable', 'use rig:spec=mixamo for Mixamo, spec=unity / spec=unreal for Unity Humanoid / Unreal', 'prompt for a T-pose, or pre-generate with text-to-image template=t_pose'],
|
|
91
71
|
},
|
|
92
72
|
{
|
|
93
73
|
id: 'toy',
|
|
94
|
-
label:
|
|
95
|
-
description:
|
|
96
|
-
model:
|
|
74
|
+
label: 'Stylized toy / desk piece',
|
|
75
|
+
description: 'Cute low-poly desk piece',
|
|
76
|
+
model: MODEL_P2,
|
|
97
77
|
genParams: { texture: true, pbr: true, face_limit: 20000 },
|
|
98
78
|
chain: [],
|
|
99
79
|
deliverables: ['GLB', 'STL via convert'],
|
|
100
|
-
notes:
|
|
101
|
-
zh: ['P1 出干净低模', '要打印实物追加 --then convert:stl'],
|
|
102
|
-
en: ['P1 produces clean low-poly', 'append --then convert:stl to print it'],
|
|
103
|
-
},
|
|
80
|
+
notes: ['P2 produces editable low-poly', 'P2 costs more credits than H/P1; check credits before running', 'append --then convert:stl to print it'],
|
|
104
81
|
},
|
|
105
82
|
];
|
|
106
83
|
const byId = new Map(SCENARIOS.map((s) => [s.id, s]));
|