tripo-cli 0.1.0 → 0.2.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.
Files changed (65) hide show
  1. package/README.md +20 -4
  2. package/dist/commands/completion.js +1 -1
  3. package/dist/commands/config-cmd.js +32 -17
  4. package/dist/commands/doctor.js +6 -5
  5. package/dist/commands/home.js +4 -2
  6. package/dist/commands/login.js +201 -39
  7. package/dist/config/config.js +103 -14
  8. package/dist/core/client.js +16 -2
  9. package/dist/core/device-auth.js +41 -9
  10. package/dist/program.js +21 -1
  11. package/package.json +2 -2
  12. package/skill/SKILL.md +13 -1
  13. package/skill/commands/account.md +19 -5
  14. package/dist/src/ai/experts.js +0 -115
  15. package/dist/src/ai/llm.js +0 -88
  16. package/dist/src/ai/plan-card.js +0 -79
  17. package/dist/src/ai/wizard.js +0 -104
  18. package/dist/src/cli.js +0 -7
  19. package/dist/src/commands/account.js +0 -27
  20. package/dist/src/commands/ai.js +0 -113
  21. package/dist/src/commands/batch.js +0 -148
  22. package/dist/src/commands/completion.js +0 -84
  23. package/dist/src/commands/config-cmd.js +0 -84
  24. package/dist/src/commands/docs.js +0 -30
  25. package/dist/src/commands/doctor.js +0 -94
  26. package/dist/src/commands/generate.js +0 -81
  27. package/dist/src/commands/home.js +0 -68
  28. package/dist/src/commands/login.js +0 -225
  29. package/dist/src/commands/make.js +0 -142
  30. package/dist/src/commands/mcp.js +0 -194
  31. package/dist/src/commands/process-groups.js +0 -164
  32. package/dist/src/commands/shared.js +0 -75
  33. package/dist/src/commands/task.js +0 -127
  34. package/dist/src/commands/topup.js +0 -61
  35. package/dist/src/commands/view.js +0 -97
  36. package/dist/src/config/config.js +0 -79
  37. package/dist/src/config/context.js +0 -44
  38. package/dist/src/config/history.js +0 -104
  39. package/dist/src/core/client.js +0 -120
  40. package/dist/src/core/download.js +0 -107
  41. package/dist/src/core/errors.js +0 -64
  42. package/dist/src/core/output.js +0 -72
  43. package/dist/src/core/requests.js +0 -169
  44. package/dist/src/core/task-service.js +0 -161
  45. package/dist/src/core/upload.js +0 -36
  46. package/dist/src/core/viewer-assets.js +0 -24
  47. package/dist/src/core/viewer.js +0 -59
  48. package/dist/src/core/watch.js +0 -41
  49. package/dist/src/i18n.js +0 -19
  50. package/dist/src/knowledge/chains.js +0 -152
  51. package/dist/src/knowledge/error-catalog.js +0 -91
  52. package/dist/src/knowledge/models.js +0 -110
  53. package/dist/src/knowledge/params.js +0 -174
  54. package/dist/src/knowledge/scenarios.js +0 -112
  55. package/dist/src/pipeline/make.js +0 -233
  56. package/dist/src/pipeline/runner.js +0 -73
  57. package/dist/src/pipeline/stdin.js +0 -40
  58. package/dist/src/program.js +0 -147
  59. package/dist/src/types.js +0 -2
  60. package/dist/src/util/input-detect.js +0 -92
  61. package/dist/src/util/notify.js +0 -17
  62. package/dist/src/util/open.js +0 -18
  63. package/dist/src/util/spinner.js +0 -62
  64. package/dist/src/util/term-image.js +0 -28
  65. package/dist/src/version.js +0 -10
