tripo-cli 0.7.2 → 0.8.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/CHANGELOG.md +29 -0
- package/README.md +16 -14
- package/dist/ai/experts.js +26 -20
- package/dist/ai/plan-card.js +73 -34
- package/dist/ai/wizard.js +19 -19
- package/dist/commands/account.js +2 -6
- package/dist/commands/ai.js +18 -16
- package/dist/commands/batch.js +58 -18
- package/dist/commands/completion.js +1 -1
- package/dist/commands/config-cmd.js +1 -1
- package/dist/commands/doctor.js +1 -3
- package/dist/commands/generate.js +50 -8
- package/dist/commands/home.js +12 -13
- package/dist/commands/login.js +125 -182
- package/dist/commands/make.js +50 -25
- package/dist/commands/mcp.js +8 -3
- package/dist/commands/process-groups.js +29 -18
- package/dist/commands/shared.js +22 -13
- package/dist/commands/task.js +12 -11
- package/dist/commands/topup.js +6 -10
- package/dist/commands/update.js +23 -11
- package/dist/commands/view.js +3 -4
- package/dist/config/config.js +1 -1
- package/dist/core/analytics.js +39 -16
- package/dist/core/client.js +53 -21
- package/dist/core/device-auth.js +49 -1
- package/dist/core/download.js +2 -1
- package/dist/core/errors.js +24 -14
- package/dist/core/output.js +0 -4
- package/dist/core/requests.js +94 -31
- package/dist/core/task-service.js +21 -20
- package/dist/core/upload.js +23 -5
- package/dist/core/watch.js +3 -0
- package/dist/knowledge/chains.js +16 -17
- package/dist/knowledge/error-catalog.js +39 -37
- package/dist/knowledge/models.js +147 -27
- package/dist/knowledge/params.js +143 -32
- package/dist/knowledge/scenarios.js +32 -55
- package/dist/pipeline/make.js +38 -30
- package/dist/pipeline/runner.js +27 -8
- package/dist/pipeline/stdin.js +40 -3
- package/dist/program.js +3 -10
- package/dist/util/input-detect.js +10 -1
- package/package.json +3 -2
- package/skill/SKILL.md +52 -15
- package/skill/commands/account.md +8 -3
- package/skill/commands/batch.md +13 -3
- package/skill/commands/generate.md +57 -23
- package/skill/commands/make.md +43 -16
- package/skill/commands/process.md +29 -24
- package/skill/commands/task.md +4 -4
- package/skill/commands/view.md +1 -1
- package/skill/common-errors.md +14 -10
- package/skill/examples/animation.md +4 -2
- package/skill/examples/ar-web.md +6 -4
- package/skill/examples/game-asset.md +18 -7
- package/skill/examples/pipes.md +2 -2
- package/skill/examples/print.md +7 -3
- package/dist/i18n.js +0 -19
package/dist/commands/ai.js
CHANGED
|
@@ -9,10 +9,10 @@ import * as p from '@clack/prompts';
|
|
|
9
9
|
import { resolveReference } from '../config/history.js';
|
|
10
10
|
import { CliError } from '../core/errors.js';
|
|
11
11
|
import { isInteractive, isYes, log, warn } from '../core/output.js';
|
|
12
|
-
import { t } from '../i18n.js';
|
|
13
12
|
import { EXIT } from '../knowledge/error-catalog.js';
|
|
13
|
+
import { getScenario } from '../knowledge/scenarios.js';
|
|
14
14
|
import { executeMake, planMake } from '../pipeline/make.js';
|
|
15
|
-
import { renderPlanCard } from '../ai/plan-card.js';
|
|
15
|
+
import { cardFromMakePlan, renderPlanCard } from '../ai/plan-card.js';
|
|
16
16
|
import { resolveLlmConfig } from '../ai/llm.js';
|
|
17
17
|
import { buildExpertPlan } from '../ai/experts.js';
|
|
18
18
|
import { runWizard } from '../ai/wizard.js';
|
|
@@ -22,7 +22,7 @@ export function registerAiCommand(program) {
|
|
|
22
22
|
const cmd = program
|
|
23
23
|
.command('ai')
|
|
24
24
|
.description('describe what you want in natural language; experts plan it, you confirm, it runs')
|
|
25
|
-
.argument('[request...]', 'e.g. "
|
|
25
|
+
.argument('[request...]', 'e.g. "a low-poly knight for my mobile game, with a run animation"')
|
|
26
26
|
.option('--wizard', 'force wizard mode even when an LLM is configured');
|
|
27
27
|
withTaskOptions(cmd);
|
|
28
28
|
cmd.action(async (request, flags) => {
|
|
@@ -31,14 +31,15 @@ export function registerAiCommand(program) {
|
|
|
31
31
|
let card;
|
|
32
32
|
let makeOptions;
|
|
33
33
|
if (llm && !flags.wizard && text) {
|
|
34
|
-
log(
|
|
34
|
+
log(`conversation mode (${llm.model})…`);
|
|
35
35
|
try {
|
|
36
|
-
const plan = await buildExpertPlan(text, isInteractive() ? askUser : undefined);
|
|
36
|
+
const plan = await buildExpertPlan(text, isInteractive() ? askUser : undefined, flags.param);
|
|
37
37
|
card = plan.card;
|
|
38
38
|
makeOptions = {
|
|
39
39
|
inputs: [plan.prompt],
|
|
40
40
|
scenario: plan.scenarioId,
|
|
41
41
|
chain: plan.chain,
|
|
42
|
+
model: plan.card.model,
|
|
42
43
|
params: plan.params,
|
|
43
44
|
name: flags.name,
|
|
44
45
|
outDir: flags.out,
|
|
@@ -47,10 +48,7 @@ export function registerAiCommand(program) {
|
|
|
47
48
|
};
|
|
48
49
|
}
|
|
49
50
|
catch (err) {
|
|
50
|
-
warn(
|
|
51
|
-
zh: `LLM 编排失败(${err instanceof Error ? err.message : err}),回落到向导模式`,
|
|
52
|
-
en: `LLM planning failed (${err instanceof Error ? err.message : err}); falling back to the wizard`,
|
|
53
|
-
}));
|
|
51
|
+
warn(`LLM planning failed (${err instanceof Error ? err.message : err}); falling back to the wizard`);
|
|
54
52
|
const wizard = await runWizard(text);
|
|
55
53
|
card = wizard.card;
|
|
56
54
|
makeOptions = wizardToOptions(wizard.options, flags);
|
|
@@ -59,24 +57,28 @@ export function registerAiCommand(program) {
|
|
|
59
57
|
else {
|
|
60
58
|
// The wizard is interactive by nature — never enter it headless.
|
|
61
59
|
if (!isInteractive()) {
|
|
62
|
-
throw new CliError(
|
|
63
|
-
zh: '向导模式需要交互终端;无头环境请用 tripo make --for <场景>(或配置 llm.base_url/llm.model 走对话模式)',
|
|
64
|
-
en: 'wizard mode needs an interactive terminal; headless callers should use "tripo make --for <scenario>" (or configure llm.base_url/llm.model for conversation mode)',
|
|
65
|
-
}), EXIT.USAGE);
|
|
60
|
+
throw new CliError('wizard mode needs an interactive terminal; headless callers should use "tripo make --for <scenario>" (or configure llm.base_url/llm.model for conversation mode)', EXIT.USAGE);
|
|
66
61
|
}
|
|
67
62
|
const wizard = await runWizard(text || undefined);
|
|
68
63
|
card = wizard.card;
|
|
69
64
|
makeOptions = wizardToOptions(wizard.options, flags);
|
|
70
65
|
}
|
|
66
|
+
// An explicit chain counts as a --then override in planMake, which drops the preset's dual delivery.
|
|
67
|
+
const preset = getScenario(makeOptions.scenario ?? '');
|
|
68
|
+
if (preset && JSON.stringify(makeOptions.chain) === JSON.stringify(preset.chain))
|
|
69
|
+
makeOptions.chain = undefined;
|
|
70
|
+
const client = newClient();
|
|
71
|
+
const preview = await planMake(client, { ...makeOptions, dryRun: true });
|
|
72
|
+
card = { ...card, ...cardFromMakePlan(preview, makeOptions), notes: card.notes };
|
|
73
|
+
makeOptions.model = preview.model;
|
|
71
74
|
process.stderr.write(renderPlanCard(card) + '\n');
|
|
72
75
|
if (!isYes()) {
|
|
73
|
-
const go = await p.confirm({ message:
|
|
76
|
+
const go = await p.confirm({ message: 'Run this plan?' });
|
|
74
77
|
if (p.isCancel(go) || !go) {
|
|
75
|
-
log(
|
|
78
|
+
log('cancelled; no credits spent');
|
|
76
79
|
return;
|
|
77
80
|
}
|
|
78
81
|
}
|
|
79
|
-
const client = newClient();
|
|
80
82
|
const plan = await planMake(client, makeOptions);
|
|
81
83
|
const outcome = await executeMake(client, plan, makeOptions);
|
|
82
84
|
emitMakeOutcome({ results: [outcome.primary], chainResults: outcome.chainResults, stoppedEarly: outcome.stoppedEarly });
|
package/dist/commands/batch.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `tripo batch run <manifest.yaml>` — bulk production with concurrency,
|
|
3
3
|
* retries, and resumable state (skip jobs already succeeded on re-run).
|
|
4
|
+
* A job is never resubmitted once a paid task may exist for it: such jobs
|
|
5
|
+
* stop as needs_attention instead of retrying.
|
|
4
6
|
*
|
|
5
7
|
* Manifest:
|
|
6
8
|
* concurrency: 2
|
|
@@ -14,10 +16,10 @@
|
|
|
14
16
|
*/
|
|
15
17
|
import { readFileSync, writeFileSync } from 'node:fs';
|
|
16
18
|
import { resolve } from 'node:path';
|
|
19
|
+
import pc from 'picocolors';
|
|
17
20
|
import { parse as parseYaml } from 'yaml';
|
|
18
|
-
import { UsageError } from '../core/errors.js';
|
|
19
|
-
import { emitResult, errorLine, log, success } from '../core/output.js';
|
|
20
|
-
import { t } from '../i18n.js';
|
|
21
|
+
import { CliError, TaskFailedError, UsageError } from '../core/errors.js';
|
|
22
|
+
import { emitResult, errorLine, log, success, warn } from '../core/output.js';
|
|
21
23
|
import { parseThenExpression } from '../knowledge/chains.js';
|
|
22
24
|
import { executeMake, planMake } from '../pipeline/make.js';
|
|
23
25
|
import { newClient } from './shared.js';
|
|
@@ -27,7 +29,7 @@ export function registerBatchCommand(program) {
|
|
|
27
29
|
.command('run <manifest>')
|
|
28
30
|
.description('run all jobs (resumable: finished jobs are skipped on re-run)')
|
|
29
31
|
.option('--concurrency <n>', 'parallel jobs (default from manifest or 2)', (v) => Number(v))
|
|
30
|
-
.option('--retries <n>', 'retries per failed job', (v) => Number(v), 1)
|
|
32
|
+
.option('--retries <n>', 'retries per failed job (only when no paid task was left behind)', (v) => Number(v), 1)
|
|
31
33
|
.option('--fresh', 'ignore previous state and re-run everything')
|
|
32
34
|
.action(async (manifestPath, opts) => {
|
|
33
35
|
const absPath = resolve(manifestPath);
|
|
@@ -46,12 +48,17 @@ export function registerBatchCommand(program) {
|
|
|
46
48
|
seen.add(key);
|
|
47
49
|
}
|
|
48
50
|
const concurrency = Math.max(1, opts.concurrency ?? manifest.concurrency ?? 2);
|
|
49
|
-
const queue = jobs.filter(({ key }) => state.jobs[key]?.status !== 'success');
|
|
50
|
-
const
|
|
51
|
+
const queue = jobs.filter(({ key }) => state.jobs[key]?.status !== 'success' && state.jobs[key]?.status !== 'needs_attention');
|
|
52
|
+
const attention = jobs.filter(({ key }) => state.jobs[key]?.status === 'needs_attention');
|
|
53
|
+
const skipped = jobs.length - queue.length - attention.length;
|
|
51
54
|
if (skipped > 0) {
|
|
52
|
-
log(
|
|
55
|
+
log(`skipping ${skipped} already-finished job(s) (resume)`);
|
|
53
56
|
}
|
|
54
|
-
|
|
57
|
+
for (const { key } of attention) {
|
|
58
|
+
const ids = (state.jobs[key].tasks ?? []).map((t) => t.task_id).join(', ') || 'unknown';
|
|
59
|
+
warn(`[${key}] needs attention, not resubmitted (tasks: ${ids}); delete its entry in ${statePath} or use --fresh to run it again`);
|
|
60
|
+
}
|
|
61
|
+
log(`running ${queue.length} job(s), concurrency ${concurrency}`);
|
|
55
62
|
const client = newClient();
|
|
56
63
|
let cursor = 0;
|
|
57
64
|
const runNext = async () => {
|
|
@@ -64,20 +71,34 @@ export function registerBatchCommand(program) {
|
|
|
64
71
|
const maxAttempts = 1 + opts.retries;
|
|
65
72
|
for (let attempt = jobState.attempts + 1; attempt <= maxAttempts; attempt++) {
|
|
66
73
|
jobState.attempts = attempt;
|
|
74
|
+
const created = [];
|
|
67
75
|
try {
|
|
68
|
-
const outcome = await runJob(client, item.key, item.spec)
|
|
76
|
+
const outcome = await runJob(client, item.key, item.spec, (task) => {
|
|
77
|
+
created.push(task);
|
|
78
|
+
(jobState.tasks ??= []).push(task);
|
|
79
|
+
saveState(statePath, state);
|
|
80
|
+
});
|
|
69
81
|
jobState.status = 'success';
|
|
70
82
|
jobState.task_id = outcome.taskId;
|
|
71
83
|
jobState.output_dir = outcome.outputDir;
|
|
72
84
|
delete jobState.error;
|
|
85
|
+
delete jobState.suggestion;
|
|
73
86
|
success(`[${item.key}] done${outcome.outputDir ? ` → ${outcome.outputDir}` : ''}`);
|
|
74
87
|
break;
|
|
75
88
|
}
|
|
76
89
|
catch (err) {
|
|
77
90
|
const message = err instanceof Error ? err.message : String(err);
|
|
78
|
-
|
|
91
|
+
const retryable = mayRetry(err, created);
|
|
92
|
+
jobState.status = retryable ? 'failed' : 'needs_attention';
|
|
79
93
|
jobState.error = message;
|
|
80
|
-
|
|
94
|
+
jobState.suggestion = retryable
|
|
95
|
+
? err instanceof CliError ? err.suggestion : undefined
|
|
96
|
+
: attentionSuggestion(err, jobState.tasks ?? []);
|
|
97
|
+
errorLine(`[${item.key}] attempt ${attempt}/${maxAttempts} failed${retryable ? '' : ' (needs attention: a paid task may exist, not retried)'}: ${message}`);
|
|
98
|
+
if (jobState.suggestion)
|
|
99
|
+
process.stderr.write(pc.dim(` → ${jobState.suggestion}\n`));
|
|
100
|
+
if (!retryable)
|
|
101
|
+
break;
|
|
81
102
|
}
|
|
82
103
|
finally {
|
|
83
104
|
saveState(statePath, state);
|
|
@@ -90,19 +111,18 @@ export function registerBatchCommand(program) {
|
|
|
90
111
|
total: jobs.length,
|
|
91
112
|
success: Object.values(state.jobs).filter((j) => j.status === 'success').length,
|
|
92
113
|
failed: Object.values(state.jobs).filter((j) => j.status === 'failed').length,
|
|
114
|
+
needs_attention: Object.values(state.jobs).filter((j) => j.status === 'needs_attention').length,
|
|
93
115
|
state_file: statePath,
|
|
94
116
|
jobs: state.jobs,
|
|
95
117
|
};
|
|
96
|
-
const failed = summary
|
|
97
|
-
emitResult(summary,
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
}));
|
|
101
|
-
if (failed > 0)
|
|
118
|
+
const { failed, needs_attention: needsAttention } = summary;
|
|
119
|
+
emitResult(summary, `batch finished: ${summary.success}/${summary.total} succeeded${failed ? `, ${failed} failed (re-run the same command to resume)` : ''}` +
|
|
120
|
+
`${needsAttention ? `, ${needsAttention} need attention (never resubmitted automatically; see "suggestion" in ${statePath})` : ''}`);
|
|
121
|
+
if (failed > 0 || needsAttention > 0)
|
|
102
122
|
process.exitCode = 6;
|
|
103
123
|
});
|
|
104
124
|
}
|
|
105
|
-
async function runJob(client, key, spec) {
|
|
125
|
+
async function runJob(client, key, spec, onTaskCreated) {
|
|
106
126
|
const inputs = spec.inputs ?? (spec.input !== undefined ? [spec.input] : []);
|
|
107
127
|
if (inputs.length === 0)
|
|
108
128
|
throw new UsageError(`job "${key}" has no input`);
|
|
@@ -115,12 +135,32 @@ async function runJob(client, key, spec) {
|
|
|
115
135
|
params: spec.params ?? {},
|
|
116
136
|
name: spec.name ?? key,
|
|
117
137
|
outDir: spec.out,
|
|
138
|
+
onTaskCreated,
|
|
118
139
|
};
|
|
119
140
|
const plan = await planMake(client, options);
|
|
120
141
|
const outcome = await executeMake(client, plan, options);
|
|
121
142
|
const finalResult = outcome.chainResults[outcome.chainResults.length - 1] ?? outcome.primary;
|
|
122
143
|
return { taskId: finalResult.task_id, outputDir: finalResult.output_dir ?? outcome.primary.output_dir };
|
|
123
144
|
}
|
|
145
|
+
/**
|
|
146
|
+
* Retrying is safe only when the failed attempt left no billed or still-running
|
|
147
|
+
* task behind: it failed before anything was submitted, or its only task failed
|
|
148
|
+
* server-side (failed/cancelled tasks are refunded).
|
|
149
|
+
*/
|
|
150
|
+
function mayRetry(err, created) {
|
|
151
|
+
if (created.length === 0)
|
|
152
|
+
return !(err instanceof CliError && err.submissionUncertain);
|
|
153
|
+
return (created.length === 1 &&
|
|
154
|
+
err instanceof TaskFailedError &&
|
|
155
|
+
err.taskId === created[0].task_id &&
|
|
156
|
+
(err.status === 'failed' || err.status === 'cancelled'));
|
|
157
|
+
}
|
|
158
|
+
function attentionSuggestion(err, tasks) {
|
|
159
|
+
if (err instanceof CliError && err.suggestion)
|
|
160
|
+
return err.suggestion;
|
|
161
|
+
const list = tasks.map((t) => `${t.task_id} (${t.type})`).join(', ');
|
|
162
|
+
return `paid tasks already exist for this job: ${list}; do not rerun the whole job, continue from the last successful one (tripo make <task_id> --then <remaining steps>)`;
|
|
163
|
+
}
|
|
124
164
|
function loadManifest(path) {
|
|
125
165
|
let raw;
|
|
126
166
|
try {
|
|
@@ -9,7 +9,7 @@ const COMMANDS = [
|
|
|
9
9
|
];
|
|
10
10
|
const SUBS = {
|
|
11
11
|
generate: ['text-to-model', 'image-to-model', 'multiview-to-model', 'text-to-image', 'image-to-image', 'image-to-multiview', 'edit-multiview', 'image-to-splat'],
|
|
12
|
-
model: ['
|
|
12
|
+
model: ['texture', 'convert', 'import'],
|
|
13
13
|
anim: ['check', 'rig', 'retarget'],
|
|
14
14
|
mesh: ['segment', 'complete', 'decimate', 'uv', ...(isMeshEditEnabled() ? ['edit'] : []), 'smartsegment'],
|
|
15
15
|
task: ['get', 'list', 'watch'],
|
|
@@ -7,7 +7,7 @@ import { coerceValue } from './shared.js';
|
|
|
7
7
|
/** Account-bound settings — these live in the *active profile*. */
|
|
8
8
|
const PROFILE_SETTABLE = new Set(['region', 'api_base_url', 'platform_base_url']);
|
|
9
9
|
/** User preferences — top-level, shared by all profiles. */
|
|
10
|
-
const GLOBAL_SETTABLE = new Set(['
|
|
10
|
+
const GLOBAL_SETTABLE = new Set(['default_output_dir', 'llm.base_url', 'llm.api_key', 'llm.model']);
|
|
11
11
|
const SETTABLE = [...PROFILE_SETTABLE, ...GLOBAL_SETTABLE];
|
|
12
12
|
export function registerConfigCommands(program) {
|
|
13
13
|
const config = program.command('config').description('global config (~/.tripo) and directory context (.tripo)');
|
package/dist/commands/doctor.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { statSync } from 'node:fs';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import pc from 'picocolors';
|
|
5
|
-
import { configFileExists, describeKeySource, listProfiles,
|
|
5
|
+
import { configFileExists, describeKeySource, listProfiles, resolveApiBaseUrl, resolveApiKey, resolveRegion, tripoHome } from '../config/config.js';
|
|
6
6
|
import { TripoClient } from '../core/client.js';
|
|
7
7
|
import { CliError } from '../core/errors.js';
|
|
8
8
|
import { detectRegionForKey } from '../core/region.js';
|
|
@@ -93,8 +93,6 @@ export function registerDoctorCommand(program) {
|
|
|
93
93
|
detail: `${process.stdout.isTTY ? 'interactive TTY' : 'non-TTY (headless mode)'}${terminalSupportsInlineImages() ? ' · inline images supported' : ''}`,
|
|
94
94
|
critical: false,
|
|
95
95
|
});
|
|
96
|
-
const language = loadConfig().language ?? 'auto';
|
|
97
|
-
checks.push({ name: 'language', ok: true, detail: language, critical: false });
|
|
98
96
|
const failedCritical = checks.some((c) => c.critical && !c.ok);
|
|
99
97
|
const human = checks
|
|
100
98
|
.map((c) => `${c.ok ? pc.green('✓') : c.critical ? pc.red('✗') : pc.yellow('!')} ${c.name.padEnd(34)} ${c.detail}`)
|
|
@@ -3,8 +3,9 @@ import { UsageError } from '../core/errors.js';
|
|
|
3
3
|
import { warn } from '../core/output.js';
|
|
4
4
|
import { buildEditMultiview, buildImageToImage, buildImageToModel, buildImageToMultiview, buildImageToSplat, buildMultiviewToModel, buildTextToImage, buildTextToModel, } from '../core/requests.js';
|
|
5
5
|
import { resolveInputValue, runTask } from '../core/task-service.js';
|
|
6
|
-
import { chooseModel } from '../knowledge/models.js';
|
|
6
|
+
import { chooseModel, detectDenseGeometryIntent, detectLowPolyIntent, detectRealtimeIntent } from '../knowledge/models.js';
|
|
7
7
|
import { assignViews } from '../pipeline/make.js';
|
|
8
|
+
import { detectInput } from '../util/input-detect.js';
|
|
8
9
|
import { newClient, printTaskResult, withTaskOptions } from './shared.js';
|
|
9
10
|
export function registerGenerateCommands(program) {
|
|
10
11
|
const generate = program.command('generate').description('deterministic generation endpoints (text/image/multiview → model/image/splat)');
|
|
@@ -38,29 +39,70 @@ export function registerGenerateCommands(program) {
|
|
|
38
39
|
};
|
|
39
40
|
add('text-to-model', 'text → 3D model', 'prompt', async (_c, inputs, flags) => {
|
|
40
41
|
const prompt = inputs.join(' ');
|
|
41
|
-
const { model } = chooseModel({
|
|
42
|
+
const { model } = chooseModel({
|
|
43
|
+
explicit: flags.model,
|
|
44
|
+
faceLimit: numberParam(flags, 'face_limit'),
|
|
45
|
+
quad: flags.param.quad === true,
|
|
46
|
+
lowPoly: detectLowPolyIntent(prompt),
|
|
47
|
+
realtime: detectRealtimeIntent(prompt),
|
|
48
|
+
denseGeometry: detectDenseGeometryIntent(prompt),
|
|
49
|
+
params: flags.param,
|
|
50
|
+
});
|
|
42
51
|
return buildTextToModel(prompt, model, flags.param);
|
|
43
52
|
});
|
|
44
53
|
add('image-to-model', 'single image → 3D model', 'single', async (client, inputs, flags) => {
|
|
54
|
+
const intent = flags.prompt ?? '';
|
|
55
|
+
const { model } = chooseModel({
|
|
56
|
+
explicit: flags.model,
|
|
57
|
+
faceLimit: numberParam(flags, 'face_limit'),
|
|
58
|
+
quad: flags.param.quad === true,
|
|
59
|
+
lowPoly: detectLowPolyIntent(intent),
|
|
60
|
+
realtime: detectRealtimeIntent(intent),
|
|
61
|
+
denseGeometry: detectDenseGeometryIntent(intent),
|
|
62
|
+
params: flags.param,
|
|
63
|
+
});
|
|
64
|
+
buildImageToModel(inputs[0], model, flags.param);
|
|
45
65
|
const { value } = await resolveInputValue(client, inputs[0]);
|
|
46
|
-
const { model } = chooseModel({ explicit: flags.model, faceLimit: numberParam(flags, 'face_limit') });
|
|
47
66
|
return buildImageToModel(value, model, flags.param);
|
|
48
67
|
});
|
|
49
|
-
add('multiview-to-model', '2–4 view images → 3D model', 'multi', async (client, inputs, flags) => {
|
|
68
|
+
add('multiview-to-model', '2–4 view images, or one image-to-multiview/edit-multiview task → 3D model', 'multi', async (client, inputs, flags) => {
|
|
69
|
+
const intent = flags.prompt ?? '';
|
|
70
|
+
const { model } = chooseModel({
|
|
71
|
+
explicit: flags.model,
|
|
72
|
+
faceLimit: numberParam(flags, 'face_limit'),
|
|
73
|
+
quad: flags.param.quad === true,
|
|
74
|
+
lowPoly: detectLowPolyIntent(intent),
|
|
75
|
+
realtime: detectRealtimeIntent(intent),
|
|
76
|
+
denseGeometry: detectDenseGeometryIntent(intent),
|
|
77
|
+
params: flags.param,
|
|
78
|
+
});
|
|
79
|
+
const kind = inputs.length === 1 ? detectInput(inputs[0]).kind : undefined;
|
|
80
|
+
if (kind === 'task-id' || kind === 'history-ref') {
|
|
81
|
+
const { value, sourceTaskType } = await resolveInputValue(client, inputs[0]);
|
|
82
|
+
if (sourceTaskType && sourceTaskType !== 'generate_multiview_image' && sourceTaskType !== 'edit_multiview_image') {
|
|
83
|
+
throw new UsageError(`multiview-to-model reuses an image-to-multiview / edit-multiview task, not ${sourceTaskType}`);
|
|
84
|
+
}
|
|
85
|
+
return buildMultiviewToModel({ task_id: value }, model, flags.param);
|
|
86
|
+
}
|
|
87
|
+
const previewViews = await assignViews(client, inputs, { dryRun: true });
|
|
88
|
+
buildMultiviewToModel(previewViews, model, flags.param);
|
|
50
89
|
const views = await assignViews(client, inputs);
|
|
51
|
-
const { model } = chooseModel({ explicit: flags.model, faceLimit: numberParam(flags, 'face_limit') });
|
|
52
90
|
return buildMultiviewToModel(views, model, flags.param);
|
|
53
91
|
});
|
|
54
92
|
add('text-to-image', 'text → image (concept art / t_pose templates)', 'prompt', async (_c, inputs, flags) => buildTextToImage(inputs.join(' '), { ...(flags.model ? { model: flags.model } : {}), ...flags.param }));
|
|
55
|
-
add('image-to-image', 'image + prompt → edited image', '
|
|
56
|
-
const { value } = await resolveInputValue(client, inputs[0]);
|
|
93
|
+
add('image-to-image', 'image(s) + prompt → edited image (several images: cite them as image[1], image[2], ...)', 'multi', async (client, inputs, flags) => {
|
|
57
94
|
const params = { ...(flags.model ? { model: flags.model } : {}), ...flags.param };
|
|
58
95
|
if (flags.prompt)
|
|
59
96
|
params.prompt = flags.prompt;
|
|
60
97
|
if (!params.prompt && !params.template) {
|
|
61
98
|
throw new UsageError('image-to-image needs --prompt or --param template=...');
|
|
62
99
|
}
|
|
63
|
-
|
|
100
|
+
// Validate deterministic parameters before resolving any local uploads.
|
|
101
|
+
buildImageToImage(inputs, params);
|
|
102
|
+
const values = [];
|
|
103
|
+
for (const input of inputs)
|
|
104
|
+
values.push((await resolveInputValue(client, input)).value);
|
|
105
|
+
return buildImageToImage(values, params);
|
|
64
106
|
});
|
|
65
107
|
add('image-to-multiview', 'single image → 4-view sheet', 'single', async (client, inputs, flags) => {
|
|
66
108
|
const { value } = await resolveInputValue(client, inputs[0]);
|
package/dist/commands/home.js
CHANGED
|
@@ -5,7 +5,6 @@ import { activeProfileName, listProfiles, loadConfig, resolveApiKey } from '../c
|
|
|
5
5
|
import { readHistory } from '../config/history.js';
|
|
6
6
|
import { TripoClient } from '../core/client.js';
|
|
7
7
|
import { isInteractive, log } from '../core/output.js';
|
|
8
|
-
import { t } from '../i18n.js';
|
|
9
8
|
import { runLogin } from './login.js';
|
|
10
9
|
export function registerHomeAction(program) {
|
|
11
10
|
program.action(async () => {
|
|
@@ -16,30 +15,30 @@ export function registerHomeAction(program) {
|
|
|
16
15
|
p.intro(pc.bold('tripo'));
|
|
17
16
|
const key = resolveApiKey(loadConfig());
|
|
18
17
|
if (!key) {
|
|
19
|
-
log(
|
|
18
|
+
log('no API key yet — log in first (new accounts get free credits)');
|
|
20
19
|
await runLogin({});
|
|
21
20
|
return;
|
|
22
21
|
}
|
|
23
22
|
let balanceLine = '';
|
|
24
23
|
try {
|
|
25
24
|
const balance = await new TripoClient().getBalance();
|
|
26
|
-
balanceLine =
|
|
25
|
+
balanceLine = `${balance.balance} credits`;
|
|
27
26
|
}
|
|
28
27
|
catch {
|
|
29
|
-
balanceLine =
|
|
28
|
+
balanceLine = 'balance unavailable';
|
|
30
29
|
}
|
|
31
30
|
const last = readHistory().filter((e) => e.status === 'success').pop();
|
|
32
31
|
const config = loadConfig();
|
|
33
|
-
const profileNote = listProfiles(config).length > 1 ?
|
|
34
|
-
log(`${balanceLine}${profileNote}${last ?
|
|
32
|
+
const profileNote = listProfiles(config).length > 1 ? ` · profile ${activeProfileName(config)}` : '';
|
|
33
|
+
log(`${balanceLine}${profileNote}${last ? ` · last task @${last.name}` : ''}`);
|
|
35
34
|
const action = await p.select({
|
|
36
|
-
message:
|
|
35
|
+
message: 'What do you want to do?',
|
|
37
36
|
options: [
|
|
38
|
-
{ value: 'ai', label:
|
|
39
|
-
{ value: 'make', label:
|
|
40
|
-
...(last ? [{ value: 'view', label:
|
|
41
|
-
{ value: 'topup', label:
|
|
42
|
-
{ value: 'help', label:
|
|
37
|
+
{ value: 'ai', label: 'describe what I want, experts plan it (tripo ai)' },
|
|
38
|
+
{ value: 'make', label: 'generate directly (tripo make ...)' },
|
|
39
|
+
...(last ? [{ value: 'view', label: 'view the last result (tripo view)' }] : []),
|
|
40
|
+
{ value: 'topup', label: 'top up credits (tripo topup)' },
|
|
41
|
+
{ value: 'help', label: 'show all commands' },
|
|
43
42
|
],
|
|
44
43
|
});
|
|
45
44
|
if (p.isCancel(action))
|
|
@@ -50,7 +49,7 @@ export function registerHomeAction(program) {
|
|
|
50
49
|
break;
|
|
51
50
|
case 'make': {
|
|
52
51
|
const promptText = await p.text({
|
|
53
|
-
message:
|
|
52
|
+
message: 'enter a description or file path',
|
|
54
53
|
});
|
|
55
54
|
if (p.isCancel(promptText) || !promptText.trim())
|
|
56
55
|
return;
|