@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.
- package/README.md +13 -0
- package/dist/index.js +263 -63
- 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
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
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
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
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
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
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
|
-
|
|
1034
|
-
if (
|
|
1035
|
-
throw new Error('
|
|
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
|
-
|
|
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
|
-
|
|
1045
|
-
|
|
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
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
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
|
-
|
|
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));
|