@assethub/cli 0.1.2 → 0.1.3

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 (3) hide show
  1. package/README.md +13 -0
  2. package/dist/index.js +263 -63
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -13,6 +13,7 @@ assethub image generate --prompt "stylized prop concept" --wait
13
13
  assethub mesh generate --file ./input.png --model-id meshGen.hunyuan31 --wait
14
14
  assethub jobs watch job_123 --download --out-dir ./out/job
15
15
  assethub parts split --file ./input.png --part-extractor "V1.5" --wait --all-ready --download --out-dir ./out/parts
16
+ assethub parts compare --file ./input.png --preprocess-prompt "clean white background, centered product photo" --part-extractor "V1.5" --wait --all-ready
16
17
  ```
17
18
 
18
19
  Configuration:
@@ -28,6 +29,7 @@ printf '%s' "$ASSETHUB_API_KEY" | assethub auth login --api-key-stdin
28
29
  assethub image generate --prompt "small stylized game asset chest" --wait --download --out-dir ./out/image
29
30
  assethub mesh generate --source-url https://example.com/input.png --model-id meshGen.hunyuan31 --wait --download --out-dir ./out/mesh
30
31
  cat ./character.png | assethub parts split --stdin --file-name character.png --part-extractor "V1.5" --wait --all-ready --download --out-dir ./out/parts
32
+ cat ./character.png | assethub parts compare --stdin --file-name character.png --preprocess-prompt "clean white background" --part-extractor "V1.5" --wait --all-ready
31
33
  ```
32
34
 
33
35
  Agent-oriented input:
@@ -48,6 +50,17 @@ Agent-oriented output:
48
50
  - `--download --out-dir <dir>` recursively downloads public artifact URLs from
49
51
  job or production status payloads and writes a `manifest.json`.
50
52
 
53
+ `assethub parts compare` runs the normal part split first. When
54
+ `--preprocess-prompt <text>` is provided, it then uses the Nano Banana image
55
+ model to generate a preprocessed image, waits for that image job to finish, and
56
+ runs a second split on the generated image. The final JSON contains `direct`
57
+ and `preprocessed` sections so agents can compare both outputs. If
58
+ preprocessing fails, the direct result is still printed with
59
+ `preprocessed.ok: false`; pass `--fail-on-preprocess-error` when a non-zero exit
60
+ is preferred. Use `--all-ready` rather than explicit `--task-id` values when
61
+ preprocessing is enabled, because the generated-image split creates a separate
62
+ production order.
63
+
51
64
  Image generation defaults to the deployed `v1` endpoint. Pass `--api-version v2`
52
65
  only when the `v2` image endpoint is available in the target environment.
53
66
 
package/dist/index.js CHANGED
@@ -69,6 +69,7 @@ Usage:
69
69
  assethub production status <order-id> [--wait] [--download --out-dir <dir>]
70
70
  assethub production execute --order-id <id> --mission-id <id> --task-id <id> [--task-id <id>...] [--part-extractor <name>] [--part-extraction-mode fast|high_quality] [--wait] [--download --out-dir <dir>]
71
71
  assethub parts split (--file <path> | --source-url <url> | --source-id <id> | --image-url <url> | --file-ref-json <json> | --stdin | --data-uri <uri> | --order-id <id>) [--part-extractor <name>] [--wait] [--task-id <id>...] [--mission-id <id>] [--all-ready] [--download --out-dir <dir>]
72
+ assethub parts compare (--file <path> | --source-url <url> | --source-id <id> | --image-url <url> | --file-ref-json <json> | --stdin | --data-uri <uri>) [--preprocess-prompt <text>] [--preprocess-model-id <id>] [--fail-on-preprocess-error] [--part-extractor <name>] [--wait] [--task-id <id>...] [--mission-id <id>] [--all-ready] [--download --out-dir <dir>]
72
73
 
73
74
  Global options:
74
75
  --api-key <key> Overrides saved auth and ASSETHUB_API_KEY.
@@ -88,6 +89,7 @@ Examples:
88
89
  assethub mesh generate --file ./input.png --model-id meshGen.hunyuan31 --wait
89
90
  assethub production analyze --file ./input.png --part-extractor "V1.5" --wait
90
91
  assethub parts split --file ./input.png --part-extractor "V1.5" --wait --all-ready
