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,110 @@
|
|
|
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
|
+
export const MODEL_V31 = 'tripo-v3.1';
|
|
9
|
+
export const MODEL_P1 = 'tripo-p1';
|
|
10
|
+
/** Wire aliases accepted by the API for the two production models. */
|
|
11
|
+
export const MODEL_ALIASES = {
|
|
12
|
+
'v3.1': MODEL_V31,
|
|
13
|
+
'v3.1-20260211': MODEL_V31,
|
|
14
|
+
'tripo-v3.1': MODEL_V31,
|
|
15
|
+
p1: MODEL_P1,
|
|
16
|
+
P1: MODEL_P1,
|
|
17
|
+
'P1-20260311': MODEL_P1,
|
|
18
|
+
'tripo-p1': MODEL_P1,
|
|
19
|
+
};
|
|
20
|
+
/** Other versions the API accepts; the CLI passes them through when explicit. */
|
|
21
|
+
export const LEGACY_3D_MODELS = ['tripo-turbo', 'tripo-v3.0', 'tripo-v2.5', 'tripo-v2.0'];
|
|
22
|
+
export const P1_FACE_LIMIT = { min: 48, max: 20000 };
|
|
23
|
+
/** Parameters the server rejects for P1 — stripped locally with a warning. */
|
|
24
|
+
export const P1_FORBIDDEN_PARAMS = [
|
|
25
|
+
'quad',
|
|
26
|
+
'smart_low_poly',
|
|
27
|
+
'generate_parts',
|
|
28
|
+
'geometry_quality',
|
|
29
|
+
];
|
|
30
|
+
/** Default versions for non-generation capabilities. */
|
|
31
|
+
export const CAPABILITY_DEFAULTS = {
|
|
32
|
+
texture: 'v3.0-20250812',
|
|
33
|
+
/** Server default is rig-v1.0 (biped-only presets); the CLI defaults to
|
|
34
|
+
* rig-v2.0 which has presets for all seven body types. */
|
|
35
|
+
rig: 'rig-v2.0',
|
|
36
|
+
segment: 'v1.0-20250506',
|
|
37
|
+
textToImage: 'seedream_v4',
|
|
38
|
+
imageToImage: 'seedream_v5',
|
|
39
|
+
};
|
|
40
|
+
export const RIG_TYPES = [
|
|
41
|
+
'biped',
|
|
42
|
+
'quadruped',
|
|
43
|
+
'hexapod',
|
|
44
|
+
'octopod',
|
|
45
|
+
'avian',
|
|
46
|
+
'serpentine',
|
|
47
|
+
'aquatic',
|
|
48
|
+
];
|
|
49
|
+
export const RIG_SPECS = ['tripo', 'mixamo'];
|
|
50
|
+
export const IMAGE_MODELS = [
|
|
51
|
+
'seedream_v4',
|
|
52
|
+
'seedream_v5',
|
|
53
|
+
'banana',
|
|
54
|
+
'banana_pro',
|
|
55
|
+
'banana2',
|
|
56
|
+
'chat_image_1',
|
|
57
|
+
'chat_image_1.5',
|
|
58
|
+
'chat_image_2',
|
|
59
|
+
];
|
|
60
|
+
export const IMAGE_TEMPLATES = [
|
|
61
|
+
'asset_extraction',
|
|
62
|
+
'character_completion',
|
|
63
|
+
't_pose',
|
|
64
|
+
'head_extraction',
|
|
65
|
+
'3d_enhance',
|
|
66
|
+
'variants',
|
|
67
|
+
'print_clay',
|
|
68
|
+
'figure',
|
|
69
|
+
];
|
|
70
|
+
export const CONVERT_FORMATS = ['GLTF', 'USDZ', 'FBX', 'OBJ', 'STL', '3MF'];
|
|
71
|
+
export const TEXTURE_FORMATS = [
|
|
72
|
+
'BMP',
|
|
73
|
+
'DPX',
|
|
74
|
+
'HDR',
|
|
75
|
+
'JPEG',
|
|
76
|
+
'OPEN_EXR',
|
|
77
|
+
'PNG',
|
|
78
|
+
'TARGA',
|
|
79
|
+
'TIFF',
|
|
80
|
+
'WEBP',
|
|
81
|
+
];
|
|
82
|
+
export const FBX_PRESETS = ['blender', '3dsmax', 'mixamo'];
|
|
83
|
+
export const EXPORT_ORIENTATIONS = ['+x', '-x', '+y', '-y'];
|
|
84
|
+
export const STYLIZE_STYLES = ['lego', 'voxel', 'voronoi', 'minecraft'];
|
|
85
|
+
export const TEXTURE_QUALITIES = ['standard', 'detailed', 'extreme'];
|
|
86
|
+
/** Normalize any accepted alias to the canonical wire value; unknown values pass through. */
|
|
87
|
+
export function normalizeModel(input) {
|
|
88
|
+
return MODEL_ALIASES[input] ?? input;
|
|
89
|
+
}
|
|
90
|
+
export function isP1(model) {
|
|
91
|
+
return normalizeModel(model) === MODEL_P1;
|
|
92
|
+
}
|
|
93
|
+
/** The one rule. Returns the model plus a human-readable reason. */
|
|
94
|
+
export function chooseModel(intent) {
|
|
95
|
+
if (intent.explicit) {
|
|
96
|
+
return { model: normalizeModel(intent.explicit), reason: 'explicit --model' };
|
|
97
|
+
}
|
|
98
|
+
if (intent.lowPoly) {
|
|
99
|
+
return { model: MODEL_P1, reason: 'low-poly intent → P1' };
|
|
100
|
+
}
|
|
101
|
+
if (intent.faceLimit !== undefined && intent.faceLimit <= P1_FACE_LIMIT.max) {
|
|
102
|
+
return { model: MODEL_P1, reason: `face budget ${intent.faceLimit} ≤ ${P1_FACE_LIMIT.max} → P1` };
|
|
103
|
+
}
|
|
104
|
+
return { model: MODEL_V31, reason: 'default high-fidelity → v3.1' };
|
|
105
|
+
}
|
|
106
|
+
/** Keywords that signal low-poly intent in a prompt (used by make/ai). */
|
|
107
|
+
const LOW_POLY_HINTS = [/low[\s_-]?poly/i, /低模/, /低面/, /手游/, /voxel/i, /像素风/];
|
|
108
|
+
export function detectLowPolyIntent(text) {
|
|
109
|
+
return LOW_POLY_HINTS.some((re) => re.test(text));
|
|
110
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
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
|
+
/** Validate parameters for mesh/decimate. */
|
|
144
|
+
export function validateDecimateParams(input) {
|
|
145
|
+
const params = { ...input };
|
|
146
|
+
const warnings = [];
|
|
147
|
+
const errors = [];
|
|
148
|
+
if (typeof params.face_limit === 'number' && (params.face_limit < 500 || params.face_limit > 20000)) {
|
|
149
|
+
errors.push('decimate face_limit must be within 500–20000');
|
|
150
|
+
}
|
|
151
|
+
return { params, warnings, errors };
|
|
152
|
+
}
|
|
153
|
+
/** Validate retarget animations list. */
|
|
154
|
+
export function validateRetargetParams(input) {
|
|
155
|
+
const params = { ...input };
|
|
156
|
+
const warnings = [];
|
|
157
|
+
const errors = [];
|
|
158
|
+
const anims = params.animations;
|
|
159
|
+
if (Array.isArray(anims)) {
|
|
160
|
+
if (anims.length === 0)
|
|
161
|
+
errors.push('animations must not be empty');
|
|
162
|
+
if (anims.length > 5)
|
|
163
|
+
errors.push(`retarget accepts at most 5 animations per task (got ${anims.length}); billing is per animation`);
|
|
164
|
+
if (anims.length === 1) {
|
|
165
|
+
// Server accepts either `animation` (single) or `animations` (list).
|
|
166
|
+
params.animation = anims[0];
|
|
167
|
+
delete params.animations;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
else if (typeof params.animation !== 'string') {
|
|
171
|
+
errors.push('retarget requires animation (single) or animations (list)');
|
|
172
|
+
}
|
|
173
|
+
return { params, warnings, errors };
|
|
174
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scenario decision matrix: --for presets and the AI wizard both read this
|
|
3
|
+
* table. Changing production strategy means editing data here, not logic.
|
|
4
|
+
*/
|
|
5
|
+
import { MODEL_P1, MODEL_V31 } from './models.js';
|
|
6
|
+
export const SCENARIOS = [
|
|
7
|
+
{
|
|
8
|
+
id: 'game-mobile',
|
|
9
|
+
label: { zh: '手游/低模', en: 'Mobile game / low-poly' },
|
|
10
|
+
description: { zh: '移动端游戏资产,面数预算严格,干净拓扑', en: 'Mobile game assets with strict face budgets and clean topology' },
|
|
11
|
+
model: MODEL_P1,
|
|
12
|
+
genParams: { texture: true, pbr: true, texture_quality: 'standard', face_limit: 15000 },
|
|
13
|
+
chain: [{ step: 'convert', args: { format: 'FBX', texture_size: 2048 } }],
|
|
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
|
+
},
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
id: 'game-pc',
|
|
22
|
+
label: { zh: '端游/主机', en: 'PC / console game' },
|
|
23
|
+
description: { zh: '高预算引擎资产,PBR 全开', en: 'High-budget engine assets with full PBR' },
|
|
24
|
+
model: MODEL_V31,
|
|
25
|
+
genParams: { texture: true, pbr: true, texture_quality: 'detailed' },
|
|
26
|
+
chain: [{ step: 'convert', args: { format: 'FBX' } }],
|
|
27
|
+
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
|
+
},
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
id: 'film',
|
|
35
|
+
label: { zh: '影视/离线渲染', en: 'Film / offline rendering' },
|
|
36
|
+
description: { zh: '细分友好四边面,规格拉满', en: 'Subdivision-friendly quads, maximum quality' },
|
|
37
|
+
model: MODEL_V31,
|
|
38
|
+
genParams: { texture: true, pbr: true, quad: true, geometry_quality: 'detailed', texture_quality: 'detailed' },
|
|
39
|
+
chain: [],
|
|
40
|
+
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
|
+
},
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
id: 'print',
|
|
48
|
+
label: { zh: '3D 打印', en: '3D printing' },
|
|
49
|
+
description: { zh: '水密可打印几何,免贴图省积分', en: 'Watertight printable geometry, skipping textures saves credits' },
|
|
50
|
+
model: MODEL_V31,
|
|
51
|
+
genParams: { texture: false, pbr: false },
|
|
52
|
+
chain: [{ step: 'convert', args: { format: 'STL', flatten_bottom: true } }],
|
|
53
|
+
deliverables: ['STL', '3MF (colored)'],
|
|
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
|
+
},
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
id: 'ar-web',
|
|
61
|
+
label: { zh: 'AR/Web/电商', en: 'AR / Web / e-commerce' },
|
|
62
|
+
description: { zh: '移动端性能预算,glb+usdz 双交付', en: 'Mobile performance budgets with dual glb+usdz delivery' },
|
|
63
|
+
model: MODEL_V31,
|
|
64
|
+
genParams: { texture: true, pbr: true },
|
|
65
|
+
chain: [
|
|
66
|
+
{ step: 'decimate', args: { face_limit: 20000 } },
|
|
67
|
+
{ step: 'convert', args: { format: 'USDZ' } },
|
|
68
|
+
],
|
|
69
|
+
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
|
+
},
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
id: 'anim',
|
|
77
|
+
label: { zh: '角色动画', en: 'Character animation' },
|
|
78
|
+
description: { zh: '绑骨+动作重定向全链路', en: 'Full rig + retarget pipeline' },
|
|
79
|
+
model: MODEL_V31,
|
|
80
|
+
genParams: { texture: true, pbr: true },
|
|
81
|
+
chain: [
|
|
82
|
+
{ step: 'rig-check', args: {} },
|
|
83
|
+
{ step: 'rig', args: { model: 'rig-v2.0', spec: 'tripo', out_format: 'glb' } },
|
|
84
|
+
{ step: 'retarget', args: { animations: ['preset:idle', 'preset:walk'] } },
|
|
85
|
+
],
|
|
86
|
+
deliverables: ['GLB', 'FBX'],
|
|
87
|
+
notes: {
|
|
88
|
+
zh: ['先 rig-check 预检,不可绑骨即止损', '接 Mixamo/Unity Humanoid 用 rig:spec=mixamo', '提示词建议 T-pose 站姿,或先 text-to-image template=t_pose'],
|
|
89
|
+
en: ['rig-check first: stop early if not riggable', 'use rig:spec=mixamo for Mixamo/Unity Humanoid', 'prompt for a T-pose, or pre-generate with text-to-image template=t_pose'],
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
id: 'toy',
|
|
94
|
+
label: { zh: '风格化摆件', en: 'Stylized toy / desk piece' },
|
|
95
|
+
description: { zh: '低模可爱风或积木/体素风格化', en: 'Cute low-poly or brick/voxel stylization' },
|
|
96
|
+
model: MODEL_P1,
|
|
97
|
+
genParams: { texture: true, pbr: true, face_limit: 20000 },
|
|
98
|
+
chain: [],
|
|
99
|
+
deliverables: ['GLB', 'STL via convert'],
|
|
100
|
+
notes: {
|
|
101
|
+
zh: ['P1 出干净低模', '积木/体素风追加 --then stylize:lego 或 stylize:voxel', '要打印实物追加 --then convert:stl'],
|
|
102
|
+
en: ['P1 produces clean low-poly', 'append --then stylize:lego or stylize:voxel for brick/voxel looks', 'append --then convert:stl to print it'],
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
];
|
|
106
|
+
const byId = new Map(SCENARIOS.map((s) => [s.id, s]));
|
|
107
|
+
export function getScenario(id) {
|
|
108
|
+
return byId.get(id);
|
|
109
|
+
}
|
|
110
|
+
export function scenarioIds() {
|
|
111
|
+
return SCENARIOS.map((s) => s.id);
|
|
112
|
+
}
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `make` orchestrator — one entry point that turns any input (text /
|
|
3
|
+
* image / multiview / model file / task ref) into finished artifacts,
|
|
4
|
+
* applying the dual-model rule, scenario presets, and the processing chain.
|
|
5
|
+
* Shared by `tripo make`, `tripo ai`, `tripo redo`, batch, and the MCP server.
|
|
6
|
+
*/
|
|
7
|
+
import { basename } from 'node:path';
|
|
8
|
+
import { UsageError } from '../core/errors.js';
|
|
9
|
+
import { log, warn } from '../core/output.js';
|
|
10
|
+
import { buildImageToModel, buildMultiviewToModel, buildStepRequest, buildTextToModel, } from '../core/requests.js';
|
|
11
|
+
import { precheckBalance, resolveInputValue, runTask } from '../core/task-service.js';
|
|
12
|
+
import { t } from '../i18n.js';
|
|
13
|
+
import { validateChain, ChainError } from '../knowledge/chains.js';
|
|
14
|
+
import { chooseModel, detectLowPolyIntent } from '../knowledge/models.js';
|
|
15
|
+
import { getScenario } from '../knowledge/scenarios.js';
|
|
16
|
+
import { classifyInputs } from '../util/input-detect.js';
|
|
17
|
+
import { runChain, assertSourceReady } from './runner.js';
|
|
18
|
+
/** Build the full plan (no network calls except none): what model, what chain, why. */
|
|
19
|
+
export async function planMake(client, options) {
|
|
20
|
+
const classified = classifyInputs(options.inputs);
|
|
21
|
+
const scenario = options.scenario ? getScenario(options.scenario) : undefined;
|
|
22
|
+
if (options.scenario && !scenario) {
|
|
23
|
+
throw new UsageError(`unknown scenario "${options.scenario}" (valid: game-mobile, game-pc, film, print, ar-web, anim, toy)`);
|
|
24
|
+
}
|
|
25
|
+
const userParams = options.params ?? {};
|
|
26
|
+
const genParams = { ...(scenario?.genParams ?? {}), ...userParams };
|
|
27
|
+
const chain = options.chain && options.chain.length > 0 ? options.chain : (scenario?.chain ?? []);
|
|
28
|
+
const warnings = [];
|
|
29
|
+
if (options.chain && options.chain.length > 0 && scenario && scenario.chain.length > 0) {
|
|
30
|
+
warnings.push(t({
|
|
31
|
+
zh: `--then 覆盖了 ${scenario.id} 场景默认加工链`,
|
|
32
|
+
en: `--then overrides the default ${scenario.id} processing chain`,
|
|
33
|
+
}));
|
|
34
|
+
}
|
|
35
|
+
switch (classified.kind) {
|
|
36
|
+
case 'text': {
|
|
37
|
+
const prompt = classified.values[0].value;
|
|
38
|
+
const choice = chooseModel({
|
|
39
|
+
explicit: options.model ?? scenario?.model,
|
|
40
|
+
faceLimit: typeof genParams.face_limit === 'number' ? genParams.face_limit : undefined,
|
|
41
|
+
lowPoly: detectLowPolyIntent(prompt),
|
|
42
|
+
});
|
|
43
|
+
const generation = buildTextToModel(prompt, choice.model, genParams);
|
|
44
|
+
return {
|
|
45
|
+
generation,
|
|
46
|
+
chain,
|
|
47
|
+
model: choice.model,
|
|
48
|
+
modelReason: choice.reason,
|
|
49
|
+
name: options.name ?? prompt,
|
|
50
|
+
warnings: [...warnings, ...generation.warnings],
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
case 'image':
|
|
54
|
+
case 'url-image': {
|
|
55
|
+
const raw = classified.values[0].value;
|
|
56
|
+
const { value } = await resolveInputValue(client, raw);
|
|
57
|
+
const choice = chooseModel({
|
|
58
|
+
explicit: options.model ?? scenario?.model,
|
|
59
|
+
faceLimit: typeof genParams.face_limit === 'number' ? genParams.face_limit : undefined,
|
|
60
|
+
});
|
|
61
|
+
const generation = buildImageToModel(value, choice.model, genParams);
|
|
62
|
+
return {
|
|
63
|
+
generation,
|
|
64
|
+
chain,
|
|
65
|
+
model: choice.model,
|
|
66
|
+
modelReason: choice.reason,
|
|
67
|
+
name: options.name ?? basename(raw),
|
|
68
|
+
warnings: [...warnings, ...generation.warnings],
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
case 'multiview': {
|
|
72
|
+
const views = await assignViews(client, classified.values.map((v) => v.value));
|
|
73
|
+
const choice = chooseModel({
|
|
74
|
+
explicit: options.model ?? scenario?.model,
|
|
75
|
+
faceLimit: typeof genParams.face_limit === 'number' ? genParams.face_limit : undefined,
|
|
76
|
+
});
|
|
77
|
+
const generation = buildMultiviewToModel(views, choice.model, genParams);
|
|
78
|
+
return {
|
|
79
|
+
generation,
|
|
80
|
+
chain,
|
|
81
|
+
model: choice.model,
|
|
82
|
+
modelReason: choice.reason,
|
|
83
|
+
name: options.name ?? basename(classified.values[0].value),
|
|
84
|
+
warnings: [...warnings, ...generation.warnings],
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
case 'model': {
|
|
88
|
+
const raw = classified.values[0].value;
|
|
89
|
+
const { value } = await resolveInputValue(client, raw);
|
|
90
|
+
const generation = buildStepRequest('import', value, {});
|
|
91
|
+
if (chain.length === 0) {
|
|
92
|
+
warnings.push(t({
|
|
93
|
+
zh: '导入模型后没有加工步骤;用 --then 指定(如 --then texture,rig)',
|
|
94
|
+
en: 'importing a model with no processing steps; add --then (e.g. --then texture,rig)',
|
|
95
|
+
}));
|
|
96
|
+
}
|
|
97
|
+
return {
|
|
98
|
+
generation,
|
|
99
|
+
chain,
|
|
100
|
+
name: options.name ?? basename(raw),
|
|
101
|
+
warnings: [...warnings, ...generation.warnings],
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
case 'task': {
|
|
105
|
+
const { value, sourceTaskType } = await resolveInputValue(client, classified.values[0].value);
|
|
106
|
+
if (chain.length === 0) {
|
|
107
|
+
throw new UsageError('a task reference needs processing steps: add --then (e.g. tripo make @last --then texture,convert:fbx)');
|
|
108
|
+
}
|
|
109
|
+
return {
|
|
110
|
+
sourceTaskId: value,
|
|
111
|
+
sourceType: sourceTaskType,
|
|
112
|
+
chain,
|
|
113
|
+
name: options.name ?? value.slice(0, 8),
|
|
114
|
+
warnings,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
/** Execute a plan produced by planMake. */
|
|
120
|
+
export async function executeMake(client, plan, options) {
|
|
121
|
+
for (const w of plan.warnings)
|
|
122
|
+
warn(w);
|
|
123
|
+
// Validate chain legality up front so an illegal plan spends zero credits.
|
|
124
|
+
if (plan.chain.length > 0) {
|
|
125
|
+
const upstream = plan.generation?.taskType ?? plan.sourceType;
|
|
126
|
+
if (upstream) {
|
|
127
|
+
try {
|
|
128
|
+
validateChain(plan.chain, upstream);
|
|
129
|
+
}
|
|
130
|
+
catch (error) {
|
|
131
|
+
if (error instanceof ChainError)
|
|
132
|
+
throw new UsageError(error.message);
|
|
133
|
+
throw error;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
await precheckBalance(client);
|
|
138
|
+
let sourceTaskId;
|
|
139
|
+
let sourceType;
|
|
140
|
+
let primary;
|
|
141
|
+
if (plan.generation) {
|
|
142
|
+
if (plan.model && plan.modelReason) {
|
|
143
|
+
log(t({ zh: `模型选择:${plan.model}(${plan.modelReason})`, en: `model: ${plan.model} (${plan.modelReason})` }));
|
|
144
|
+
}
|
|
145
|
+
primary = await runTask({
|
|
146
|
+
client,
|
|
147
|
+
request: plan.generation.request,
|
|
148
|
+
taskType: plan.generation.taskType,
|
|
149
|
+
name: plan.name,
|
|
150
|
+
outDir: options.outDir,
|
|
151
|
+
noWait: options.noWait,
|
|
152
|
+
noDownload: options.noDownload || plan.chain.length > 0,
|
|
153
|
+
timeoutMs: options.timeoutMs,
|
|
154
|
+
notify: options.notify && plan.chain.length === 0,
|
|
155
|
+
precheckBalance: false,
|
|
156
|
+
});
|
|
157
|
+
if (options.noWait)
|
|
158
|
+
return { primary, chainResults: [] };
|
|
159
|
+
sourceTaskId = primary.task_id;
|
|
160
|
+
sourceType = plan.generation.taskType;
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
sourceTaskId = plan.sourceTaskId;
|
|
164
|
+
sourceType = plan.sourceType ?? (await assertSourceReady(client, sourceTaskId));
|
|
165
|
+
primary = { task_id: sourceTaskId, type: sourceType, status: 'success' };
|
|
166
|
+
}
|
|
167
|
+
if (plan.chain.length === 0) {
|
|
168
|
+
return { primary, chainResults: [] };
|
|
169
|
+
}
|
|
170
|
+
const { results, stoppedEarly } = await runChain({
|
|
171
|
+
client,
|
|
172
|
+
chain: plan.chain,
|
|
173
|
+
sourceTaskId,
|
|
174
|
+
sourceType,
|
|
175
|
+
namePrefix: plan.name,
|
|
176
|
+
outDir: options.outDir,
|
|
177
|
+
notify: false,
|
|
178
|
+
timeoutMs: options.timeoutMs,
|
|
179
|
+
});
|
|
180
|
+
// If the chain stopped early (not riggable) and nothing was downloaded yet,
|
|
181
|
+
// fetch the source artifacts so the user still gets their model.
|
|
182
|
+
if (stoppedEarly && plan.generation && !options.noDownload) {
|
|
183
|
+
const detail = await client.getTask(sourceTaskId);
|
|
184
|
+
const { downloadArtifacts, defaultOutputDir } = await import('../core/download.js');
|
|
185
|
+
const { slugify } = await import('../config/history.js');
|
|
186
|
+
const dir = defaultOutputDir(options.outDir ?? process.cwd(), slugify(plan.name), sourceTaskId);
|
|
187
|
+
const artifacts = await downloadArtifacts(detail, dir);
|
|
188
|
+
primary.output_dir = artifacts.outputDir;
|
|
189
|
+
primary.model_file = artifacts.modelFile;
|
|
190
|
+
primary.preview = artifacts.preview;
|
|
191
|
+
}
|
|
192
|
+
return { primary, chainResults: results, stoppedEarly };
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Map 2–4 images to views. Filename hints (front/back/left/right, 前/后/左/右)
|
|
196
|
+
* win; remaining slots fill positionally in [front, left, back, right] order.
|
|
197
|
+
*/
|
|
198
|
+
export async function assignViews(client, files) {
|
|
199
|
+
if (files.length < 2 || files.length > 4) {
|
|
200
|
+
throw new UsageError(`multiview needs 2–4 images (got ${files.length})`);
|
|
201
|
+
}
|
|
202
|
+
const hints = {
|
|
203
|
+
front: /front|正面|前/i,
|
|
204
|
+
back: /back|背面|后/i,
|
|
205
|
+
left: /left|左/i,
|
|
206
|
+
right: /right|右/i,
|
|
207
|
+
};
|
|
208
|
+
const views = {};
|
|
209
|
+
const unassigned = [];
|
|
210
|
+
for (const file of files) {
|
|
211
|
+
const name = basename(file);
|
|
212
|
+
const hit = Object.entries(hints).find(([view, re]) => !views[view] && re.test(name));
|
|
213
|
+
if (hit)
|
|
214
|
+
views[hit[0]] = file;
|
|
215
|
+
else
|
|
216
|
+
unassigned.push(file);
|
|
217
|
+
}
|
|
218
|
+
const order = ['front', 'left', 'back', 'right'];
|
|
219
|
+
for (const file of unassigned) {
|
|
220
|
+
const slot = order.find((v) => !views[v]);
|
|
221
|
+
if (!slot)
|
|
222
|
+
throw new UsageError('too many images for 4 view slots');
|
|
223
|
+
views[slot] = file;
|
|
224
|
+
}
|
|
225
|
+
if (!views.front) {
|
|
226
|
+
throw new UsageError('multiview requires a front view — name one file with "front" or pass it first');
|
|
227
|
+
}
|
|
228
|
+
const resolved = {};
|
|
229
|
+
for (const [view, file] of Object.entries(views)) {
|
|
230
|
+
resolved[view] = (await resolveInputValue(client, file)).value;
|
|
231
|
+
}
|
|
232
|
+
return resolved;
|
|
233
|
+
}
|