focalapi-cli 0.4.0 → 0.5.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 +18 -0
- package/dist/cli.js +305 -35
- package/package.json +1 -1
- package/skills/focalapi-gen/SKILL.md +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.5.0 - 2026-08-25
|
|
4
|
+
|
|
5
|
+
- Fixed `krea-2-medium/-turbo/large` reference-image validation: the local table said 0 while the platform contract accepts up to 10 reference images (each maps to a Krea style reference), so every `gen image --image` edit was rejected locally without reaching the server (2026-08-25 live audit P01).
|
|
6
|
+
- Added local constraints for the August image wave so invalid parameters are intercepted before submission: `topaz-image-reimagine`/`-bloom-2`/`-wonder-3-5`, `wavespeed-seedvr2-upscale`/`-ultimate-upscale`, `quiver-text-to-svg`/`-image-to-svg`, `hitpaw-image-enhance`/`-portrait-enhance`, `beeble-switchx-image-720p`/`-1080p`, `recraft-v4`/`-v4-pro`.
|
|
7
|
+
- Enforce the exact-one-input-image requirement locally for the enhance/upscale/vectorize/switch families instead of surfacing the server 400.
|
|
8
|
+
- Map `--resolution` to the `target_resolution` wire field for WaveSpeed upscales (the generic `resolution` field is silently ignored by that family).
|
|
9
|
+
- Send Topaz `--creativity` as an integer 1-9 (the platform contract takes a number; the Krea string modes are unchanged).
|
|
10
|
+
- Validate Recraft sizes against the official V4 tables (non-pro caps at ~1.5K, pro at ~3K) and reject reference images locally (the channel is text-to-image only).
|
|
11
|
+
- Intercept `gen image` on Gemini image models with a direct pointer to `gen gemini-image` (native generateContent endpoint) instead of an opaque upstream failure (audit P07).
|
|
12
|
+
|
|
13
|
+
## 0.4.1 - 2026-08-18
|
|
14
|
+
|
|
15
|
+
- Added `@file` syntax for local reference media on all gen paths (`--image @C:/path/ref.jpg`): the file is read and inlined as a data URI with per-file 8MB and per-command 12MB guards — local files no longer require manual hosting.
|
|
16
|
+
- Bare local paths (e.g., `C:/...` without `@`) are intercepted locally with actionable guidance to add the `@` prefix or host the file, instead of round-tripping to the server's 400.
|
|
17
|
+
- Added `--content @file.json` and `--content -` (stdin) for gen video, bypassing the Windows ~32KB argv limit for multi-image content arrays; capped at 64MB.
|
|
18
|
+
- Added `task download --direct`: fetches the task's upstream artifact URL directly (CDN/edge typically beats the cross-border gateway hop; measured 328KB/s client-side vs 26MB/s server-local) and falls back to the gateway proxy automatically.
|
|
19
|
+
- Added grok video prompt budget validation: the upstream 4096-character limit applies to the composite of text plus ~500 characters per reference image — the CLI mirrors the gateway's calibrated gate and rejects locally before submission.
|
|
20
|
+
|
|
3
21
|
## 0.4.0 - 2026-08-18
|
|
4
22
|
|
|
5
23
|
- Added `task status --wait [--download]`: built-in bounded polling with elapsed-time reporting and an optional auto-download, so agents never need to write their own poller (the audited agent session's custom script crashed on the Windows `.cmd` shim and stalled blind).
|
package/dist/cli.js
CHANGED
|
@@ -176,7 +176,7 @@ function displayWidth(s) {
|
|
|
176
176
|
}
|
|
177
177
|
|
|
178
178
|
// src/lib/version.ts
|
|
179
|
-
var VERSION = true ? "0.
|
|
179
|
+
var VERSION = true ? "0.5.0" : "0.0.0-dev";
|
|
180
180
|
|
|
181
181
|
// src/commands/auth.ts
|
|
182
182
|
import { createInterface } from "readline/promises";
|
|
@@ -912,8 +912,8 @@ function registerDoctor(program) {
|
|
|
912
912
|
|
|
913
913
|
// src/commands/gen.ts
|
|
914
914
|
import { createWriteStream as createWriteStream2 } from "fs";
|
|
915
|
-
import { mkdir as mkdir2, writeFile } from "fs/promises";
|
|
916
|
-
import { join as join3, resolve as resolve2 } from "path";
|
|
915
|
+
import { mkdir as mkdir2, readFile, writeFile } from "fs/promises";
|
|
916
|
+
import { extname as extname2, join as join3, resolve as resolve2 } from "path";
|
|
917
917
|
import { pipeline as pipeline2 } from "stream/promises";
|
|
918
918
|
import { Readable as Readable2 } from "stream";
|
|
919
919
|
import { randomUUID } from "crypto";
|
|
@@ -1050,7 +1050,7 @@ var IMAGE_CONSTRAINTS = {
|
|
|
1050
1050
|
},
|
|
1051
1051
|
"krea-2-medium": {
|
|
1052
1052
|
maxN: 1,
|
|
1053
|
-
maxReferenceImages:
|
|
1053
|
+
maxReferenceImages: 10,
|
|
1054
1054
|
aspectRatios: KREA_IMAGE_ASPECT_RATIOS,
|
|
1055
1055
|
resolutions: ["1k"],
|
|
1056
1056
|
supportsSeed: true,
|
|
@@ -1061,7 +1061,7 @@ var IMAGE_CONSTRAINTS = {
|
|
|
1061
1061
|
},
|
|
1062
1062
|
"krea-2-medium-turbo": {
|
|
1063
1063
|
maxN: 1,
|
|
1064
|
-
maxReferenceImages:
|
|
1064
|
+
maxReferenceImages: 10,
|
|
1065
1065
|
aspectRatios: KREA_IMAGE_ASPECT_RATIOS,
|
|
1066
1066
|
resolutions: ["1k"],
|
|
1067
1067
|
supportsSeed: true,
|
|
@@ -1072,7 +1072,7 @@ var IMAGE_CONSTRAINTS = {
|
|
|
1072
1072
|
},
|
|
1073
1073
|
"krea-2-large": {
|
|
1074
1074
|
maxN: 1,
|
|
1075
|
-
maxReferenceImages:
|
|
1075
|
+
maxReferenceImages: 10,
|
|
1076
1076
|
aspectRatios: KREA_IMAGE_ASPECT_RATIOS,
|
|
1077
1077
|
resolutions: ["1k"],
|
|
1078
1078
|
supportsSeed: true,
|
|
@@ -1106,8 +1106,112 @@ var IMAGE_CONSTRAINTS = {
|
|
|
1106
1106
|
supportsNegativePrompt: true,
|
|
1107
1107
|
supportsPromptExtend: true,
|
|
1108
1108
|
supportsWatermark: true
|
|
1109
|
+
},
|
|
1110
|
+
"topaz-image-reimagine": {
|
|
1111
|
+
maxN: 1,
|
|
1112
|
+
minImageCount: 1,
|
|
1113
|
+
maxReferenceImages: 1,
|
|
1114
|
+
integerCreativity: [1, 9]
|
|
1115
|
+
},
|
|
1116
|
+
"topaz-image-bloom-2": {
|
|
1117
|
+
maxN: 1,
|
|
1118
|
+
minImageCount: 1,
|
|
1119
|
+
maxReferenceImages: 1
|
|
1120
|
+
},
|
|
1121
|
+
"topaz-image-wonder-3-5": {
|
|
1122
|
+
maxN: 1,
|
|
1123
|
+
minImageCount: 1,
|
|
1124
|
+
maxReferenceImages: 1
|
|
1125
|
+
},
|
|
1126
|
+
"wavespeed-seedvr2-upscale": {
|
|
1127
|
+
maxN: 1,
|
|
1128
|
+
minImageCount: 1,
|
|
1129
|
+
maxReferenceImages: 1,
|
|
1130
|
+
resolutions: ["2k", "4k", "8k"],
|
|
1131
|
+
resolutionParam: "target_resolution"
|
|
1132
|
+
},
|
|
1133
|
+
"wavespeed-ultimate-upscale": {
|
|
1134
|
+
maxN: 1,
|
|
1135
|
+
minImageCount: 1,
|
|
1136
|
+
maxReferenceImages: 1,
|
|
1137
|
+
resolutions: ["2k", "4k", "8k"],
|
|
1138
|
+
resolutionParam: "target_resolution"
|
|
1139
|
+
},
|
|
1140
|
+
"quiver-text-to-svg": {
|
|
1141
|
+
maxN: 1,
|
|
1142
|
+
maxReferenceImages: 4
|
|
1143
|
+
},
|
|
1144
|
+
"quiver-image-to-svg": {
|
|
1145
|
+
maxN: 1,
|
|
1146
|
+
minImageCount: 1,
|
|
1147
|
+
maxReferenceImages: 1
|
|
1148
|
+
},
|
|
1149
|
+
"hitpaw-image-enhance": {
|
|
1150
|
+
maxN: 1,
|
|
1151
|
+
minImageCount: 1,
|
|
1152
|
+
maxReferenceImages: 1
|
|
1153
|
+
},
|
|
1154
|
+
"hitpaw-image-portrait-enhance": {
|
|
1155
|
+
maxN: 1,
|
|
1156
|
+
minImageCount: 1,
|
|
1157
|
+
maxReferenceImages: 1
|
|
1158
|
+
},
|
|
1159
|
+
"beeble-switchx-image-720p": {
|
|
1160
|
+
maxN: 1,
|
|
1161
|
+
minImageCount: 1,
|
|
1162
|
+
maxReferenceImages: 2
|
|
1163
|
+
},
|
|
1164
|
+
"beeble-switchx-image-1080p": {
|
|
1165
|
+
maxN: 1,
|
|
1166
|
+
minImageCount: 1,
|
|
1167
|
+
maxReferenceImages: 2
|
|
1168
|
+
},
|
|
1169
|
+
"recraft-v4": {
|
|
1170
|
+
defaultSize: "1024x1024",
|
|
1171
|
+
maxN: 6,
|
|
1172
|
+
maxReferenceImages: 0,
|
|
1173
|
+
allowedSizes: [
|
|
1174
|
+
"1024x1024",
|
|
1175
|
+
"1280x832",
|
|
1176
|
+
"832x1280",
|
|
1177
|
+
"1216x896",
|
|
1178
|
+
"896x1216",
|
|
1179
|
+
"1152x896",
|
|
1180
|
+
"896x1152",
|
|
1181
|
+
"1536x768",
|
|
1182
|
+
"768x1536",
|
|
1183
|
+
"1344x768",
|
|
1184
|
+
"768x1344",
|
|
1185
|
+
"832x1344",
|
|
1186
|
+
"1280x896",
|
|
1187
|
+
"896x1280"
|
|
1188
|
+
]
|
|
1189
|
+
},
|
|
1190
|
+
"recraft-v4-pro": {
|
|
1191
|
+
defaultSize: "2048x2048",
|
|
1192
|
+
maxN: 6,
|
|
1193
|
+
maxReferenceImages: 0,
|
|
1194
|
+
allowedSizes: [
|
|
1195
|
+
"2048x2048",
|
|
1196
|
+
"2560x1664",
|
|
1197
|
+
"1664x2560",
|
|
1198
|
+
"2432x1792",
|
|
1199
|
+
"1792x2432",
|
|
1200
|
+
"2304x1792",
|
|
1201
|
+
"1792x2304",
|
|
1202
|
+
"3072x1536",
|
|
1203
|
+
"1536x3072",
|
|
1204
|
+
"2688x1536",
|
|
1205
|
+
"1536x2688",
|
|
1206
|
+
"1664x2688",
|
|
1207
|
+
"2560x1792",
|
|
1208
|
+
"1792x2560"
|
|
1209
|
+
]
|
|
1109
1210
|
}
|
|
1110
1211
|
};
|
|
1212
|
+
function getImageConstraint(model) {
|
|
1213
|
+
return IMAGE_CONSTRAINTS[model.trim()];
|
|
1214
|
+
}
|
|
1111
1215
|
var SEEDANCE_RATIOS = ["adaptive", "16:9", "4:3", "1:1", "3:4", "9:16", "21:9"];
|
|
1112
1216
|
var GROK_VIDEO_ASPECT_RATIOS = ["auto", "16:9", "4:3", "3:2", "1:1", "2:3", "3:4", "9:16"];
|
|
1113
1217
|
var KLING_VIDEO_ASPECT_RATIOS = ["16:9", "9:16", "1:1"];
|
|
@@ -1158,7 +1262,8 @@ var VIDEO_CONSTRAINTS = {
|
|
|
1158
1262
|
disallowReferences: true,
|
|
1159
1263
|
maxFirstFrameImages: 1,
|
|
1160
1264
|
supportsGenerateAudio: false,
|
|
1161
|
-
supportsWatermark: false
|
|
1265
|
+
supportsWatermark: false,
|
|
1266
|
+
maxPromptRunes: 4096
|
|
1162
1267
|
},
|
|
1163
1268
|
"grok-imagine-video-1.5": {
|
|
1164
1269
|
resolutions: ["480p", "720p", "1080p"],
|
|
@@ -1169,7 +1274,8 @@ var VIDEO_CONSTRAINTS = {
|
|
|
1169
1274
|
referenceResolutions: ["480p", "720p"],
|
|
1170
1275
|
maxFirstFrameImages: 1,
|
|
1171
1276
|
supportsGenerateAudio: false,
|
|
1172
|
-
supportsWatermark: false
|
|
1277
|
+
supportsWatermark: false,
|
|
1278
|
+
maxPromptRunes: 4096
|
|
1173
1279
|
},
|
|
1174
1280
|
"kling-3.0": {
|
|
1175
1281
|
resolutions: ["720p", "1080p", "4k"],
|
|
@@ -1283,11 +1389,18 @@ function validateImageGeneration(model, input) {
|
|
|
1283
1389
|
if (input.responseFormat && input.responseFormat !== "url" && input.responseFormat !== "b64_json") {
|
|
1284
1390
|
throw new ApiError("invalid_request", `response_format must be url or b64_json (received: ${input.responseFormat})`);
|
|
1285
1391
|
}
|
|
1286
|
-
const
|
|
1392
|
+
const trimmedModel = model.trim();
|
|
1393
|
+
if (GEMINI_IMAGE_CONSTRAINTS[trimmedModel]) {
|
|
1394
|
+
throw new ApiError("invalid_request", `${model} is a Gemini image model; use \`focalapi gen gemini-image\` (native generateContent endpoint) instead of \`gen image\``);
|
|
1395
|
+
}
|
|
1396
|
+
const constraint = IMAGE_CONSTRAINTS[trimmedModel];
|
|
1287
1397
|
if (!constraint) return;
|
|
1288
1398
|
if (input.n < 1 || input.n > constraint.maxN) {
|
|
1289
1399
|
throw new ApiError("invalid_request", `${model} n must be 1-${constraint.maxN} (received: ${input.n})`);
|
|
1290
1400
|
}
|
|
1401
|
+
if (input.imageCount !== void 0 && constraint.minImageCount !== void 0 && input.imageCount < constraint.minImageCount) {
|
|
1402
|
+
throw new ApiError("invalid_request", `${model} requires at least ${constraint.minImageCount} input image${constraint.minImageCount === 1 ? "" : "s"}`);
|
|
1403
|
+
}
|
|
1291
1404
|
if (input.imageCount !== void 0 && constraint.maxReferenceImages !== void 0 && input.imageCount > constraint.maxReferenceImages) {
|
|
1292
1405
|
throw new ApiError("invalid_request", `${model} supports at most ${constraint.maxReferenceImages} reference images`);
|
|
1293
1406
|
}
|
|
@@ -1303,7 +1416,16 @@ function validateImageGeneration(model, input) {
|
|
|
1303
1416
|
validateOptionalChoice(model, "resolution", input.resolution, constraint.resolutions);
|
|
1304
1417
|
validateOptionalChoice(model, "output_format", input.outputFormat, constraint.outputFormats);
|
|
1305
1418
|
validateOptionalChoice(model, "optimize_prompt", input.optimizePrompt, constraint.optimizePromptModes);
|
|
1306
|
-
|
|
1419
|
+
if (!constraint.integerCreativity) {
|
|
1420
|
+
validateOptionalChoice(model, "creativity", input.creativity, constraint.creativityModes);
|
|
1421
|
+
}
|
|
1422
|
+
if (input.creativity !== void 0 && constraint.integerCreativity) {
|
|
1423
|
+
const [minimum, maximum] = constraint.integerCreativity;
|
|
1424
|
+
const creativity = Number(input.creativity);
|
|
1425
|
+
if (!Number.isInteger(creativity) || creativity < minimum || creativity > maximum) {
|
|
1426
|
+
throw new ApiError("invalid_request", `${model} creativity must be an integer ${minimum}-${maximum} (received: ${input.creativity})`);
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
1307
1429
|
if (input.negativePrompt !== void 0 && !constraint.supportsNegativePrompt) {
|
|
1308
1430
|
throw new ApiError("invalid_request", `${model} does not support negative_prompt`);
|
|
1309
1431
|
}
|
|
@@ -1338,6 +1460,12 @@ function validateImageGeneration(model, input) {
|
|
|
1338
1460
|
return;
|
|
1339
1461
|
}
|
|
1340
1462
|
const suppliedSize = input.size ?? constraint.defaultSize;
|
|
1463
|
+
if (constraint.allowedSizes) {
|
|
1464
|
+
if (!constraint.allowedSizes.includes(suppliedSize.toLowerCase())) {
|
|
1465
|
+
throw new ApiError("invalid_request", `${model} size must be one of ${constraint.allowedSizes.join(", ")} (received: ${suppliedSize})`);
|
|
1466
|
+
}
|
|
1467
|
+
return;
|
|
1468
|
+
}
|
|
1341
1469
|
if (constraint.sizeTiers?.includes(suppliedSize.toLowerCase())) return;
|
|
1342
1470
|
if (constraint.sizeTiers && !/^\d+x\d+$/i.test(suppliedSize)) {
|
|
1343
1471
|
throw new ApiError("invalid_request", `${model} size must be one of ${constraint.sizeTiers.join(", ")} or WIDTHxHEIGHT (received: ${suppliedSize})`);
|
|
@@ -1395,6 +1523,12 @@ function validateGeminiImageGeneration(model, input) {
|
|
|
1395
1523
|
function validateVideoGeneration(model, input) {
|
|
1396
1524
|
const constraint = VIDEO_CONSTRAINTS[model.trim()];
|
|
1397
1525
|
if (!constraint) return;
|
|
1526
|
+
if (constraint.maxPromptRunes !== void 0 && input.promptRunes !== void 0) {
|
|
1527
|
+
const referenceCount = input.imageCount ?? input.firstFrameCount ?? 0;
|
|
1528
|
+
if (input.promptRunes + 500 * referenceCount > constraint.maxPromptRunes) {
|
|
1529
|
+
throw new ApiError("invalid_request", `${model} prompt budget exceeded: upstream counts prompt text plus ~500 characters per reference image against its ${constraint.maxPromptRunes}-character limit (received ${input.promptRunes} characters of text + ${referenceCount} reference images)`);
|
|
1530
|
+
}
|
|
1531
|
+
}
|
|
1398
1532
|
if ((input.imageCount ?? 0) > 0 && (input.firstFrameCount ?? 0) > 0) {
|
|
1399
1533
|
throw new ApiError("invalid_request", `${model}: --image and --first-frame are mutually exclusive (reference-to-video vs image-to-video)`);
|
|
1400
1534
|
}
|
|
@@ -1659,6 +1793,46 @@ var EXT_BY_CONTENT_TYPE = {
|
|
|
1659
1793
|
"audio/mpeg": ".mp3",
|
|
1660
1794
|
"audio/wav": ".wav"
|
|
1661
1795
|
};
|
|
1796
|
+
function extractTaskArtifactURL(raw) {
|
|
1797
|
+
if (!raw || typeof raw !== "object") return void 0;
|
|
1798
|
+
const obj = raw;
|
|
1799
|
+
const data = obj.data;
|
|
1800
|
+
const candidates = [obj.url, data?.url, data?.data?.url, data?.data?.video_url];
|
|
1801
|
+
for (const candidate of candidates) {
|
|
1802
|
+
if (typeof candidate === "string" && /^https?:\/\//.test(candidate)) return candidate;
|
|
1803
|
+
}
|
|
1804
|
+
return void 0;
|
|
1805
|
+
}
|
|
1806
|
+
async function downloadTaskArtifact(baseUrl, apiKey, taskId, outDir, opts) {
|
|
1807
|
+
let upstreamURL;
|
|
1808
|
+
if (opts?.direct) {
|
|
1809
|
+
const info2 = await fetchTask(baseUrl, apiKey, taskId);
|
|
1810
|
+
upstreamURL = extractTaskArtifactURL(info2.raw);
|
|
1811
|
+
if (upstreamURL) {
|
|
1812
|
+
try {
|
|
1813
|
+
const res = await fetch(upstreamURL, {
|
|
1814
|
+
signal: AbortSignal.timeout(6e5)
|
|
1815
|
+
});
|
|
1816
|
+
if (res.ok && res.body) {
|
|
1817
|
+
const contentType = res.headers.get("content-type")?.split(";")[0]?.trim() ?? "";
|
|
1818
|
+
const ext = EXT_BY_CONTENT_TYPE[contentType] ?? extFromURL(upstreamURL) ?? ".bin";
|
|
1819
|
+
const dir = resolve(outDir);
|
|
1820
|
+
await mkdir(dir, { recursive: true });
|
|
1821
|
+
const filePath = join2(dir, `${opts?.filenameBase ?? `task-${taskId}`}${ext}`);
|
|
1822
|
+
await pipeline(Readable.fromWeb(res.body), createWriteStream(filePath));
|
|
1823
|
+
return { file: filePath, source: "upstream" };
|
|
1824
|
+
}
|
|
1825
|
+
} catch {
|
|
1826
|
+
}
|
|
1827
|
+
}
|
|
1828
|
+
}
|
|
1829
|
+
const file = await downloadTaskContent(baseUrl, apiKey, taskId, outDir, opts?.filenameBase);
|
|
1830
|
+
return { file, source: "proxy" };
|
|
1831
|
+
}
|
|
1832
|
+
function extFromURL(url) {
|
|
1833
|
+
const match = /\.(mp4|webm|png|jpe?g|webp|mp3|wav)(?:[?#]|$)/i.exec(url);
|
|
1834
|
+
return match ? match[1].toLowerCase() === "jpeg" ? ".jpg" : `.${match[1].toLowerCase()}` : void 0;
|
|
1835
|
+
}
|
|
1662
1836
|
async function downloadTaskContent(baseUrl, apiKey, taskId, outDir, filenameBase) {
|
|
1663
1837
|
const res = await rawRequest({
|
|
1664
1838
|
baseUrl,
|
|
@@ -1769,6 +1943,93 @@ function parseJsonArray(raw, option) {
|
|
|
1769
1943
|
throw new ApiError("invalid_request", `--${option} must be a JSON array`);
|
|
1770
1944
|
}
|
|
1771
1945
|
}
|
|
1946
|
+
var MAX_CONTENT_FILE_BYTES = 64 * 1024 * 1024;
|
|
1947
|
+
async function readContentArgument(raw) {
|
|
1948
|
+
if (raw === "-") {
|
|
1949
|
+
const chunks = [];
|
|
1950
|
+
for await (const chunk of process.stdin) {
|
|
1951
|
+
chunks.push(Buffer.from(chunk));
|
|
1952
|
+
if (Buffer.concat(chunks).byteLength > MAX_CONTENT_FILE_BYTES) {
|
|
1953
|
+
throw new ApiError("invalid_request", "--content stdin \u8F93\u5165\u8D85\u8FC7 64MB \u4E0A\u9650");
|
|
1954
|
+
}
|
|
1955
|
+
}
|
|
1956
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
1957
|
+
}
|
|
1958
|
+
if (raw.startsWith("@")) {
|
|
1959
|
+
const path = raw.slice(1);
|
|
1960
|
+
let content;
|
|
1961
|
+
try {
|
|
1962
|
+
content = await readFile(path);
|
|
1963
|
+
} catch (err) {
|
|
1964
|
+
throw new ApiError("invalid_request", `\u8BFB\u53D6 --content \u6587\u4EF6\u5931\u8D25 @${path}\uFF1A${err.message}`);
|
|
1965
|
+
}
|
|
1966
|
+
if (content.byteLength > MAX_CONTENT_FILE_BYTES) {
|
|
1967
|
+
throw new ApiError("invalid_request", `--content \u6587\u4EF6 ${(content.byteLength / 1048576).toFixed(1)}MB \u8D85\u8FC7 64MB \u4E0A\u9650`);
|
|
1968
|
+
}
|
|
1969
|
+
return content.toString("utf8");
|
|
1970
|
+
}
|
|
1971
|
+
return raw;
|
|
1972
|
+
}
|
|
1973
|
+
var MAX_LOCAL_MEDIA_BYTES = 8 * 1024 * 1024;
|
|
1974
|
+
var MAX_TOTAL_LOCAL_MEDIA_BYTES = 12 * 1024 * 1024;
|
|
1975
|
+
var MEDIA_MIME_BY_EXT = {
|
|
1976
|
+
".jpg": "image/jpeg",
|
|
1977
|
+
".jpeg": "image/jpeg",
|
|
1978
|
+
".png": "image/png",
|
|
1979
|
+
".webp": "image/webp",
|
|
1980
|
+
".gif": "image/gif",
|
|
1981
|
+
".mp4": "video/mp4",
|
|
1982
|
+
".webm": "video/webm",
|
|
1983
|
+
".mov": "video/quicktime",
|
|
1984
|
+
".mp3": "audio/mpeg",
|
|
1985
|
+
".wav": "audio/wav"
|
|
1986
|
+
};
|
|
1987
|
+
async function resolveMediaInputs(sources) {
|
|
1988
|
+
let totalLocalBytes = 0;
|
|
1989
|
+
const resolved = [];
|
|
1990
|
+
for (const source of sources) {
|
|
1991
|
+
if (!source) continue;
|
|
1992
|
+
if (!source.startsWith("@")) {
|
|
1993
|
+
const lowered = source.toLowerCase();
|
|
1994
|
+
if (!source.startsWith("data:") && !lowered.startsWith("http://") && !lowered.startsWith("https://")) {
|
|
1995
|
+
throw new ApiError("invalid_request", `\u53C2\u8003\u5A92\u4F53\u5FC5\u987B\u662F http(s) URL\u3001data URI \u6216 @\u672C\u5730\u8DEF\u5F84\uFF08\u6536\u5230\uFF1A${source.slice(0, 80)}\uFF09`, {
|
|
1996
|
+
hint: "\u672C\u5730\u6587\u4EF6\u8BF7\u52A0 @ \u524D\u7F00\u5185\u8054\uFF08\u5982 --image @C:/imgs/ref.png\uFF0C\u5355\u6587\u4EF6 \u22648MB\uFF09\uFF1B\u5927\u56FE/\u591A\u56FE\u8BF7\u5148\u6258\u7BA1\u4E3A URL \u518D\u4F20\u5165\u3002"
|
|
1997
|
+
});
|
|
1998
|
+
}
|
|
1999
|
+
resolved.push(source);
|
|
2000
|
+
continue;
|
|
2001
|
+
}
|
|
2002
|
+
const path = source.slice(1);
|
|
2003
|
+
let content;
|
|
2004
|
+
try {
|
|
2005
|
+
content = await readFile(path);
|
|
2006
|
+
} catch (err) {
|
|
2007
|
+
throw new ApiError("invalid_request", `\u8BFB\u53D6\u672C\u5730\u5A92\u4F53\u5931\u8D25 @${path}\uFF1A${err.message}`, {
|
|
2008
|
+
hint: "@ \u540E\u9762\u662F\u6587\u4EF6\u8DEF\u5F84\uFF08\u5982 --image @C:/imgs/ref.png\uFF09\u3002\u82E5\u6587\u4EF6\u5728\u522B\u7684\u4E3B\u673A\u4E0A\uFF0C\u8BF7\u5148\u6258\u7BA1\u5E76\u4F20 http(s) URL\u3002"
|
|
2009
|
+
});
|
|
2010
|
+
}
|
|
2011
|
+
const ext = extname2(path).toLowerCase();
|
|
2012
|
+
const mime = MEDIA_MIME_BY_EXT[ext];
|
|
2013
|
+
if (!mime) {
|
|
2014
|
+
throw new ApiError("invalid_request", `\u4E0D\u652F\u6301\u7684\u672C\u5730\u5A92\u4F53\u7C7B\u578B ${ext}\uFF08@${path}\uFF09`, {
|
|
2015
|
+
hint: `\u652F\u6301\uFF1A${Object.keys(MEDIA_MIME_BY_EXT).join(" ")}`
|
|
2016
|
+
});
|
|
2017
|
+
}
|
|
2018
|
+
if (content.byteLength > MAX_LOCAL_MEDIA_BYTES) {
|
|
2019
|
+
throw new ApiError("invalid_request", `\u672C\u5730\u5A92\u4F53 @${path} \u4E3A ${(content.byteLength / 1048576).toFixed(1)}MB\uFF0C\u8D85\u8FC7\u5355\u6587\u4EF6 ${MAX_LOCAL_MEDIA_BYTES / 1048576}MB \u4E0A\u9650`, {
|
|
2020
|
+
hint: "\u5927\u6587\u4EF6\u8BF7\u5148\u538B\u7F29\u6216\u6258\u7BA1\u4E3A URL \u518D\u4F20\u5165\uFF1B\u547D\u4EE4\u884C\u5185\u8054\u4EC5\u9002\u5408\u4E2D\u5C0F\u56FE\u3002"
|
|
2021
|
+
});
|
|
2022
|
+
}
|
|
2023
|
+
totalLocalBytes += content.byteLength;
|
|
2024
|
+
if (totalLocalBytes > MAX_TOTAL_LOCAL_MEDIA_BYTES) {
|
|
2025
|
+
throw new ApiError("invalid_request", `\u672C\u5730\u5A92\u4F53\u603B\u91CF\u8D85\u8FC7 ${MAX_TOTAL_LOCAL_MEDIA_BYTES / 1048576}MB \u4E0A\u9650`, {
|
|
2026
|
+
hint: "\u591A\u5F20\u5927\u56FE\u8BF7\u5148\u6258\u7BA1\u4E3A URL\uFF08\u751F\u6210\u7C7B\u63A5\u53E3\u8FD4\u56DE\u7684\u4EA7\u7269 URL \u53EF\u76F4\u63A5\u590D\u7528\uFF09\u3002"
|
|
2027
|
+
});
|
|
2028
|
+
}
|
|
2029
|
+
resolved.push(`data:${mime};base64,${content.toString("base64")}`);
|
|
2030
|
+
}
|
|
2031
|
+
return resolved;
|
|
2032
|
+
}
|
|
1772
2033
|
function parseGeminiResponseModalities(raw) {
|
|
1773
2034
|
const modalities = raw.split(",").map((value) => value.trim().toUpperCase()).filter(Boolean);
|
|
1774
2035
|
const unique = new Set(modalities);
|
|
@@ -1812,7 +2073,7 @@ function extractGeminiImageItems(response) {
|
|
|
1812
2073
|
}
|
|
1813
2074
|
function registerGen(program) {
|
|
1814
2075
|
const gen = program.command("gen").description("\u56FE\u50CF / \u89C6\u9891\u751F\u6210");
|
|
1815
|
-
gen.command("image").description("\u751F\u6210\u56FE\u50CF\uFF08\u7701\u7565 --model \u65F6\u81EA\u52A8\u9009\u62E9\u5F53\u524D\u53EF\u7528\u9ED8\u8BA4\u6A21\u578B\uFF09").argument("<prompt...>", "\u63D0\u793A\u8BCD").option("-m, --model <model>", "\u56FE\u50CF\u6A21\u578B ID\uFF1B\u7701\u7565\u65F6\u7531 focalapi \u81EA\u52A8\u9009\u62E9").option("--size <size>", "\u5C3A\u5BF8\uFF0C\u5982 1024x1024").option("--aspect-ratio <ratio>", "\u6A21\u578B\u539F\u751F\u753B\u9762\u6BD4\u4F8B\uFF0C\u5982 16:9").option("--resolution <resolution>", "\u6A21\u578B\u539F\u751F\u8F93\u51FA\u6863\u4F4D\uFF0C\u5982 1k\u30012k").option("--seed <n>", "\u6A21\u578B\u968F\u673A\u79CD\u5B50\uFF08\u975E\u8D1F\u6574\u6570\uFF09", (v) => Number.parseInt(v, 10)).option("--quality <quality>", "\u56FE\u50CF\u8D28\u91CF\u6863\u4F4D\uFF08\u4EC5\u652F\u6301\u8BE5\u53C2\u6570\u7684\u6A21\u578B\u751F\u6548\uFF09").option("--background <background>", "\u80CC\u666F\u6A21\u5F0F\uFF08\u4EC5 gpt-image-2 \u652F\u6301 auto/opaque\uFF09").option("--negative-prompt <text>", "\u8D1F\u9762\u63D0\u793A\u8BCD\uFF08\u4EC5\u652F\u6301\u8BE5\u53C2\u6570\u7684\u6A21\u578B\u751F\u6548\uFF09").option("--creativity <level>", "\u63D0\u793A\u8BCD\u6269\u5C55\u5F3A\u5EA6\uFF0C\u5982 raw\u3001low\u3001medium\u3001high").option("--prompt-extend <boolean>", "\u662F\u5426\u6269\u5C55\u63D0\u793A\u8BCD\uFF08\u53EA\u63A5\u53D7 true \u6216 false\uFF09", (v) => parseBooleanOption(v, "prompt-extend")).option("--style-references <json>", "Krea image_style_references JSON \u6570\u7EC4").option("--moodboards <json>", "Krea moodboards JSON \u6570\u7EC4").option("--watermark <boolean>", "\u662F\u5426\u6DFB\u52A0\u6C34\u5370\uFF08\u4EC5\u652F\u6301\u8BE5\u53C2\u6570\u7684\u6A21\u578B\u751F\u6548\uFF09", (v) => parseBooleanOption(v, "watermark")).option("--output-format <format>", "\u8F93\u51FA\u683C\u5F0F\uFF08\u4EC5 Seedream\uFF1Apng \u6216 jpeg\uFF09").option("--optimize-prompt <mode>", "\u63D0\u793A\u8BCD\u4F18\u5316\uFF08\u4EC5 Seedream\uFF1Aauto\u3001enabled \u6216 disabled\uFF09").option("--image <url...>", "\u53C2\u8003\u56FE
|
|
2076
|
+
gen.command("image").description("\u751F\u6210\u56FE\u50CF\uFF08\u7701\u7565 --model \u65F6\u81EA\u52A8\u9009\u62E9\u5F53\u524D\u53EF\u7528\u9ED8\u8BA4\u6A21\u578B\uFF09").argument("<prompt...>", "\u63D0\u793A\u8BCD").option("-m, --model <model>", "\u56FE\u50CF\u6A21\u578B ID\uFF1B\u7701\u7565\u65F6\u7531 focalapi \u81EA\u52A8\u9009\u62E9").option("--size <size>", "\u5C3A\u5BF8\uFF0C\u5982 1024x1024").option("--aspect-ratio <ratio>", "\u6A21\u578B\u539F\u751F\u753B\u9762\u6BD4\u4F8B\uFF0C\u5982 16:9").option("--resolution <resolution>", "\u6A21\u578B\u539F\u751F\u8F93\u51FA\u6863\u4F4D\uFF0C\u5982 1k\u30012k").option("--seed <n>", "\u6A21\u578B\u968F\u673A\u79CD\u5B50\uFF08\u975E\u8D1F\u6574\u6570\uFF09", (v) => Number.parseInt(v, 10)).option("--quality <quality>", "\u56FE\u50CF\u8D28\u91CF\u6863\u4F4D\uFF08\u4EC5\u652F\u6301\u8BE5\u53C2\u6570\u7684\u6A21\u578B\u751F\u6548\uFF09").option("--background <background>", "\u80CC\u666F\u6A21\u5F0F\uFF08\u4EC5 gpt-image-2 \u652F\u6301 auto/opaque\uFF09").option("--negative-prompt <text>", "\u8D1F\u9762\u63D0\u793A\u8BCD\uFF08\u4EC5\u652F\u6301\u8BE5\u53C2\u6570\u7684\u6A21\u578B\u751F\u6548\uFF09").option("--creativity <level>", "\u63D0\u793A\u8BCD\u6269\u5C55\u5F3A\u5EA6\uFF0C\u5982 raw\u3001low\u3001medium\u3001high").option("--prompt-extend <boolean>", "\u662F\u5426\u6269\u5C55\u63D0\u793A\u8BCD\uFF08\u53EA\u63A5\u53D7 true \u6216 false\uFF09", (v) => parseBooleanOption(v, "prompt-extend")).option("--style-references <json>", "Krea image_style_references JSON \u6570\u7EC4").option("--moodboards <json>", "Krea moodboards JSON \u6570\u7EC4").option("--watermark <boolean>", "\u662F\u5426\u6DFB\u52A0\u6C34\u5370\uFF08\u4EC5\u652F\u6301\u8BE5\u53C2\u6570\u7684\u6A21\u578B\u751F\u6548\uFF09", (v) => parseBooleanOption(v, "watermark")).option("--output-format <format>", "\u8F93\u51FA\u683C\u5F0F\uFF08\u4EC5 Seedream\uFF1Apng \u6216 jpeg\uFF09").option("--optimize-prompt <mode>", "\u63D0\u793A\u8BCD\u4F18\u5316\uFF08\u4EC5 Seedream\uFF1Aauto\u3001enabled \u6216 disabled\uFF09").option("--image <url...>", "\u53C2\u8003\u56FE/\u7F16\u8F91\u56FE URL \u6216\u672C\u5730\u8DEF\u5F84\uFF08@C:/path/x.jpg \u81EA\u52A8\u5185\u8054\uFF09\uFF0C\u53EF\u591A\u4E2A").option("--mask <url>", "\u7F16\u8F91 mask URL\uFF08gpt-image-2 \u9700\u8981\u5355\u5F20\u53C2\u8003\u56FE\uFF09").option("--response-format <format>", "\u56FE\u50CF\u54CD\u5E94\u683C\u5F0F\uFF1Aurl \u6216 b64_json").option("--n <count>", "\u5F20\u6570\uFF081\u2013128\uFF09", (v) => Number.parseInt(v, 10), 1).option("--no-wait", "\u63D0\u4EA4\u540E\u7ACB\u5373\u8FD4\u56DE task_id\uFF0C\u4E0D\u7B49\u5F85\u56FE\u50CF\u751F\u6210\u5B8C\u6210").option("--idempotency-key <key>", "\u5E42\u7B49\u952E\uFF088\u2013128 \u4E2A\u53EF\u6253\u5370 ASCII \u5B57\u7B26\uFF09\u3002\u540C\u4E00 key \u7684\u91CD\u590D\u63D0\u4EA4\u62FF\u56DE\u539F\u4EFB\u52A1\u800C\u4E0D\u91CD\u590D\u8BA1\u8D39\uFF1B\u7701\u7565\u65F6\u81EA\u52A8\u751F\u6210\u3002\u91CD\u8BD5\u4E0D\u786E\u5B9A\u7684\u63D0\u4EA4\u65F6\u52A1\u5FC5\u590D\u7528\u539F key").option("-o, --out <dir>", "\u8F93\u51FA\u76EE\u5F55", DEFAULT_OUT_DIR).action(async (promptParts, opts, cmd) => {
|
|
1816
2077
|
const g = cmd.optsWithGlobals();
|
|
1817
2078
|
const auth = resolveAuth(g);
|
|
1818
2079
|
const model = opts.model ?? (await resolveCreativeModel(auth, "image")).model.id;
|
|
@@ -1820,6 +2081,8 @@ function registerGen(program) {
|
|
|
1820
2081
|
const n = clampInt(opts.n, 1, MAX_IMAGE_N, "n");
|
|
1821
2082
|
const styleReferences = opts.styleReferences ? parseJsonArray(opts.styleReferences, "style-references") : void 0;
|
|
1822
2083
|
const moodboards = opts.moodboards ? parseJsonArray(opts.moodboards, "moodboards") : void 0;
|
|
2084
|
+
const referenceImages = await resolveMediaInputs(opts.image ?? []);
|
|
2085
|
+
const maskImage = (await resolveMediaInputs(opts.mask ? [opts.mask] : []))[0];
|
|
1823
2086
|
validateImageGeneration(model, {
|
|
1824
2087
|
n,
|
|
1825
2088
|
size: opts.size,
|
|
@@ -1837,32 +2100,35 @@ function registerGen(program) {
|
|
|
1837
2100
|
styleReferenceCount: styleReferences?.length,
|
|
1838
2101
|
moodboardCount: moodboards?.length,
|
|
1839
2102
|
responseFormat: opts.responseFormat,
|
|
1840
|
-
imageCount:
|
|
1841
|
-
hasMask: Boolean(
|
|
2103
|
+
imageCount: referenceImages.length,
|
|
2104
|
+
hasMask: Boolean(maskImage)
|
|
1842
2105
|
});
|
|
1843
2106
|
if (opts.wait === false && opts.responseFormat === "b64_json") {
|
|
1844
2107
|
throw new ApiError("invalid_request", "--response-format b64_json cannot be used with --no-wait; use url");
|
|
1845
2108
|
}
|
|
1846
2109
|
const body = { model, prompt: promptParts.join(" "), n };
|
|
2110
|
+
const constraint = getImageConstraint(model);
|
|
1847
2111
|
if (opts.size) body.size = opts.size;
|
|
1848
2112
|
if (opts.aspectRatio) body.aspect_ratio = opts.aspectRatio;
|
|
1849
|
-
if (opts.resolution) body
|
|
2113
|
+
if (opts.resolution) body[constraint?.resolutionParam ?? "resolution"] = opts.resolution.toLowerCase();
|
|
1850
2114
|
if (opts.seed !== void 0) body.seed = opts.seed;
|
|
1851
2115
|
if (opts.quality) body.quality = opts.quality;
|
|
1852
2116
|
if (opts.background) body.background = opts.background;
|
|
1853
2117
|
if (opts.negativePrompt) body.negative_prompt = opts.negativePrompt;
|
|
1854
|
-
if (opts.creativity)
|
|
2118
|
+
if (opts.creativity) {
|
|
2119
|
+
body.creativity = constraint?.integerCreativity ? Number(opts.creativity) : opts.creativity.toLowerCase();
|
|
2120
|
+
}
|
|
1855
2121
|
if (opts.promptExtend !== void 0) body.prompt_extend = opts.promptExtend;
|
|
1856
2122
|
if (styleReferences) body.image_style_references = styleReferences;
|
|
1857
2123
|
if (moodboards) body.moodboards = moodboards;
|
|
1858
2124
|
if (opts.watermark !== void 0) body.watermark = opts.watermark;
|
|
1859
2125
|
if (opts.outputFormat) body.output_format = opts.outputFormat.toLowerCase();
|
|
1860
2126
|
if (opts.optimizePrompt) body.optimize_prompt_options = { thinking: opts.optimizePrompt.toLowerCase() };
|
|
1861
|
-
if (
|
|
1862
|
-
if (
|
|
2127
|
+
if (referenceImages.length > 0) body.image = referenceImages;
|
|
2128
|
+
if (maskImage) body.mask = maskImage;
|
|
1863
2129
|
if (opts.responseFormat) body.response_format = opts.responseFormat;
|
|
1864
2130
|
const idempotencyKey = opts.wait === false ? resolveIdempotencyKey(opts.idempotencyKey) : void 0;
|
|
1865
|
-
warnDoubleEncodedReferenceURLs("--image",
|
|
2131
|
+
warnDoubleEncodedReferenceURLs("--image", referenceImages);
|
|
1866
2132
|
const res = await withProgress(opts.wait === false ? "\u6B63\u5728\u63D0\u4EA4\u56FE\u50CF\u4EFB\u52A1" : "\u6B63\u5728\u751F\u6210\u56FE\u50CF", () => request({
|
|
1867
2133
|
baseUrl: auth.baseUrl,
|
|
1868
2134
|
path: "/v1/images/generations",
|
|
@@ -1906,18 +2172,19 @@ function registerGen(program) {
|
|
|
1906
2172
|
for (const f of files) info(`\u2713 ${f}`);
|
|
1907
2173
|
}
|
|
1908
2174
|
});
|
|
1909
|
-
gen.command("gemini-image").description("\u4F7F\u7528 Gemini \u539F\u751F generateContent \u63A5\u53E3\u751F\u6210\u56FE\u50CF").argument("<prompt...>", "\u63D0\u793A\u8BCD").requiredOption("-m, --model <model>", "Gemini \u56FE\u50CF\u6A21\u578B ID\uFF1B\u5148\u7528 focalapi models get \u786E\u8BA4").option("--aspect-ratio <ratio>", "\u753B\u9762\u6BD4\u4F8B\uFF0C\u4F8B\u5982 1:1\u300116:9\u3001auto").option("--image-size <size>", "\u8F93\u51FA\u5C3A\u5BF8\uFF0C\u4F8B\u5982 1K\u30012K\u30014K").option("--response-modalities <modalities>", "\u8F93\u51FA\u7C7B\u578B\uFF1AIMAGE \u6216 IMAGE,TEXT\uFF1B\u672A\u4F20\u65F6\u7531\u670D\u52A1\u7AEF\u9ED8\u8BA4 IMAGE,TEXT").option("--config <json>", "\u9644\u52A0 Gemini generationConfig JSON\uFF1B\u547D\u4EE4\u56FA\u5B9A responseFormat.image \u548C\u5355\u5019\u9009").option("-o, --out <dir>", "\u8F93\u51FA\u76EE\u5F55", DEFAULT_OUT_DIR).option("--image <url...>", "Gemini reference image URL or
|
|
2175
|
+
gen.command("gemini-image").description("\u4F7F\u7528 Gemini \u539F\u751F generateContent \u63A5\u53E3\u751F\u6210\u56FE\u50CF").argument("<prompt...>", "\u63D0\u793A\u8BCD").requiredOption("-m, --model <model>", "Gemini \u56FE\u50CF\u6A21\u578B ID\uFF1B\u5148\u7528 focalapi models get \u786E\u8BA4").option("--aspect-ratio <ratio>", "\u753B\u9762\u6BD4\u4F8B\uFF0C\u4F8B\u5982 1:1\u300116:9\u3001auto").option("--image-size <size>", "\u8F93\u51FA\u5C3A\u5BF8\uFF0C\u4F8B\u5982 1K\u30012K\u30014K").option("--response-modalities <modalities>", "\u8F93\u51FA\u7C7B\u578B\uFF1AIMAGE \u6216 IMAGE,TEXT\uFF1B\u672A\u4F20\u65F6\u7531\u670D\u52A1\u7AEF\u9ED8\u8BA4 IMAGE,TEXT").option("--config <json>", "\u9644\u52A0 Gemini generationConfig JSON\uFF1B\u547D\u4EE4\u56FA\u5B9A responseFormat.image \u548C\u5355\u5019\u9009").option("-o, --out <dir>", "\u8F93\u51FA\u76EE\u5F55", DEFAULT_OUT_DIR).option("--image <url...>", "Gemini reference image URL, data URI, or local path (@C:/path/x.jpg); repeatable").option("--system <text>", "Gemini systemInstruction text").option("--seed <n>", "Non-negative Gemini generation seed", (v) => Number.parseInt(v, 10)).option("--thinking-level <level>", "Nano Banana 2 Lite: MINIMAL or HIGH").option("--temperature <n>", "Nano Banana 2 Lite: 0 through 2", (v) => Number.parseFloat(v)).option("--top-p <n>", "Nano Banana 2 Lite: 0 through 1", (v) => Number.parseFloat(v)).action(async (promptParts, opts, cmd) => {
|
|
1910
2176
|
const g = cmd.optsWithGlobals();
|
|
1911
2177
|
const auth = resolveAuth(g);
|
|
2178
|
+
const geminiReferenceImages = await resolveMediaInputs(opts.image ?? []);
|
|
1912
2179
|
validateGeminiImageGeneration(opts.model, {
|
|
2180
|
+
referenceImageCount: geminiReferenceImages.length,
|
|
2181
|
+
nonDataUriReferenceCount: geminiReferenceImages.filter((source) => !source.startsWith("data:")).length,
|
|
1913
2182
|
aspectRatio: opts.aspectRatio,
|
|
1914
2183
|
imageSize: opts.imageSize,
|
|
1915
2184
|
seed: opts.seed,
|
|
1916
2185
|
thinkingLevel: opts.thinkingLevel,
|
|
1917
2186
|
temperature: opts.temperature,
|
|
1918
|
-
topP: opts.topP
|
|
1919
|
-
referenceImageCount: opts.image?.length,
|
|
1920
|
-
nonDataUriReferenceCount: opts.image?.filter((source) => !source.trim().startsWith("data:")).length
|
|
2187
|
+
topP: opts.topP
|
|
1921
2188
|
});
|
|
1922
2189
|
const suppliedConfig = parseGenerationConfig(opts.config);
|
|
1923
2190
|
const suppliedResponseFormat = suppliedConfig.responseFormat;
|
|
@@ -1942,7 +2209,7 @@ function registerGen(program) {
|
|
|
1942
2209
|
path: `/v1beta/models/${encodeURIComponent(opts.model)}:generateContent`,
|
|
1943
2210
|
apiKey: auth.apiKey,
|
|
1944
2211
|
body: {
|
|
1945
|
-
contents: [{ role: "user", parts: [{ text: promptParts.join(" ") }, ...
|
|
2212
|
+
contents: [{ role: "user", parts: [{ text: promptParts.join(" ") }, ...geminiReferenceImages.map(geminiImagePart)] }],
|
|
1946
2213
|
...opts.system ? { systemInstruction: { parts: [{ text: opts.system }] } } : {},
|
|
1947
2214
|
generationConfig
|
|
1948
2215
|
},
|
|
@@ -2007,7 +2274,7 @@ function registerGen(program) {
|
|
|
2007
2274
|
if (res.id) info(`\u4EA4\u4E92 ID\uFF1A${res.id}`);
|
|
2008
2275
|
}
|
|
2009
2276
|
});
|
|
2010
|
-
gen.command("video").description("\u751F\u6210\u89C6\u9891\uFF08\u7701\u7565 --model \u65F6\u81EA\u52A8\u9009\u62E9\u5F53\u524D\u53EF\u7528\u9ED8\u8BA4\u6A21\u578B\uFF09").argument("<prompt...>", "\u63D0\u793A\u8BCD").option("-m, --model <model>", "\u89C6\u9891\u6A21\u578B ID\uFF1B\u7701\u7565\u65F6\u7531 focalapi \u81EA\u52A8\u9009\u62E9").option("--seconds <n>", "\u65F6\u957F\u79D2\u6570\uFF1B--duration \u4E3A\u540C\u4E49\u522B\u540D\u3002\u7CBE\u786E\u8303\u56F4\u8FD0\u884C focalapi models get <model> \u67E5\u770B", (v) => Number.parseInt(v, 10)).option("--duration <n>", "\u65F6\u957F\u79D2\u6570\uFF08--seconds \u7684\u522B\u540D\uFF0C\u4E0E API \u5951\u7EA6\u5B57\u6BB5\u540C\u540D\uFF09", (v) => Number.parseInt(v, 10)).option("--size <size>", "\u5206\u8FA8\u7387\uFF0C\u5982 1280x720").option("--resolution <resolution>", "\u539F\u751F\u8F93\u51FA\u5206\u8FA8\u7387\uFF0C\u5982 480p\u3001720p\u30011080p\u30014k").option("--ratio <ratio>", "\u539F\u751F\u5BBD\u9AD8\u6BD4\uFF0C\u5982 16:9\u30019:16\u3001adaptive").option("--aspect-ratio <ratio>", "\u6A21\u578B\u539F\u751F\u753B\u9762\u6BD4\u4F8B\uFF0C\u5982 16:9\u30019:16\u3001auto").option("--seed <n>", "\u6A21\u578B\u968F\u673A\u79CD\u5B50\uFF08\u975E\u8D1F\u6574\u6570\uFF09", (v) => Number.parseInt(v, 10)).option("--fps <n>", "\u8F93\u51FA\u5E27\u7387\uFF08\u4EC5\u652F\u6301\u8BE5\u53C2\u6570\u7684\u6A21\u578B\u751F\u6548\uFF09", (v) => Number.parseInt(v, 10)).option("--safety-tolerance <n>", "\u5B89\u5168\u5BB9\u5FCD\u5EA6\uFF08\u4EC5\u652F\u6301\u8BE5\u53C2\u6570\u7684\u6A21\u578B\u751F\u6548\uFF09", (v) => Number.parseInt(v, 10)).option("--image <url...>", "\u53C2\u8003\u56FE URL\
|
|
2277
|
+
gen.command("video").description("\u751F\u6210\u89C6\u9891\uFF08\u7701\u7565 --model \u65F6\u81EA\u52A8\u9009\u62E9\u5F53\u524D\u53EF\u7528\u9ED8\u8BA4\u6A21\u578B\uFF09").argument("<prompt...>", "\u63D0\u793A\u8BCD").option("-m, --model <model>", "\u89C6\u9891\u6A21\u578B ID\uFF1B\u7701\u7565\u65F6\u7531 focalapi \u81EA\u52A8\u9009\u62E9").option("--seconds <n>", "\u65F6\u957F\u79D2\u6570\uFF1B--duration \u4E3A\u540C\u4E49\u522B\u540D\u3002\u7CBE\u786E\u8303\u56F4\u8FD0\u884C focalapi models get <model> \u67E5\u770B", (v) => Number.parseInt(v, 10)).option("--duration <n>", "\u65F6\u957F\u79D2\u6570\uFF08--seconds \u7684\u522B\u540D\uFF0C\u4E0E API \u5951\u7EA6\u5B57\u6BB5\u540C\u540D\uFF09", (v) => Number.parseInt(v, 10)).option("--size <size>", "\u5206\u8FA8\u7387\uFF0C\u5982 1280x720").option("--resolution <resolution>", "\u539F\u751F\u8F93\u51FA\u5206\u8FA8\u7387\uFF0C\u5982 480p\u3001720p\u30011080p\u30014k").option("--ratio <ratio>", "\u539F\u751F\u5BBD\u9AD8\u6BD4\uFF0C\u5982 16:9\u30019:16\u3001adaptive").option("--aspect-ratio <ratio>", "\u6A21\u578B\u539F\u751F\u753B\u9762\u6BD4\u4F8B\uFF0C\u5982 16:9\u30019:16\u3001auto").option("--seed <n>", "\u6A21\u578B\u968F\u673A\u79CD\u5B50\uFF08\u975E\u8D1F\u6574\u6570\uFF09", (v) => Number.parseInt(v, 10)).option("--fps <n>", "\u8F93\u51FA\u5E27\u7387\uFF08\u4EC5\u652F\u6301\u8BE5\u53C2\u6570\u7684\u6A21\u578B\u751F\u6548\uFF09", (v) => Number.parseInt(v, 10)).option("--safety-tolerance <n>", "\u5B89\u5168\u5BB9\u5FCD\u5EA6\uFF08\u4EC5\u652F\u6301\u8BE5\u53C2\u6570\u7684\u6A21\u578B\u751F\u6548\uFF09", (v) => Number.parseInt(v, 10)).option("--image <url...>", "\u53C2\u8003\u56FE URL \u6216\u672C\u5730\u8DEF\u5F84\uFF08@C:/path/x.jpg\uFF09\uFF0CGrok \u89C6\u9891\u4E3A reference-to-video \u6A21\u5F0F").option("--first-frame <url>", "\u56FE\u751F\u89C6\u9891\u9996\u5E27\u56FE URL\uFF08image-to-video \u6A21\u5F0F\uFF1B\u4E0E --image \u4E92\u65A5\uFF09").option("--generate-audio <boolean>", "\u662F\u5426\u751F\u6210\u97F3\u9891\uFF08\u53EA\u63A5\u53D7 true \u6216 false\uFF09", (v) => parseBooleanOption(v, "generate-audio")).option("--watermark <boolean>", "\u662F\u5426\u6DFB\u52A0\u6C34\u5370\uFF08\u53EA\u63A5\u53D7 true \u6216 false\uFF09", (v) => parseBooleanOption(v, "watermark")).option("--service-tier <tier>", "\u670D\u52A1\u5C42\u7EA7\uFF08Seedance 2.0 \u9ED8\u8BA4 default\uFF09").option("--priority <n>", "\u4EFB\u52A1\u4F18\u5148\u7EA7\uFF08\u4EC5 Seedance 2.0 \u7CFB\u5217\uFF09", (v) => Number.parseInt(v, 10)).option("--callback-url <url>", "\u4EFB\u52A1\u5B8C\u6210\u56DE\u8C03 URL").option("--return-last-frame <boolean>", "\u662F\u5426\u8FD4\u56DE\u6700\u540E\u4E00\u5E27\uFF08\u53EA\u63A5\u53D7 true \u6216 false\uFF09", (v) => parseBooleanOption(v, "return-last-frame")).option("--execution-expires-after <seconds>", "\u4EFB\u52A1\u8FC7\u671F\u79D2\u6570\uFF083600\u2013259200\uFF09", (v) => Number.parseInt(v, 10)).option("--safety-identifier <identifier>", "Seedance \u5B89\u5168\u6807\u8BC6\u7B26\uFF081\u201364 \u4E2A\u53EF\u6253\u5370 ASCII \u5B57\u7B26\uFF09").option("--no-wait", "\u63D0\u4EA4\u540E\u7ACB\u5373\u8FD4\u56DE task_id\uFF0C\u4E0D\u7B49\u5F85\u5B8C\u6210").option("--idempotency-key <key>", "\u5E42\u7B49\u952E\uFF088\u2013128 \u4E2A\u53EF\u6253\u5370 ASCII \u5B57\u7B26\uFF09\u3002\u540C\u4E00 key \u7684\u91CD\u590D\u63D0\u4EA4\u62FF\u56DE\u539F\u4EFB\u52A1\u800C\u4E0D\u91CD\u590D\u8BA1\u8D39\uFF1B\u7701\u7565\u65F6\u81EA\u52A8\u751F\u6210\u3002\u91CD\u8BD5\u4E0D\u786E\u5B9A\u7684\u63D0\u4EA4\u65F6\u52A1\u5FC5\u590D\u7528\u539F key").option("--poll-interval <ms>", "\u8F6E\u8BE2\u95F4\u9694\u6BEB\u79D2", (v) => Number.parseInt(v, 10), 5e3).option("--timeout <minutes>", "\u6700\u957F\u7B49\u5F85\u5206\u949F", (v) => Number.parseInt(v, 10), 30).option("-o, --out <dir>", "\u8F93\u51FA\u76EE\u5F55", DEFAULT_OUT_DIR).option("--content <json>", "Ark content JSON \u6570\u7EC4\uFF1B\u652F\u6301\u5185\u8054 JSON\u3001@\u6587\u4EF6\u8DEF\u5F84\uFF08\u591A\u56FE base64 \u5FC5\u987B\u8D70\u6587\u4EF6\uFF09\u6216 - \uFF08stdin\uFF09\uFF1B\u8986\u76D6 prompt/image \u95E8\u9762\u5B57\u6BB5").action(
|
|
2011
2278
|
async (promptParts, opts, cmd) => {
|
|
2012
2279
|
const g = cmd.optsWithGlobals();
|
|
2013
2280
|
const auth = resolveAuth(g);
|
|
@@ -2024,8 +2291,10 @@ function registerGen(program) {
|
|
|
2024
2291
|
body.duration = secondsValue;
|
|
2025
2292
|
}
|
|
2026
2293
|
if (opts.size) body.size = opts.size;
|
|
2027
|
-
|
|
2028
|
-
|
|
2294
|
+
const videoReferenceImages = await resolveMediaInputs(opts.image ?? []);
|
|
2295
|
+
const firstFrameImage = (await resolveMediaInputs(opts.firstFrame ? [opts.firstFrame] : []))[0];
|
|
2296
|
+
if (videoReferenceImages.length > 0) body.images = videoReferenceImages;
|
|
2297
|
+
if (firstFrameImage) body.image = firstFrameImage;
|
|
2029
2298
|
if (opts.resolution) metadata.resolution = opts.resolution.toLowerCase();
|
|
2030
2299
|
if (opts.ratio) metadata.ratio = opts.ratio;
|
|
2031
2300
|
if (opts.aspectRatio) metadata.ratio = opts.aspectRatio;
|
|
@@ -2040,8 +2309,9 @@ function registerGen(program) {
|
|
|
2040
2309
|
if (opts.returnLastFrame !== void 0) metadata.return_last_frame = opts.returnLastFrame;
|
|
2041
2310
|
if (opts.executionExpiresAfter !== void 0) metadata.execution_expires_after = opts.executionExpiresAfter;
|
|
2042
2311
|
if (opts.safetyIdentifier) metadata.safety_identifier = opts.safetyIdentifier;
|
|
2043
|
-
if (opts.content) metadata.content = parseJsonArray(opts.content, "content");
|
|
2312
|
+
if (opts.content) metadata.content = parseJsonArray(await readContentArgument(opts.content), "content");
|
|
2044
2313
|
validateVideoGeneration(model, {
|
|
2314
|
+
promptRunes: [...promptParts.join(" ")].length,
|
|
2045
2315
|
seconds,
|
|
2046
2316
|
resolution: opts.resolution,
|
|
2047
2317
|
ratio: opts.ratio,
|
|
@@ -2053,14 +2323,14 @@ function registerGen(program) {
|
|
|
2053
2323
|
priority: opts.priority,
|
|
2054
2324
|
executionExpiresAfter: opts.executionExpiresAfter,
|
|
2055
2325
|
safetyIdentifier: opts.safetyIdentifier,
|
|
2056
|
-
imageCount:
|
|
2057
|
-
firstFrameCount:
|
|
2326
|
+
imageCount: videoReferenceImages.length,
|
|
2327
|
+
firstFrameCount: firstFrameImage ? 1 : 0,
|
|
2058
2328
|
generateAudio: opts.generateAudio,
|
|
2059
2329
|
watermark: opts.watermark
|
|
2060
2330
|
});
|
|
2061
2331
|
if (Object.keys(metadata).length > 0) body.metadata = metadata;
|
|
2062
2332
|
const idempotencyKey = resolveIdempotencyKey(opts.idempotencyKey);
|
|
2063
|
-
warnDoubleEncodedReferenceURLs("--image/--first-frame", [...
|
|
2333
|
+
warnDoubleEncodedReferenceURLs("--image/--first-frame", [...videoReferenceImages, firstFrameImage]);
|
|
2064
2334
|
const created = await withProgress("\u6B63\u5728\u63D0\u4EA4\u89C6\u9891\u4EFB\u52A1", () => request({
|
|
2065
2335
|
baseUrl: auth.baseUrl,
|
|
2066
2336
|
path: "/v1/video/generations",
|
|
@@ -2144,7 +2414,7 @@ function registerTask(program) {
|
|
|
2144
2414
|
const elapsed = taskAgeSeconds(info_.createdAt);
|
|
2145
2415
|
let file;
|
|
2146
2416
|
if (opts.download && info_.status === "success") {
|
|
2147
|
-
file = await
|
|
2417
|
+
file = (await downloadTaskArtifact(auth.baseUrl, auth.apiKey, taskId, opts.out)).file;
|
|
2148
2418
|
}
|
|
2149
2419
|
if (g.json) {
|
|
2150
2420
|
printJson({
|
|
@@ -2212,14 +2482,14 @@ function registerTask(program) {
|
|
|
2212
2482
|
info(`\u2713 \u4EFB\u52A1 ${taskId} \u5DF2\u53D6\u6D88`);
|
|
2213
2483
|
}
|
|
2214
2484
|
});
|
|
2215
|
-
task.command("download").description("\u4E0B\u8F7D\u4EFB\u52A1\u4EA7\u7269\uFF08\u7ECF focalapi \u5185\u5BB9\u4EE3\u7406\
|
|
2485
|
+
task.command("download").description("\u4E0B\u8F7D\u4EFB\u52A1\u4EA7\u7269\uFF08\u9ED8\u8BA4\u7ECF focalapi \u5185\u5BB9\u4EE3\u7406\uFF1B--direct \u4F18\u5148\u76F4\u8FDE\u4EA7\u7269 URL\uFF0C\u5931\u8D25\u81EA\u52A8\u56DE\u9000\u4EE3\u7406\uFF09").argument("<task_id>", "\u4EFB\u52A1 ID").option("-o, --out <dir>", "\u8F93\u51FA\u76EE\u5F55", "focalapi-out").option("--direct", "\u4F18\u5148\u76F4\u8FDE\u4EFB\u52A1\u7684\u4EA7\u7269 URL \u4E0B\u8F7D\uFF08\u8DE8\u5883\u94FE\u8DEF\u4E0B\u901A\u5E38\u663E\u8457\u5FEB\u4E8E\u4EE3\u7406\uFF09\uFF0C\u5931\u8D25\u81EA\u52A8\u56DE\u9000").action(async (taskId, opts, cmd) => {
|
|
2216
2486
|
const g = cmd.optsWithGlobals();
|
|
2217
2487
|
const auth = resolveAuth(g);
|
|
2218
|
-
const filePath = await
|
|
2488
|
+
const { file: filePath, source } = await downloadTaskArtifact(auth.baseUrl, auth.apiKey, taskId, opts.out, { direct: opts.direct });
|
|
2219
2489
|
if (g.json) {
|
|
2220
|
-
printJson({ task_id: taskId, file: filePath, files: [filePath] });
|
|
2490
|
+
printJson({ task_id: taskId, file: filePath, files: [filePath], source });
|
|
2221
2491
|
} else {
|
|
2222
|
-
info(`\u2713 ${filePath}`);
|
|
2492
|
+
info(`\u2713 ${filePath}${source === "upstream" ? "\uFF08\u76F4\u8FDE\u4EA7\u7269 URL\uFF09" : ""}`);
|
|
2223
2493
|
}
|
|
2224
2494
|
});
|
|
2225
2495
|
}
|
package/package.json
CHANGED
|
@@ -31,7 +31,7 @@ focalapi gen image "<prompt>" -m <model-id> [contract-supported options] -o ./fo
|
|
|
31
31
|
focalapi gen video "<prompt>" -m <model-id> [contract-supported options] --no-wait -o ./focalapi-out --json
|
|
32
32
|
```
|
|
33
33
|
|
|
34
|
-
- Use `--image <url...>` for image editing and reference images. Pass `--mask` only when the contract lists it.
|
|
34
|
+
- Use `--image <url...>` for image editing and reference images. Local files work directly with the `@` prefix (`--image @C:/path/ref.jpg`, inlined as a data URI; per-file ≤8MB, per-command ≤12MB — larger sets must be hosted as URLs first). Pass `--mask` only when the contract lists it.
|
|
35
35
|
- Use `--negative-prompt`, `--creativity`, `--prompt-extend`, `--style-references`, and `--moodboards` only when the image contract lists the corresponding field.
|
|
36
36
|
- For video inputs, `--image <url...>` means reference images (Grok 1.5 reference-to-video, capped at 720p and 7 images) and `--first-frame <url>` means image-to-video from a single starting frame. The two flags are mutually exclusive and both are validated against the live contract before submission.
|
|
37
37
|
- Pass duration, resolution, aspect ratio, and audio options only as allowed by `supported_params`.
|