92
+ assethub parts compare --file ./input.png --preprocess-prompt "clean white background, centered product photo" --part-extractor "V1.5" --wait --all-ready
91
93
  `;
92
94
  const print = (value) => {
93
95
  stdout.write(`${JSON.stringify(value, null, 2)}\n`);
@@ -458,6 +460,24 @@ const resolveProductionAnalyzeSource = async (ctx) => {
458
460
  }
459
461
  return { imageBlobLocation: source.fileRef };
460
462
  };
463
+ const productionAnalyzeSourceFromSource = (source) => {
464
+ if (source.resourceId != null) {
465
+ return { imageAssetId: source.resourceId };
466
+ }
467
+ if (source.url != null) {
468
+ return { imageUrl: source.url };
469
+ }
470
+ return { imageBlobLocation: source.fileRef };
471
+ };
472
+ const sourceFromProductionAnalyzeSource = (source) => {
473
+ if (source.imageAssetId != null) {
474
+ return { resourceId: source.imageAssetId };
475
+ }
476
+ if (source.imageUrl != null) {
477
+ return { url: source.imageUrl };
478
+ }
479
+ return { fileRef: source.imageBlobLocation };
480
+ };
461
481
  const productionAnalyzeTerminalStatuses = new Set(['ready', 'failed']);
462
482
  const productionExecuteTerminalStatuses = new Set(['completed', 'failed']);
463
483
  const pollProductionStatus = async ({ ctx, orderId, terminalStatuses, }) => {
@@ -885,6 +905,106 @@ const withOptionalDownloads = async (flags, value) => {
885
905
  const downloads = await maybeDownloadUrls(flags, value);
886
906
  return downloads == null ? value : { ...value, downloads };
887
907
  };
908
+ const firstStringValueForKeys = (value, keys) => {
909
+ if (value == null || typeof value !== 'object') {
910
+ return undefined;
911
+ }
912
+ if (Array.isArray(value)) {
913
+ for (const item of value) {
914
+ const found = firstStringValueForKeys(item, keys);
915
+ if (found != null) {
916
+ return found;
917
+ }
918
+ }
919
+ return undefined;
920
+ }
921
+ for (const [key, entry] of Object.entries(value)) {
922
+ if (keys.has(key) && typeof entry === 'string' && entry.trim() !== '') {
923
+ return entry;
924
+ }
925
+ const found = firstStringValueForKeys(entry, keys);
926
+ if (found != null) {
927
+ return found;
928
+ }
929
+ }
930
+ return undefined;
931
+ };
932
+ const firstBlobLocation = (value) => {
933
+ if (value == null || typeof value !== 'object') {
934
+ return undefined;
935
+ }
936
+ if (Array.isArray(value)) {
937
+ for (const item of value) {
938
+ const found = firstBlobLocation(item);
939
+ if (found != null) {
940
+ return found;
941
+ }
942
+ }
943
+ return undefined;
944
+ }
945
+ const record = value;
946
+ if (record.type === 'supabase' && typeof record.bucket === 'string' && typeof record.path === 'string') {
947
+ return {
948
+ type: 'supabase',
949
+ bucket: record.bucket,
950
+ path: record.path,
951
+ };
952
+ }
953
+ if (record.type === 'signedSupabase' && typeof record.signedUrl === 'string') {
954
+ return {
955
+ type: 'signedSupabase',
956
+ signedUrl: record.signedUrl,
957
+ };
958
+ }
959
+ if (record.type === 'asset' && typeof record.assetId === 'string') {
960
+ return {
961
+ type: 'asset',
962
+ assetId: record.assetId,
963
+ };
964
+ }
965
+ for (const entry of Object.values(record)) {
966
+ const found = firstBlobLocation(entry);
967
+ if (found != null) {
968
+ return found;
969
+ }
970
+ }
971
+ return undefined;
972
+ };
973
+ const imageSourceFromCompletedJob = (job) => {
974
+ if (job.resourceId != null && job.resourceId.trim() !== '') {
975
+ return { imageAssetId: job.resourceId };
976
+ }
977
+ const resourceId = firstStringValueForKeys(job, new Set(['resourceId', 'resource_id', 'assetId', 'asset_id', 'imageAssetId']));
978
+ if (resourceId != null) {
979
+ return { imageAssetId: resourceId };
980
+ }
981
+ const imageUrl = firstStringValueForKeys(job, new Set(['imageUrl', 'image_url', 'url', 'publicUrl', 'public_url']));
982
+ if (imageUrl != null) {
983
+ return { imageUrl };
984
+ }
985
+ const blobLocation = firstBlobLocation(job);
986
+ if (blobLocation != null) {
987
+ return { imageBlobLocation: blobLocation };
988
+ }
989
+ throw new Error('Preprocessing completed, but the image job did not include an image source for part splitting.');
990
+ };
991
+ const errorToOutput = (error) => {
992
+ if (error instanceof AssetHubApiError) {
993
+ return {
994
+ name: error.name,
995
+ message: error.message,
996
+ status: error.status,
997
+ code: error.code,
998
+ };
999
+ }
1000
+ if (error instanceof Error) {
1001
+ return {
1002
+ name: error.name,
1003
+ message: error.message,
1004
+ };
1005
+ }
1006
+ return { message: String(error) };
1007
+ };
888
1008
  const readyTaskIdsFromStatus = (status) => {
889
1009
  const missions = status != null && typeof status === 'object' && 'missions' in status
890
1010
  ? status.missions
@@ -996,83 +1116,163 @@ const commandProduction = async (subcommand, positionals, ctx) => {
996
1116
  throw new Error('Unknown production command. Use "production analyze", "production status", or "production execute".');
997
1117
  };
998
1118
  const commandParts = async (subcommand, ctx) => {
999
- if (subcommand !== 'split') {
1000
- throw new Error('Unknown parts command. Use "parts split".');
1001
- }
1002
- let orderId = getFlag(ctx.flags, 'order-id');
1003
- let analyzed;
1004
- let status;
1005
- if (orderId == null) {
1006
- const result = await ctx.client.v1.analyzeProduction({
1007
- ...(await resolveProductionAnalyzeSource(ctx)),
1008
- agentVersion: resolvePartExtractorForAnalyze(ctx.flags),
1009
- name: getFlag(ctx.flags, 'name'),
1010
- });
1011
- analyzed = result;
1012
- orderId = result.orderId;
1013
- if (hasFlag(ctx.flags, 'wait') || hasFlag(ctx.flags, 'all-ready')) {
1119
+ const runPartSplit = async (source) => {
1120
+ let orderId = source == null ? getFlag(ctx.flags, 'order-id') : undefined;
1121
+ let analyzed;
1122
+ let status;
1123
+ if (orderId == null) {
1124
+ const result = await ctx.client.v1.analyzeProduction({
1125
+ ...(source ?? (await resolveProductionAnalyzeSource(ctx))),
1126
+ agentVersion: resolvePartExtractorForAnalyze(ctx.flags),
1127
+ name: getFlag(ctx.flags, 'name'),
1128
+ });
1129
+ analyzed = result;
1130
+ orderId = result.orderId;
1131
+ if (hasFlag(ctx.flags, 'wait') || hasFlag(ctx.flags, 'all-ready')) {
1132
+ status = await pollProductionStatus({
1133
+ ctx,
1134
+ orderId,
1135
+ terminalStatuses: productionAnalyzeTerminalStatuses,
1136
+ });
1137
+ }
1138
+ }
1139
+ else if (hasFlag(ctx.flags, 'wait') || hasFlag(ctx.flags, 'all-ready')) {
1014
1140
  status = await pollProductionStatus({
1015
1141
  ctx,
1016
1142
  orderId,
1017
1143
  terminalStatuses: productionAnalyzeTerminalStatuses,
1018
1144
  });
1019
1145
  }
1020
- }
1021
- else if (hasFlag(ctx.flags, 'wait') || hasFlag(ctx.flags, 'all-ready')) {
1022
- status = await pollProductionStatus({
1023
- ctx,
1024
- orderId,
1025
- terminalStatuses: productionAnalyzeTerminalStatuses,
1146
+ let taskIds = getFlagValues(ctx.flags, 'task-id');
1147
+ if (hasFlag(ctx.flags, 'all-ready')) {
1148
+ if (status == null) {
1149
+ status = await ctx.client.v1.getProductionStatus(orderId);
1150
+ }
1151
+ taskIds = readyTaskIdsFromStatus(status);
1152
+ if (taskIds.length === 0) {
1153
+ throw new Error('No ready tasks were found for --all-ready.');
1154
+ }
1155
+ }
1156
+ let executed;
1157
+ let executionStatus;
1158
+ if (taskIds.length > 0) {
1159
+ if (status == null) {
1160
+ status = await ctx.client.v1.getProductionStatus(orderId);
1161
+ }
1162
+ const missionId = getFlag(ctx.flags, 'mission-id') ?? singleMissionIdFromStatus(status);
1163
+ const partExtractorFlag = getPartExtractorFlag(ctx.flags);
1164
+ const agentVersion = partExtractorFlag == null
1165
+ ? await resolvePartExtractorFromMission(ctx, orderId, missionId)
1166
+ : resolvePartExtractorPublicName(partExtractorFlag);
1167
+ const partExtractionMode = getFlag(ctx.flags, 'part-extraction-mode');
1168
+ if (partExtractionMode != null &&
1169
+ partExtractionMode !== 'fast' &&
1170
+ partExtractionMode !== 'high_quality') {
1171
+ throw new Error('--part-extraction-mode must be "fast" or "high_quality"');
1172
+ }
1173
+ executed = await ctx.client.v1.executeProduction({
1174
+ orderId,
1175
+ missionId,
1176
+ confirmedTaskIds: taskIds,
1177
+ agentVersion,
1178
+ partExtractionMode,
1179
+ });
1180
+ if (hasFlag(ctx.flags, 'wait')) {
1181
+ executionStatus = await pollProductionStatus({
1182
+ ctx,
1183
+ orderId,
1184
+ terminalStatuses: productionExecuteTerminalStatuses,
1185
+ });
1186
+ }
1187
+ }
1188
+ return sanitizeProductionOutput({
1189
+ analyzed,
1190
+ status,
1191
+ executed,
1192
+ executionStatus,
1026
1193
  });
1194
+ };
1195
+ if (subcommand === 'split') {
1196
+ print(await withOptionalDownloads(ctx.flags, await runPartSplit()));
1197
+ return;
1027
1198
  }
1028
- let taskIds = getFlagValues(ctx.flags, 'task-id');
1029
- if (hasFlag(ctx.flags, 'all-ready')) {
1030
- if (status == null) {
1031
- status = await ctx.client.v1.getProductionStatus(orderId);
1199
+ if (subcommand === 'compare') {
1200
+ if (getFlag(ctx.flags, 'order-id') != null) {
1201
+ throw new Error('parts compare requires an image source. Use --file, --source-url, --source-id, --image-url, --file-ref-json, --stdin, or --data-uri.');
1032
1202
  }
1033
- taskIds = readyTaskIdsFromStatus(status);
1034
- if (taskIds.length === 0) {
1035
- throw new Error('No ready tasks were found for --all-ready.');
1203
+ const preprocessPrompt = getFlag(ctx.flags, 'preprocess-prompt');
1204
+ if (preprocessPrompt === 'true') {
1205
+ throw new Error('Missing required flag value: --preprocess-prompt <text>');
1036
1206
  }
1037
- }
1038
- let executed;
1039
- let executionStatus;
1040
- if (taskIds.length > 0) {
1041
- if (status == null) {
1042
- status = await ctx.client.v1.getProductionStatus(orderId);
1207
+ if (preprocessPrompt != null && getFlagValues(ctx.flags, 'task-id').length > 0) {
1208
+ throw new Error('parts compare preprocessing creates a second order. Use --all-ready instead of --task-id.');
1043
1209
  }
1044
- const missionId = getFlag(ctx.flags, 'mission-id') ?? singleMissionIdFromStatus(status);
1045
- const partExtractorFlag = getPartExtractorFlag(ctx.flags);
1046
- const agentVersion = partExtractorFlag == null
1047
- ? await resolvePartExtractorFromMission(ctx, orderId, missionId)
1048
- : resolvePartExtractorPublicName(partExtractorFlag);
1049
- const partExtractionMode = getFlag(ctx.flags, 'part-extraction-mode');
1050
- if (partExtractionMode != null &&
1051
- partExtractionMode !== 'fast' &&
1052
- partExtractionMode !== 'high_quality') {
1053
- throw new Error('--part-extraction-mode must be "fast" or "high_quality"');
1210
+ if (preprocessPrompt != null && getFlag(ctx.flags, 'mission-id') != null) {
1211
+ throw new Error('parts compare preprocessing creates a second order. Omit --mission-id and let the CLI infer it.');
1054
1212
  }
1055
- executed = await ctx.client.v1.executeProduction({
1056
- orderId,
1057
- missionId,
1058
- confirmedTaskIds: taskIds,
1059
- agentVersion,
1060
- partExtractionMode,
1061
- });
1062
- if (hasFlag(ctx.flags, 'wait')) {
1063
- executionStatus = await pollProductionStatus({
1064
- ctx,
1065
- orderId,
1066
- terminalStatuses: productionExecuteTerminalStatuses,
1067
- });
1213
+ const imageUrlFlag = getFlag(ctx.flags, 'image-url');
1214
+ if (imageUrlFlag === 'true') {
1215
+ throw new Error('Missing required flag value: --image-url <url>');
1216
+ }
1217
+ const inputSource = imageUrlFlag == null
1218
+ ? productionAnalyzeSourceFromSource(await requireSource({
1219
+ flags: ctx.flags,
1220
+ client: ctx.client,
1221
+ fileMediaType: 'image',
1222
+ }))
1223
+ : { imageUrl: imageUrlFlag };
1224
+ const direct = await runPartSplit(inputSource);
1225
+ let preprocessed = null;
1226
+ if (preprocessPrompt != null && preprocessPrompt !== 'true') {
1227
+ try {
1228
+ const queued = await ctx.client.v2.generateImage({
1229
+ source: sourceFromProductionAnalyzeSource(inputSource),
1230
+ prompt: preprocessPrompt,
1231
+ modelId: getFlag(ctx.flags, 'preprocess-model-id') ?? defaultImageModelId,
1232
+ batchSize: 1,
1233
+ negativePrompt: getFlag(ctx.flags, 'preprocess-negative-prompt'),
1234
+ seed: getFlag(ctx.flags, 'preprocess-seed') == null
1235
+ ? undefined
1236
+ : parsePositiveIntegerFlag(ctx.flags, 'preprocess-seed', 1),
1237
+ });
1238
+ const job = await ctx.client.v2.pollJob(queued.jobId, {
1239
+ intervalMs: parsePositiveIntegerFlag(ctx.flags, 'interval-ms', 5000),
1240
+ timeoutMs: parsePositiveIntegerFlag(ctx.flags, 'timeout-ms', 900000),
1241
+ onPoll: current => {
1242
+ stderr.write(`[poll] preprocess job=${current.id} status=${current.status}\n`);
1243
+ },
1244
+ });
1245
+ if (job.status !== 'completed') {
1246
+ throw new Error(`Preprocessing failed: job=${job.id} status=${job.status}`);
1247
+ }
1248
+ const generatedSource = imageSourceFromCompletedJob(job);
1249
+ preprocessed = {
1250
+ ok: true,
1251
+ image: {
1252
+ queued,
1253
+ job,
1254
+ source: generatedSource,
1255
+ },
1256
+ split: await runPartSplit(generatedSource),
1257
+ };
1258
+ }
1259
+ catch (error) {
1260
+ if (hasFlag(ctx.flags, 'fail-on-preprocess-error')) {
1261
+ throw error;
1262
+ }
1263
+ preprocessed = {
1264
+ ok: false,
1265
+ error: errorToOutput(error),
1266
+ };
1267
+ }
1068
1268
  }
1269
+ print(await withOptionalDownloads(ctx.flags, {
1270
+ direct,
1271
+ preprocessed,
1272
+ }));
1273
+ return;
1069
1274
  }
1070
- print(await withOptionalDownloads(ctx.flags, sanitizeProductionOutput({
1071
- analyzed,
1072
- status,
1073
- executed,
1074
- executionStatus,
1075
- })));
1275
+ throw new Error('Unknown parts command. Use "parts split" or "parts compare".');
1076
1276
  };
1077
1277
  const run = async () => {
1078
1278
  const parsed = parseArgs(argv.slice(2));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@assethub/cli",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "Command line interface for AssetHub.",
5
5
  "type": "module",
6
6
  "bin": {