@@ -1,169 +0,0 @@
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, normalizeModel } 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
- export function buildTextToImage(prompt, params) {
64
- return {
65
- request: {
66
- endpoint: '/v3/generation/text-to-image',
67
- payload: { prompt, model: params.model ?? CAPABILITY_DEFAULTS.textToImage, ...omit(params, ['model']) },
68
- },
69
- taskType: 'generate_image',
70
- warnings: [],
71
- };
72
- }
73
- export function buildImageToImage(input, params) {
74
- return {
75
- request: {
76
- endpoint: '/v3/generation/image-to-image',
77
- payload: { input, model: params.model ?? CAPABILITY_DEFAULTS.imageToImage, ...omit(params, ['model']) },
78
- },
79
- taskType: 'image_to_image',
80
- warnings: [],
81
- };
82
- }
83
- export function buildImageToMultiview(input, params) {
84
- return {
85
- request: { endpoint: '/v3/generation/image-to-multiview', payload: { input, ...params } },
86
- taskType: 'generate_multiview_image',
87
- warnings: [],
88
- };
89
- }
90
- export function buildEditMultiview(input, params) {
91
- return {
92
- request: { endpoint: '/v3/generation/edit-multiview', payload: { input, ...params } },
93
- taskType: 'edit_multiview_image',
94
- warnings: [],
95
- };
96
- }
97
- export function buildImageToSplat(input, params) {
98
- return {
99
- request: { endpoint: '/v3/generation/image-to-splat', payload: { input, ...params } },
100
- taskType: 'image_to_splat',
101
- warnings: [],
102
- };
103
- }
104
- /**
105
- * Processing step (texture/convert/rig/...): validate per step, then build.
106
- * `input` must already be a task_id / file_token / URL.
107
- */
108
- export function buildStepRequest(stepName, input, args) {
109
- const def = getStep(stepName);
110
- if (!def)
111
- throw new UsageError(`unknown step "${stepName}"`);
112
- let params = { ...args };
113
- const warnings = [];
114
- switch (def.name) {
115
- case 'convert': {
116
- const v = validateConvertParams(params);
117
- ensure(v);
118
- params = v.params;
119
- warnings.push(...v.warnings);
120
- break;
121
- }
122
- case 'decimate': {
123
- const v = validateDecimateParams(params);
124
- ensure(v);
125
- params = v.params;
126
- warnings.push(...v.warnings);
127
- break;
128
- }
129
- case 'retarget': {
130
- const v = validateRetargetParams(params);
131
- ensure(v);
132
- params = v.params;
133
- warnings.push(...v.warnings);
134
- break;
135
- }
136
- case 'rig': {
137
- if (params.model === undefined)
138
- params.model = CAPABILITY_DEFAULTS.rig;
139
- break;
140
- }
141
- case 'texture': {
142
- if (params.model === undefined)
143
- params.model = CAPABILITY_DEFAULTS.texture;
144
- break;
145
- }
146
- case 'stylize': {
147
- if (typeof params.style !== 'string') {
148
- throw new UsageError('stylize requires style (lego/voxel/voronoi/minecraft)');
149
- }
150
- break;
151
- }
152
- case 'smartsegment': {
153
- if (params.seg_type === undefined)
154
- params.seg_type = 'model';
155
- break;
156
- }
157
- }
158
- return {
159
- request: { endpoint: def.endpoint, payload: { input, ...params } },
160
- taskType: def.produces,
161
- warnings,
162
- };
163
- }
164
- function omit(obj, keys) {
165
- const out = { ...obj };
166
- for (const k of keys)
167
- delete out[k];
168
- return out;
169
- }
@@ -1,161 +0,0 @@
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 } 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
- export function topupUrl() {
160
- return `${resolvePlatformBaseUrl()}/billing`;
161
- }
@@ -1,36 +0,0 @@
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
- '.glb': 'model/gltf-binary',
15
- '.gltf': 'model/gltf+json',
16
- '.fbx': 'application/octet-stream',
17
- '.obj': 'text/plain',
18
- '.stl': 'application/octet-stream',
19
- };
20
- export async function uploadLocalFile(client, filePath) {
21
- const ext = extname(filePath).toLowerCase();
22
- const isImage = IMAGE_EXTS.has(ext);
23
- const isModel = MODEL_EXTS.has(ext);
24
- if (!isImage && !isModel) {
25
- throw new UsageError(`unsupported file type "${ext}" for upload`);
26
- }
27
- const info = await stat(filePath);
28
- const limit = isImage ? IMAGE_MAX : MODEL_MAX;
29
- if (info.size > limit) {
30
- throw new UsageError(`${basename(filePath)} is ${(info.size / 1024 / 1024).toFixed(1)}MB, exceeding the ${limit / 1024 / 1024}MB ${isImage ? 'image' : 'model'} limit`);
31
- }
32
- const bytes = await readFile(filePath);
33
- const blob = new Blob([bytes], { type: MIME[ext] ?? 'application/octet-stream' });
34
- const { file_token } = await client.uploadFile(basename(filePath), blob);
35
- return file_token;
36
- }
@@ -1,24 +0,0 @@
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
- }
@@ -1,59 +0,0 @@
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
- }
@@ -1,41 +0,0 @@
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
- if (task.status === 'failed' || task.status === 'cancelled') {
18
- if (options.throwOnFailure === false)
19
- return task;
20
- throw new TaskFailedError(taskId, task.status, extractFailureReason(task));
21
- }
22
- if (Date.now() > deadline) {
23
- 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}")`);
24
- }
25
- const left = task.running_left_time ?? task.output?.running_left_time;
26
- if (typeof left === 'number' && left > 0) {
27
- interval = Math.max(2000, left * 500);
28
- backoff = 2000;
29
- }
30
- else {
31
- interval = backoff;
32
- backoff = Math.min(backoff * 1.5, 30_000);
33
- }
34
- await sleep(interval);
35
- }
36
- }
37
- function extractFailureReason(task) {
38
- const out = task.output ?? {};
39
- const candidate = out.message ?? out.error ?? out.reason;
40
- return typeof candidate === 'string' ? candidate : undefined;
41
- }
package/dist/src/i18n.js DELETED
@@ -1,19 +0,0 @@
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
- }
@@ -1,152 +0,0 @@
1
- /**
2
- * Task-chain legality — which processing step may consume which upstream task.
3
- * Mirrors server-side handler constraints; violations are rejected locally
4
- * before any credits are spent.
5
- */
6
- /** Task types that produce a downloadable 3D model. */
7
- export const MODEL_PRODUCING_TYPES = new Set([
8
- 'text_to_model',
9
- 'image_to_model',
10
- 'multiview_to_model',
11
- 'refine_model',
12
- 'texture_model',
13
- 'stylize_model',
14
- 'convert_model',
15
- 'import_model',
16
- 'highpoly_to_lowpoly',
17
- 'mesh_segmentation',
18
- 'mesh_completion',
19
- 'animate_rig',
20
- 'animate_retarget',
21
- ]);
22
- export const STEPS = [
23
- { name: 'refine', endpoint: '/v3/models/refine', produces: 'refine_model', accepts: ['text_to_model'], acceptsFile: false },
24
- { name: 'texture', endpoint: '/v3/models/texture', produces: 'texture_model', accepts: 'any-model', acceptsFile: true },
25
- { name: 'stylize', endpoint: '/v3/models/stylize', produces: 'stylize_model', accepts: 'any-model', acceptsFile: true },
26
- { name: 'convert', endpoint: '/v3/models/convert', produces: 'convert_model', accepts: 'any-model', acceptsFile: true },
27
- { name: 'import', endpoint: '/v3/models/import', produces: 'import_model', accepts: [], acceptsFile: true },
28
- { name: 'rig-check', endpoint: '/v3/animations/rig-check', produces: 'animate_prerigcheck', accepts: 'any-model', acceptsFile: true, passthrough: true, aliases: ['check', 'rigcheck'] },
29
- { name: 'rig', endpoint: '/v3/animations/rig', produces: 'animate_rig', accepts: 'any-model', acceptsFile: true },
30
- { name: 'retarget', endpoint: '/v3/animations/retarget', produces: 'animate_retarget', accepts: ['animate_rig'], acceptsFile: false, aliases: ['animate'] },
31
- { name: 'segment', endpoint: '/v3/mesh/segment', produces: 'mesh_segmentation', accepts: 'any-model', acceptsFile: true },
32
- { name: 'complete', endpoint: '/v3/mesh/complete', produces: 'mesh_completion', accepts: ['mesh_segmentation'], acceptsFile: false },
33
- { name: 'decimate', endpoint: '/v3/mesh/decimate', produces: 'highpoly_to_lowpoly', accepts: 'any-model', acceptsFile: true, aliases: ['lowpoly'] },
34
- { name: 'smartsegment', endpoint: '/v3/mesh/smartsegment', produces: 'smart_segmentation', accepts: 'any-model', acceptsFile: true },
35
- ];
36
- const stepIndex = new Map();
37
- for (const s of STEPS) {
38
- stepIndex.set(s.name, s);
39
- for (const a of s.aliases ?? [])
40
- stepIndex.set(a, s);
41
- }
42
- export function getStep(name) {
43
- return stepIndex.get(name);
44
- }
45
- /**
46
- * Parse a --then expression: "texture,rig,convert:fbx" or
47
- * "convert:format=fbx,texture_size=2048". A bare value after ":" is sugar for
48
- * the step's primary argument (convert→format, retarget→animation,
49
- * stylize→style, decimate→face_limit).
50
- */
51
- export function parseThenExpression(expr) {
52
- const out = [];
53
- // Split on commas that are not inside a step's arg list (args use "=", so a
54
- // comma always separates either steps or key=value pairs; a segment with "="
55
- // and no known step prefix belongs to the previous step).
56
- const rawParts = expr.split(',').map((p) => p.trim()).filter(Boolean);
57
- for (const part of rawParts) {
58
- const [head, ...rest] = part.split(':');
59
- const stepName = (head ?? '').trim().toLowerCase();
60
- const def = getStep(stepName);
61
- if (!def) {
62
- if (part.includes('=') && out.length > 0) {
63
- // continuation of previous step's args: "k=v"
64
- const prev = out[out.length - 1];
65
- applyArg(prev.args, part);
66
- continue;
67
- }
68
- throw new ChainError(`unknown step "${stepName}" (valid: ${STEPS.map((s) => s.name).join(', ')})`);
69
- }
70
- const args = {};
71
- const argStr = rest.join(':').trim();
72
- if (argStr) {
73
- for (const token of argStr.split(/[,;]/).map((t) => t.trim()).filter(Boolean)) {
74
- if (token.includes('='))
75
- applyArg(args, token);
76
- else
77
- applyPrimaryArg(def.name, args, token);
78
- }
79
- }
80
- out.push({ step: def.name, args });
81
- }
82
- return out;
83
- }
84
- function applyArg(args, token) {
85
- const eq = token.indexOf('=');
86
- const key = token.slice(0, eq).trim();
87
- const raw = token.slice(eq + 1).trim();
88
- args[key] = coerce(raw);
89
- }
90
- function applyPrimaryArg(step, args, value) {
91
- const primary = {
92
- convert: 'format',
93
- retarget: 'animation',
94
- stylize: 'style',
95
- decimate: 'face_limit',
96
- texture: 'texture_quality',
97
- rig: 'rig_type',
98
- segment: 'model',
99
- smartsegment: 'granularity',
100
- };
101
- const key = primary[step];
102
- if (!key)
103
- throw new ChainError(`step "${step}" does not take a bare argument ("${value}")`);
104
- args[key] = coerce(value);
105
- }
106
- function coerce(raw) {
107
- if (raw === 'true')
108
- return true;
109
- if (raw === 'false')
110
- return false;
111
- if (/^-?\d+$/.test(raw))
112
- return Number(raw);
113
- if (/^-?\d*\.\d+$/.test(raw))
114
- return Number(raw);
115
- return raw;
116
- }
117
- export class ChainError extends Error {
118
- }
119
- /**
120
- * Validate that a chain is legal given the type of the first (source) task.
121
- * Returns the list of task types the chain will produce, in order.
122
- */
123
- export function validateChain(chain, sourceType) {
124
- let currentType = sourceType;
125
- const produced = [];
126
- for (const { step } of chain) {
127
- const def = getStep(step);
128
- if (!def)
129
- throw new ChainError(`unknown step "${step}"`);
130
- if (def.accepts === 'any-model') {
131
- if (!MODEL_PRODUCING_TYPES.has(currentType)) {
132
- throw new ChainError(`step "${def.name}" needs a model-producing upstream task, but the previous step produces "${currentType}"`);
133
- }
134
- }
135
- else if (!def.accepts.includes(currentType)) {
136
- throw new ChainError(`step "${def.name}" only accepts ${def.accepts.join('/')} tasks, but the previous step produces "${currentType}"` +
137
- hintFor(def.name));
138
- }
139
- if (!def.passthrough)
140
- currentType = def.produces;
141
- produced.push(def.produces);
142
- }
143
- return produced;
144
- }
145
- function hintFor(step) {
146
- const hints = {
147
- refine: ' (refine only accepts a successful text-to-model task)',
148
- complete: ' (run "segment" first: complete only accepts mesh segmentation tasks)',
149
- retarget: ' (run "rig" first: retarget only accepts rigged tasks)',
150
- };
151
- return hints[step] ?? '';
152
- }