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.
Files changed (123) hide show
  1. package/README.md +105 -0
  2. package/dist/ai/experts.js +115 -0
  3. package/dist/ai/llm.js +88 -0
  4. package/dist/ai/plan-card.js +79 -0
  5. package/dist/ai/wizard.js +105 -0
  6. package/dist/cli.js +7 -0
  7. package/dist/commands/account.js +27 -0
  8. package/dist/commands/ai.js +114 -0
  9. package/dist/commands/batch.js +148 -0
  10. package/dist/commands/completion.js +84 -0
  11. package/dist/commands/config-cmd.js +84 -0
  12. package/dist/commands/docs.js +30 -0
  13. package/dist/commands/doctor.js +102 -0
  14. package/dist/commands/generate.js +81 -0
  15. package/dist/commands/home.js +68 -0
  16. package/dist/commands/login.js +309 -0
  17. package/dist/commands/make.js +142 -0
  18. package/dist/commands/mcp.js +194 -0
  19. package/dist/commands/process-groups.js +166 -0
  20. package/dist/commands/shared.js +75 -0
  21. package/dist/commands/task.js +128 -0
  22. package/dist/commands/topup.js +61 -0
  23. package/dist/commands/view.js +97 -0
  24. package/dist/config/config.js +99 -0
  25. package/dist/config/context.js +44 -0
  26. package/dist/config/history.js +104 -0
  27. package/dist/core/client.js +129 -0
  28. package/dist/core/device-auth.js +141 -0
  29. package/dist/core/download.js +107 -0
  30. package/dist/core/errors.js +64 -0
  31. package/dist/core/output.js +72 -0
  32. package/dist/core/region.js +45 -0
  33. package/dist/core/requests.js +197 -0
  34. package/dist/core/task-service.js +167 -0
  35. package/dist/core/upload.js +42 -0
  36. package/dist/core/viewer-assets.js +24 -0
  37. package/dist/core/viewer.js +59 -0
  38. package/dist/core/watch.js +42 -0
  39. package/dist/i18n.js +19 -0
  40. package/dist/knowledge/chains.js +157 -0
  41. package/dist/knowledge/error-catalog.js +91 -0
  42. package/dist/knowledge/models.js +158 -0
  43. package/dist/knowledge/params.js +203 -0
  44. package/dist/knowledge/scenarios.js +112 -0
  45. package/dist/pipeline/make.js +233 -0
  46. package/dist/pipeline/runner.js +73 -0
  47. package/dist/pipeline/stdin.js +40 -0
  48. package/dist/program.js +147 -0
  49. package/dist/src/ai/experts.js +115 -0
  50. package/dist/src/ai/llm.js +88 -0
  51. package/dist/src/ai/plan-card.js +79 -0
  52. package/dist/src/ai/wizard.js +104 -0
  53. package/dist/src/cli.js +7 -0
  54. package/dist/src/commands/account.js +27 -0
  55. package/dist/src/commands/ai.js +113 -0
  56. package/dist/src/commands/batch.js +148 -0
  57. package/dist/src/commands/completion.js +84 -0
  58. package/dist/src/commands/config-cmd.js +84 -0
  59. package/dist/src/commands/docs.js +30 -0
  60. package/dist/src/commands/doctor.js +94 -0
  61. package/dist/src/commands/generate.js +81 -0
  62. package/dist/src/commands/home.js +68 -0
  63. package/dist/src/commands/login.js +225 -0
  64. package/dist/src/commands/make.js +142 -0
  65. package/dist/src/commands/mcp.js +194 -0
  66. package/dist/src/commands/process-groups.js +164 -0
  67. package/dist/src/commands/shared.js +75 -0
  68. package/dist/src/commands/task.js +127 -0
  69. package/dist/src/commands/topup.js +61 -0
  70. package/dist/src/commands/view.js +97 -0
  71. package/dist/src/config/config.js +79 -0
  72. package/dist/src/config/context.js +44 -0
  73. package/dist/src/config/history.js +104 -0
  74. package/dist/src/core/client.js +120 -0
  75. package/dist/src/core/download.js +107 -0
  76. package/dist/src/core/errors.js +64 -0
  77. package/dist/src/core/output.js +72 -0
  78. package/dist/src/core/requests.js +169 -0
  79. package/dist/src/core/task-service.js +161 -0
  80. package/dist/src/core/upload.js +36 -0
  81. package/dist/src/core/viewer-assets.js +24 -0
  82. package/dist/src/core/viewer.js +59 -0
  83. package/dist/src/core/watch.js +41 -0
  84. package/dist/src/i18n.js +19 -0
  85. package/dist/src/knowledge/chains.js +152 -0
  86. package/dist/src/knowledge/error-catalog.js +91 -0
  87. package/dist/src/knowledge/models.js +110 -0
  88. package/dist/src/knowledge/params.js +174 -0
  89. package/dist/src/knowledge/scenarios.js +112 -0
  90. package/dist/src/pipeline/make.js +233 -0
  91. package/dist/src/pipeline/runner.js +73 -0
  92. package/dist/src/pipeline/stdin.js +40 -0
  93. package/dist/src/program.js +147 -0
  94. package/dist/src/types.js +2 -0
  95. package/dist/src/util/input-detect.js +92 -0
  96. package/dist/src/util/notify.js +17 -0
  97. package/dist/src/util/open.js +18 -0
  98. package/dist/src/util/spinner.js +62 -0
  99. package/dist/src/util/term-image.js +28 -0
  100. package/dist/src/version.js +10 -0
  101. package/dist/types.js +2 -0
  102. package/dist/util/input-detect.js +93 -0
  103. package/dist/util/notify.js +17 -0
  104. package/dist/util/open.js +18 -0
  105. package/dist/util/spinner.js +62 -0
  106. package/dist/util/term-image.js +28 -0
  107. package/dist/version.js +10 -0
  108. package/package.json +63 -0
  109. package/skill/SKILL.md +70 -0
  110. package/skill/commands/account.md +41 -0
  111. package/skill/commands/batch.md +33 -0
  112. package/skill/commands/generate.md +34 -0
  113. package/skill/commands/make.md +47 -0
  114. package/skill/commands/process.md +50 -0
  115. package/skill/commands/task.md +27 -0
  116. package/skill/commands/view.md +25 -0
  117. package/skill/common-errors.md +38 -0
  118. package/skill/examples/animation.md +33 -0
  119. package/skill/examples/ar-web.md +23 -0
  120. package/skill/examples/film.md +22 -0
  121. package/skill/examples/game-asset.md +35 -0
  122. package/skill/examples/pipes.md +39 -0
  123. package/skill/examples/print.md +31 -0
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Region auto-detection: a Tripo API key is valid in exactly one region
3
+ * (ov = openapi.tripo3d.ai, cn = openapi.tripo3d.com), so instead of asking
4
+ * the user "which region?" we probe both with the key and let the facts
5
+ * decide. First successful probe wins; both failing means the key is dead.
6
+ */
7
+ import { REGION_DEFAULTS } from '../config/config.js';
8
+ import { TripoClient } from './client.js';
9
+ const PROBE_TIMEOUT_MS = 12_000;
10
+ export function defaultProbeBases() {
11
+ return { ov: REGION_DEFAULTS.ov.api, cn: REGION_DEFAULTS.cn.api };
12
+ }
13
+ /**
14
+ * Probe both regions concurrently; resolve as soon as one accepts the key
15
+ * (the other side may be slow/unreachable from this network — e.g. the
16
+ * overseas host from mainland China — and must not delay the answer).
17
+ * Returns undefined when both regions reject or are unreachable.
18
+ */
19
+ export async function detectRegionForKey(key, bases = defaultProbeBases()) {
20
+ const probe = async (region) => {
21
+ try {
22
+ const client = new TripoClient({
23
+ apiKey: key,
24
+ baseUrl: bases[region],
25
+ maxRetries: 0,
26
+ timeoutMs: PROBE_TIMEOUT_MS,
27
+ });
28
+ return { region, balance: await client.getBalance() };
29
+ }
30
+ catch {
31
+ return undefined;
32
+ }
33
+ };
34
+ return new Promise((resolve) => {
35
+ let pending = 2;
36
+ for (const region of ['ov', 'cn']) {
37
+ void probe(region).then((hit) => {
38
+ if (hit)
39
+ resolve(hit);
40
+ else if (--pending === 0)
41
+ resolve(undefined);
42
+ });
43
+ }
44
+ });
45
+ }
@@ -0,0 +1,197 @@
1
+ /**
2
+ * Request builders: turn (step + args + resolved input) into the exact
3
+ * endpoint + JSON payload, applying knowledge-base validation first.
4
+ * Unknown extra args pass through untouched (the server maps them into
5
+ * `parameters`), so new API options work without a CLI release.
6
+ */
7
+ import { getStep } from '../knowledge/chains.js';
8
+ import { validateConvertParams, validateDecimateParams, validateGenerationParams, validateRetargetParams, } from '../knowledge/params.js';
9
+ import { CAPABILITY_DEFAULTS, SEGMENT_MODELS, TEXTURE_MODELS, normalizeModel, normalizeRigModel, } from '../knowledge/models.js';
10
+ import { UsageError } from './errors.js';
11
+ function ensure(result) {
12
+ if (result.errors.length > 0) {
13
+ throw new UsageError(result.errors.join('; '));
14
+ }
15
+ }
16
+ /** 3D generation: text-to-model. */
17
+ export function buildTextToModel(prompt, model, params) {
18
+ const validated = validateGenerationParams(model, params);
19
+ ensure(validated);
20
+ return {
21
+ request: {
22
+ endpoint: '/v3/generation/text-to-model',
23
+ payload: { prompt, model: normalizeModel(model), ...validated.params },
24
+ },
25
+ taskType: 'text_to_model',
26
+ warnings: validated.warnings,
27
+ };
28
+ }
29
+ /** 3D generation: image-to-model (input = file_token/url/task_id). */
30
+ export function buildImageToModel(input, model, params) {
31
+ const validated = validateGenerationParams(model, params);
32
+ ensure(validated);
33
+ return {
34
+ request: {
35
+ endpoint: '/v3/generation/image-to-model',
36
+ payload: { input, model: normalizeModel(model), ...validated.params },
37
+ },
38
+ taskType: 'image_to_model',
39
+ warnings: validated.warnings,
40
+ };
41
+ }
42
+ /** 3D generation: multiview-to-model. Views ordered [front, left, back, right]. */
43
+ export function buildMultiviewToModel(views, model, params) {
44
+ const validated = validateGenerationParams(model, params);
45
+ ensure(validated);
46
+ const inputs = ['front', 'left', 'back', 'right']
47
+ .filter((v) => views[v])
48
+ .map((v) => ({ [v]: views[v] }));
49
+ if (!views.front)
50
+ throw new UsageError('multiview requires a front view');
51
+ if (inputs.length < 2)
52
+ throw new UsageError('multiview requires at least 2 views');
53
+ return {
54
+ request: {
55
+ endpoint: '/v3/generation/multiview-to-model',
56
+ payload: { inputs, model: normalizeModel(model), ...validated.params },
57
+ },
58
+ taskType: 'multiview_to_model',
59
+ warnings: validated.warnings,
60
+ };
61
+ }
62
+ /** Image generation: text-to-image / image-to-image.
63
+ * Server task types (TaskType enum): text-to-image → `text_to_image`,
64
+ * image-to-image → `generate_image`. */
65
+ export function buildTextToImage(prompt, params) {
66
+ return {
67
+ request: {
68
+ endpoint: '/v3/generation/text-to-image',
69
+ payload: { prompt, model: params.model ?? CAPABILITY_DEFAULTS.textToImage, ...omit(params, ['model']) },
70
+ },
71
+ taskType: 'text_to_image',
72
+ warnings: [],
73
+ };
74
+ }
75
+ export function buildImageToImage(input, params) {
76
+ return {
77
+ request: {
78
+ endpoint: '/v3/generation/image-to-image',
79
+ payload: { input, model: params.model ?? CAPABILITY_DEFAULTS.imageToImage, ...omit(params, ['model']) },
80
+ },
81
+ taskType: 'generate_image',
82
+ warnings: [],
83
+ };
84
+ }
85
+ export function buildImageToMultiview(input, params) {
86
+ return {
87
+ request: { endpoint: '/v3/generation/image-to-multiview', payload: { input, ...params } },
88
+ taskType: 'generate_multiview_image',
89
+ warnings: [],
90
+ };
91
+ }
92
+ export function buildEditMultiview(input, params) {
93
+ return {
94
+ request: { endpoint: '/v3/generation/edit-multiview', payload: { input, ...params } },
95
+ taskType: 'edit_multiview_image',
96
+ warnings: [],
97
+ };
98
+ }
99
+ export function buildImageToSplat(input, params) {
100
+ return {
101
+ request: { endpoint: '/v3/generation/image-to-splat', payload: { input, ...params } },
102
+ taskType: 'image_to_splat',
103
+ warnings: [],
104
+ };
105
+ }
106
+ /**
107
+ * Processing step (texture/convert/rig/...): validate per step, then build.
108
+ * `input` must already be a task_id / file_token / URL.
109
+ */
110
+ export function buildStepRequest(stepName, input, args) {
111
+ const def = getStep(stepName);
112
+ if (!def)
113
+ throw new UsageError(`unknown step "${stepName}"`);
114
+ let params = { ...args };
115
+ const warnings = [];
116
+ switch (def.name) {
117
+ case 'convert': {
118
+ const v = validateConvertParams(params);
119
+ ensure(v);
120
+ params = v.params;
121
+ warnings.push(...v.warnings);
122
+ break;
123
+ }
124
+ case 'decimate': {
125
+ const v = validateDecimateParams(params);
126
+ ensure(v);
127
+ params = v.params;
128
+ warnings.push(...v.warnings);
129
+ break;
130
+ }
131
+ case 'retarget': {
132
+ const v = validateRetargetParams(params);
133
+ ensure(v);
134
+ params = v.params;
135
+ warnings.push(...v.warnings);
136
+ break;
137
+ }
138
+ case 'rig': {
139
+ // Marketing names (rig-v1.0/rig-v2.0) are rejected by the server; normalize to wire versions.
140
+ params.model = params.model === undefined
141
+ ? CAPABILITY_DEFAULTS.rig
142
+ : normalizeRigModel(String(params.model));
143
+ break;
144
+ }
145
+ case 'texture': {
146
+ const model = params.model === undefined
147
+ ? CAPABILITY_DEFAULTS.texture
148
+ : normalizeModel(String(params.model));
149
+ if (!TEXTURE_MODELS.includes(model)) {
150
+ throw new UsageError(`texture model must be one of ${TEXTURE_MODELS.join('/')} (got "${String(params.model)}")`);
151
+ }
152
+ params.model = model;
153
+ break;
154
+ }
155
+ case 'segment': {
156
+ if (params.model !== undefined && !SEGMENT_MODELS.includes(String(params.model))) {
157
+ throw new UsageError(`segment model must be one of ${SEGMENT_MODELS.join('/')} (got "${String(params.model)}")`);
158
+ }
159
+ break;
160
+ }
161
+ case 'stylize': {
162
+ if (typeof params.style !== 'string') {
163
+ throw new UsageError('stylize requires style (lego/voxel/voronoi/minecraft)');
164
+ }
165
+ break;
166
+ }
167
+ case 'smartsegment': {
168
+ if (params.seg_type === undefined)
169
+ params.seg_type = 'model';
170
+ // seg_type=model requires a 4×4 column-major transform (16 floats); the
171
+ // identity matrix means "use the model's own coordinates" and avoids a
172
+ // guaranteed 400 when the caller doesn't care about placement.
173
+ if (params.seg_type === 'model' && params.transform === undefined) {
174
+ params.transform = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];
175
+ warnings.push('smartsegment(model) requires a transform matrix; defaulted to identity');
176
+ }
177
+ break;
178
+ }
179
+ }
180
+ // smartsegment's task type depends on seg_type (server: smartsegment_image|smartsegment_model).
181
+ const taskType = def.name === 'smartsegment'
182
+ ? params.seg_type === 'image'
183
+ ? 'smartsegment_image'
184
+ : 'smartsegment_model'
185
+ : def.produces;
186
+ return {
187
+ request: { endpoint: def.endpoint, payload: { input, ...params } },
188
+ taskType,
189
+ warnings,
190
+ };
191
+ }
192
+ function omit(obj, keys) {
193
+ const out = { ...obj };
194
+ for (const k of keys)
195
+ delete out[k];
196
+ return out;
197
+ }
@@ -0,0 +1,167 @@
1
+ /**
2
+ * The shared task engine: resolve inputs, pre-check balance, submit, record
3
+ * history, watch progress, download artifacts, notify. Every task-producing
4
+ * command (make/generate/model/anim/mesh/pipeline/batch) runs through here so
5
+ * behavior is identical everywhere.
6
+ */
7
+ import { basename } from 'node:path';
8
+ import { resolvePlatformBaseUrl, resolveRegion } from '../config/config.js';
9
+ import { loadContext, updateContext } from '../config/context.js';
10
+ import { RefError, appendHistory, readHistory, resolveReference, slugify, updateHistoryStatus } from '../config/history.js';
11
+ import { t } from '../i18n.js';
12
+ import { detectInput } from '../util/input-detect.js';
13
+ import { notifyDone } from '../util/notify.js';
14
+ import { createSpinner } from '../util/spinner.js';
15
+ import { defaultOutputDir, downloadArtifacts } from './download.js';
16
+ import { ApiError, UsageError, balanceSuggestion } from './errors.js';
17
+ import { isQuiet, log, warn } from './output.js';
18
+ import { uploadLocalFile } from './upload.js';
19
+ /** Resolve a user input string into the API `input` value (upload as needed). */
20
+ export async function resolveInputValue(client, raw) {
21
+ const detected = detectInput(raw);
22
+ switch (detected.kind) {
23
+ case 'history-ref': {
24
+ let entry;
25
+ try {
26
+ entry = resolveReference(detected.value);
27
+ }
28
+ catch (err) {
29
+ if (err instanceof RefError)
30
+ throw new UsageError(err.message);
31
+ throw err;
32
+ }
33
+ if (!entry)
34
+ throw new UsageError(`cannot resolve reference ${raw}`);
35
+ return { value: entry.task_id, sourceTaskType: entry.type };
36
+ }
37
+ case 'image-file':
38
+ case 'model-file': {
39
+ const token = await uploadLocalFile(client, detected.value);
40
+ return { value: token };
41
+ }
42
+ case 'task-id':
43
+ return { value: detected.value };
44
+ case 'file-token':
45
+ case 'url':
46
+ return { value: detected.value };
47
+ case 'text':
48
+ throw new UsageError(`expected a file/url/task reference, got text: "${raw}"`);
49
+ }
50
+ }
51
+ /** Balance pre-check: warns when low, errors when empty. */
52
+ export async function precheckBalance(client) {
53
+ let balance;
54
+ try {
55
+ const data = await client.getBalance();
56
+ balance = Number(data.balance);
57
+ }
58
+ catch {
59
+ return; // pre-check must never block task creation on transient errors
60
+ }
61
+ if (Number.isNaN(balance))
62
+ return;
63
+ if (balance <= 0) {
64
+ throw new ApiError({ apiCode: 2010, suggestion: balanceSuggestion(undefined, balance) });
65
+ }
66
+ if (balance < 30) {
67
+ warn(t({
68
+ zh: `余额仅剩 ${balance} 积分,任务可能因余额不足失败(tripo topup 充值)`,
69
+ en: `balance is down to ${balance} credits; tasks may fail (run "tripo topup")`,
70
+ }));
71
+ }
72
+ }
73
+ /** Submit one task and (by default) watch it to completion + download. */
74
+ export async function runTask(options) {
75
+ const { client, request, taskType } = options;
76
+ if (options.precheckBalance !== false)
77
+ await precheckBalance(client);
78
+ const taskId = await client.createTask(request.endpoint, request.payload);
79
+ const name = uniqueName(slugify(options.name));
80
+ const entry = {
81
+ task_id: taskId,
82
+ type: taskType,
83
+ name,
84
+ status: 'queued',
85
+ created_at: new Date().toISOString(),
86
+ endpoint: request.endpoint,
87
+ payload: request.payload,
88
+ };
89
+ appendHistory(entry);
90
+ updateContext({ last_task_id: taskId });
91
+ log(t({ zh: `任务已创建 ${taskId} (@${name})`, en: `task created ${taskId} (@${name})` }));
92
+ if (options.noWait) {
93
+ return { task_id: taskId, type: taskType, status: 'queued' };
94
+ }
95
+ const spinner = createSpinner(`${taskType} · queued`, isQuiet());
96
+ let task;
97
+ try {
98
+ task = await (await import('./watch.js')).watchTask(client, taskId, {
99
+ timeoutMs: options.timeoutMs,
100
+ onProgress: (detail) => {
101
+ const pct = typeof detail.progress === 'number' ? ` ${detail.progress}%` : '';
102
+ spinner.update(`${taskType} · ${detail.status}${pct} · ${taskId.slice(0, 8)}`);
103
+ },
104
+ });
105
+ }
106
+ catch (err) {
107
+ spinner.fail(`${taskType} · ${taskId}`);
108
+ updateHistoryStatus(taskId, 'failed');
109
+ throw err;
110
+ }
111
+ updateHistoryStatus(taskId, 'success', { credits_consumed: task.credits_consumed });
112
+ spinner.succeed(`${taskType} · ${taskId}` +
113
+ (task.credits_consumed !== undefined
114
+ ? t({ zh: ` · 消耗 ${task.credits_consumed} 积分`, en: ` · ${task.credits_consumed} credits` })
115
+ : ''));
116
+ const result = {
117
+ task_id: taskId,
118
+ type: taskType,
119
+ status: task.status,
120
+ credits_consumed: task.credits_consumed,
121
+ };
122
+ if (!options.noDownload) {
123
+ const baseDir = options.outDir ?? loadContext().output_dir ?? process.cwd();
124
+ const dir = defaultOutputDir(baseDir, name, taskId);
125
+ const dlSpinner = createSpinner(t({ zh: '下载产物…', en: 'downloading artifacts…' }), isQuiet());
126
+ try {
127
+ const artifacts = await downloadArtifacts(task, dir, entry, {
128
+ onFile: (f) => dlSpinner.update(t({ zh: `下载 ${f}`, en: `downloading ${f}` })),
129
+ });
130
+ dlSpinner.succeed(t({ zh: `产物已保存 ${artifacts.outputDir}`, en: `artifacts saved to ${artifacts.outputDir}` }));
131
+ result.output_dir = artifacts.outputDir;
132
+ result.files = artifacts.files.map((f) => basename(f));
133
+ result.model_file = artifacts.modelFile;
134
+ result.preview = artifacts.preview;
135
+ updateHistoryStatus(taskId, 'success', { output_dir: artifacts.outputDir });
136
+ }
137
+ catch (err) {
138
+ dlSpinner.fail(t({ zh: '产物下载失败(任务本身已成功)', en: 'artifact download failed (the task itself succeeded)' }));
139
+ warn(String(err instanceof Error ? err.message : err));
140
+ warn(t({ zh: `可重试:tripo task get ${taskId} --download`, en: `retry with: tripo task get ${taskId} --download` }));
141
+ }
142
+ }
143
+ if (options.notify) {
144
+ notifyDone('Tripo', t({ zh: `${taskType} 完成`, en: `${taskType} finished` }));
145
+ }
146
+ return result;
147
+ }
148
+ /** Ensure history names stay unique by appending -2, -3, ... on collision. */
149
+ function uniqueName(base) {
150
+ const names = new Set(readHistory().map((e) => e.name));
151
+ if (!names.has(base))
152
+ return base;
153
+ for (let i = 2;; i++) {
154
+ const candidate = `${base}-${i}`;
155
+ if (!names.has(candidate))
156
+ return candidate;
157
+ }
158
+ }
159
+ /**
160
+ * Region-specific billing entry: the cn console has a dedicated Alipay
161
+ * recharge page (/billing/recharge); the ov console does Stripe top-up
162
+ * inline on /billing. Both consoles auto-prefix the language segment.
163
+ */
164
+ export function topupUrl() {
165
+ const path = resolveRegion() === 'cn' ? '/billing/recharge' : '/billing';
166
+ return `${resolvePlatformBaseUrl()}${path}`;
167
+ }
@@ -0,0 +1,42 @@
1
+ /** Local file → file_token upload with size/type guards. */
2
+ import { readFile, stat } from 'node:fs/promises';
3
+ import { basename, extname } from 'node:path';
4
+ import { UsageError } from './errors.js';
5
+ import { IMAGE_EXTS, MODEL_EXTS } from '../util/input-detect.js';
6
+ const IMAGE_MAX = 20 * 1024 * 1024;
7
+ const MODEL_MAX = 150 * 1024 * 1024;
8
+ const MIME = {
9
+ '.png': 'image/png',
10
+ '.jpg': 'image/jpeg',
11
+ '.jpeg': 'image/jpeg',
12
+ '.webp': 'image/webp',
13
+ '.bmp': 'image/bmp',
14
+ '.tiff': 'image/tiff',
15
+ '.tif': 'image/tiff',
16
+ '.glb': 'model/gltf-binary',
17
+ '.fbx': 'application/octet-stream',
18
+ '.obj': 'text/plain',
19
+ '.stl': 'application/octet-stream',
20
+ };
21
+ export async function uploadLocalFile(client, filePath) {
22
+ const ext = extname(filePath).toLowerCase();
23
+ const isImage = IMAGE_EXTS.has(ext);
24
+ const isModel = MODEL_EXTS.has(ext);
25
+ if (!isImage && !isModel) {
26
+ throw new UsageError(`unsupported file type "${ext}" for upload`);
27
+ }
28
+ // The server's upload whitelist has no gltf: a .gltf references external
29
+ // .bin/texture files, so a single-file upload would be incomplete anyway.
30
+ if (ext === '.gltf') {
31
+ throw new UsageError('the upload API does not accept .gltf (it references external files) — export as .glb, or host it and pass the URL');
32
+ }
33
+ const info = await stat(filePath);
34
+ const limit = isImage ? IMAGE_MAX : MODEL_MAX;
35
+ if (info.size > limit) {
36
+ throw new UsageError(`${basename(filePath)} is ${(info.size / 1024 / 1024).toFixed(1)}MB, exceeding the ${limit / 1024 / 1024}MB ${isImage ? 'image' : 'model'} limit`);
37
+ }
38
+ const bytes = await readFile(filePath);
39
+ const blob = new Blob([bytes], { type: MIME[ext] ?? 'application/octet-stream' });
40
+ const { file_token } = await client.uploadFile(basename(filePath), blob);
41
+ return file_token;
42
+ }
@@ -0,0 +1,24 @@
1
+ /** Static HTML template for the local model-viewer preview page. */
2
+ export function viewerHtml(modelFileName, title) {
3
+ return `<!doctype html>
4
+ <html lang="en">
5
+ <head>
6
+ <meta charset="utf-8">
7
+ <meta name="viewport" content="width=device-width, initial-scale=1">
8
+ <title>${escapeHtml(title)} · Tripo</title>
9
+ <script type="module" src="https://ajax.googleapis.com/ajax/libs/model-viewer/3.5.0/model-viewer.min.js"></script>
10
+ <style>
11
+ html, body { margin: 0; height: 100%; background: #101014; color: #e8e8ec; font: 14px/1.5 system-ui, sans-serif; }
12
+ model-viewer { width: 100%; height: 100%; --poster-color: transparent; }
13
+ .bar { position: fixed; top: 0; left: 0; right: 0; display: flex; justify-content: space-between; padding: 10px 16px; font-size: 13px; color: #9a9aa6; pointer-events: none; }
14
+ </style>
15
+ </head>
16
+ <body>
17
+ <div class="bar"><span>${escapeHtml(title)}</span><span>drag to rotate · scroll to zoom</span></div>
18
+ <model-viewer src="${encodeURI(modelFileName)}" camera-controls auto-rotate shadow-intensity="1" exposure="0.9" ar></model-viewer>
19
+ </body>
20
+ </html>`;
21
+ }
22
+ function escapeHtml(text) {
23
+ return text.replace(/[&<>"']/g, (ch) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' })[ch]);
24
+ }
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Local 3D preview: serve the model file + a model-viewer page on an
3
+ * ephemeral localhost port. The browser is an optional enhancement — the
4
+ * files are already on disk; headless callers just get the file path.
5
+ */
6
+ import { createServer } from 'node:http';
7
+ import { readFile } from 'node:fs/promises';
8
+ import { basename, extname } from 'node:path';
9
+ import { viewerHtml } from './viewer-assets.js';
10
+ const CONTENT_TYPES = {
11
+ '.glb': 'model/gltf-binary',
12
+ '.gltf': 'model/gltf+json',
13
+ '.html': 'text/html; charset=utf-8',
14
+ '.png': 'image/png',
15
+ '.jpg': 'image/jpeg',
16
+ '.webp': 'image/webp',
17
+ '.usdz': 'model/vnd.usdz+zip',
18
+ };
19
+ /** Start a one-model preview server; resolves once listening. */
20
+ export async function startViewer(modelFile, title) {
21
+ const fileName = basename(modelFile);
22
+ const server = createServer((req, res) => {
23
+ void (async () => {
24
+ const url = new URL(req.url ?? '/', 'http://localhost');
25
+ try {
26
+ if (url.pathname === '/') {
27
+ res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
28
+ res.end(viewerHtml(fileName, title));
29
+ return;
30
+ }
31
+ if (decodeURIComponent(url.pathname.slice(1)) === fileName) {
32
+ const data = await readFile(modelFile);
33
+ res.writeHead(200, {
34
+ 'Content-Type': CONTENT_TYPES[extname(fileName).toLowerCase()] ?? 'application/octet-stream',
35
+ 'Content-Length': data.length,
36
+ });
37
+ res.end(data);
38
+ return;
39
+ }
40
+ res.writeHead(404);
41
+ res.end('not found');
42
+ }
43
+ catch {
44
+ res.writeHead(500);
45
+ res.end('error');
46
+ }
47
+ })();
48
+ });
49
+ await new Promise((resolve, reject) => {
50
+ server.once('error', reject);
51
+ server.listen(0, '127.0.0.1', resolve);
52
+ });
53
+ const address = server.address();
54
+ const port = typeof address === 'object' && address ? address.port : 0;
55
+ return {
56
+ url: `http://127.0.0.1:${port}/`,
57
+ close: () => server.close(),
58
+ };
59
+ }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Adaptive task polling: start at 2s; when the API reports running_left_time
3
+ * take max(2, left*0.5); otherwise back off exponentially capped at 30s.
4
+ */
5
+ import { sleep } from './client.js';
6
+ import { TaskFailedError } from './errors.js';
7
+ export async function watchTask(client, taskId, options = {}) {
8
+ const timeoutMs = options.timeoutMs ?? 30 * 60 * 1000;
9
+ const deadline = Date.now() + timeoutMs;
10
+ let interval = 2000;
11
+ let backoff = 2000;
12
+ for (;;) {
13
+ const task = await client.getTask(taskId);
14
+ options.onProgress?.(task);
15
+ if (task.status === 'success')
16
+ return task;
17
+ // Terminal failure states (server enum also has banned/expired beyond failed/cancelled).
18
+ if (['failed', 'cancelled', 'banned', 'expired'].includes(task.status)) {
19
+ if (options.throwOnFailure === false)
20
+ return task;
21
+ throw new TaskFailedError(taskId, task.status, extractFailureReason(task));
22
+ }
23
+ if (Date.now() > deadline) {
24
+ throw new TaskFailedError(taskId, 'timeout', `still ${task.status} after ${Math.round(timeoutMs / 1000)}s (task keeps running server-side; check later with "tripo task get ${taskId}")`);
25
+ }
26
+ const left = task.running_left_time ?? task.output?.running_left_time;
27
+ if (typeof left === 'number' && left > 0) {
28
+ interval = Math.max(2000, left * 500);
29
+ backoff = 2000;
30
+ }
31
+ else {
32
+ interval = backoff;
33
+ backoff = Math.min(backoff * 1.5, 30_000);
34
+ }
35
+ await sleep(interval);
36
+ }
37
+ }
38
+ function extractFailureReason(task) {
39
+ const out = task.output ?? {};
40
+ const candidate = out.message ?? out.error ?? out.reason;
41
+ return typeof candidate === 'string' ? candidate : undefined;
42
+ }
package/dist/i18n.js ADDED
@@ -0,0 +1,19 @@
1
+ /** Minimal bilingual UX strings. Locale: config override > env (LANG/LC_ALL). */
2
+ let currentLocale;
3
+ export function setLocale(locale) {
4
+ currentLocale = locale;
5
+ }
6
+ export function getLocale() {
7
+ if (currentLocale)
8
+ return currentLocale;
9
+ const env = process.env.LC_ALL || process.env.LC_MESSAGES || process.env.LANG || '';
10
+ return /^zh/i.test(env) ? 'zh' : 'en';
11
+ }
12
+ /** Pick the localized variant of a bilingual pair. */
13
+ export function t(pair) {
14
+ return getLocale() === 'zh' ? pair.zh : pair.en;
15
+ }
16
+ /** Pick the localized variant of a bilingual string list. */
17
+ export function tList(pair) {
18
+ return getLocale() === 'zh' ? pair.zh : pair.en;
19
+ }