focalapi-cli 0.3.1 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +16 -0
- package/dist/cli.js +293 -38
- package/package.json +1 -1
- package/skills/focalapi/SKILL.md +3 -3
- package/skills/focalapi-gen/SKILL.md +1 -1
- package/skills/focalapi-task/SKILL.md +7 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.4.1 - 2026-08-18
|
|
4
|
+
|
|
5
|
+
- 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.
|
|
6
|
+
- 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.
|
|
7
|
+
- Added `--content @file.json` and `--content -` (stdin) for gen video, bypassing the Windows ~32KB argv limit for multi-image content arrays; capped at 64MB.
|
|
8
|
+
- 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.
|
|
9
|
+
- 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.
|
|
10
|
+
|
|
11
|
+
## 0.4.0 - 2026-08-18
|
|
12
|
+
|
|
13
|
+
- 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).
|
|
14
|
+
- Added `task list [--status] [--action] [--limit] [--offset]` backed by `GET /v1/tasks`, so a caller that lost a submission output can reconcile recent tasks instead of resubmitting and double-charging.
|
|
15
|
+
- Added `--idempotency-key` to `gen video` and async `gen image`: same-key retries replay the original task server-side without a second charge. Keys are auto-generated when omitted, breadcrumbed on stderr (`idempotency_key=...`), and the server's `idempotent_replay` marker is surfaced in both JSON output and stderr.
|
|
16
|
+
- Aligned `task download --json` output with `gen image` by exposing `files[]` alongside the legacy `file`.
|
|
17
|
+
- Added a stderr warning for double-encoded reference URLs (`%25XX`) before submission — the top cause of 403 `invalid_reference_url` from presigned URLs.
|
|
18
|
+
|
|
3
19
|
## 0.3.1 - 2026-08-18
|
|
4
20
|
|
|
5
21
|
- Added `--duration` as an alias of `--seconds` on `gen video` so the API contract field name works directly; conflicting values are rejected locally.
|
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.4.1" : "0.0.0-dev";
|
|
180
180
|
|
|
181
181
|
// src/commands/auth.ts
|
|
182
182
|
import { createInterface } from "readline/promises";
|
|
@@ -912,10 +912,11 @@ 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
|
+
import { randomUUID } from "crypto";
|
|
919
920
|
|
|
920
921
|
// src/lib/model-capabilities.ts
|
|
921
922
|
var GEMINI_IMAGE_MAX_SEED = 9007199254740991;
|
|
@@ -1157,7 +1158,8 @@ var VIDEO_CONSTRAINTS = {
|
|
|
1157
1158
|
disallowReferences: true,
|
|
1158
1159
|
maxFirstFrameImages: 1,
|
|
1159
1160
|
supportsGenerateAudio: false,
|
|
1160
|
-
supportsWatermark: false
|
|
1161
|
+
supportsWatermark: false,
|
|
1162
|
+
maxPromptRunes: 4096
|
|
1161
1163
|
},
|
|
1162
1164
|
"grok-imagine-video-1.5": {
|
|
1163
1165
|
resolutions: ["480p", "720p", "1080p"],
|
|
@@ -1168,7 +1170,8 @@ var VIDEO_CONSTRAINTS = {
|
|
|
1168
1170
|
referenceResolutions: ["480p", "720p"],
|
|
1169
1171
|
maxFirstFrameImages: 1,
|
|
1170
1172
|
supportsGenerateAudio: false,
|
|
1171
|
-
supportsWatermark: false
|
|
1173
|
+
supportsWatermark: false,
|
|
1174
|
+
maxPromptRunes: 4096
|
|
1172
1175
|
},
|
|
1173
1176
|
"kling-3.0": {
|
|
1174
1177
|
resolutions: ["720p", "1080p", "4k"],
|
|
@@ -1394,6 +1397,12 @@ function validateGeminiImageGeneration(model, input) {
|
|
|
1394
1397
|
function validateVideoGeneration(model, input) {
|
|
1395
1398
|
const constraint = VIDEO_CONSTRAINTS[model.trim()];
|
|
1396
1399
|
if (!constraint) return;
|
|
1400
|
+
if (constraint.maxPromptRunes !== void 0 && input.promptRunes !== void 0) {
|
|
1401
|
+
const referenceCount = input.imageCount ?? input.firstFrameCount ?? 0;
|
|
1402
|
+
if (input.promptRunes + 500 * referenceCount > constraint.maxPromptRunes) {
|
|
1403
|
+
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)`);
|
|
1404
|
+
}
|
|
1405
|
+
}
|
|
1397
1406
|
if ((input.imageCount ?? 0) > 0 && (input.firstFrameCount ?? 0) > 0) {
|
|
1398
1407
|
throw new ApiError("invalid_request", `${model}: --image and --first-frame are mutually exclusive (reference-to-video vs image-to-video)`);
|
|
1399
1408
|
}
|
|
@@ -1526,6 +1535,19 @@ function extractProgress(body) {
|
|
|
1526
1535
|
}
|
|
1527
1536
|
return void 0;
|
|
1528
1537
|
}
|
|
1538
|
+
function extractCreatedAt(raw) {
|
|
1539
|
+
if (!raw || typeof raw !== "object") return void 0;
|
|
1540
|
+
const obj = raw;
|
|
1541
|
+
const candidates = [obj.created_at, obj.data?.created_at];
|
|
1542
|
+
for (const candidate of candidates) {
|
|
1543
|
+
if (typeof candidate === "number" && candidate > 0) return candidate;
|
|
1544
|
+
if (typeof candidate === "string") {
|
|
1545
|
+
const parsed = Number.parseInt(candidate, 10);
|
|
1546
|
+
if (Number.isFinite(parsed) && parsed > 0) return parsed;
|
|
1547
|
+
}
|
|
1548
|
+
}
|
|
1549
|
+
return void 0;
|
|
1550
|
+
}
|
|
1529
1551
|
async function cancelTask(baseUrl, apiKey, taskId) {
|
|
1530
1552
|
try {
|
|
1531
1553
|
await request({
|
|
@@ -1588,9 +1610,24 @@ async function fetchTask(baseUrl, apiKey, taskId) {
|
|
|
1588
1610
|
status: normalizeTaskStatus(rawStatus),
|
|
1589
1611
|
rawStatus,
|
|
1590
1612
|
progress: extractProgress(raw),
|
|
1613
|
+
createdAt: extractCreatedAt(raw),
|
|
1591
1614
|
raw
|
|
1592
1615
|
};
|
|
1593
1616
|
}
|
|
1617
|
+
async function listTasks(baseUrl, apiKey, opts) {
|
|
1618
|
+
const raw = await request({
|
|
1619
|
+
baseUrl,
|
|
1620
|
+
path: "/v1/tasks",
|
|
1621
|
+
apiKey,
|
|
1622
|
+
query: {
|
|
1623
|
+
status: opts?.status,
|
|
1624
|
+
action: opts?.action,
|
|
1625
|
+
limit: opts?.limit,
|
|
1626
|
+
offset: opts?.offset
|
|
1627
|
+
}
|
|
1628
|
+
});
|
|
1629
|
+
return raw.data ?? [];
|
|
1630
|
+
}
|
|
1594
1631
|
async function pollTask(baseUrl, apiKey, taskId, opts) {
|
|
1595
1632
|
const intervalMs = opts?.intervalMs ?? 5e3;
|
|
1596
1633
|
const timeoutMs = opts?.timeoutMs ?? 30 * 6e4;
|
|
@@ -1615,7 +1652,7 @@ async function pollTask(baseUrl, apiKey, taskId, opts) {
|
|
|
1615
1652
|
}
|
|
1616
1653
|
if (Date.now() > deadline) {
|
|
1617
1654
|
throw new ApiError("timeout", `\u4EFB\u52A1 ${taskId} \u7B49\u5F85\u8D85\u65F6\uFF08${Math.round(timeoutMs / 6e4)} \u5206\u949F\uFF09`, {
|
|
1618
|
-
hint: `\u53EF\
|
|
1655
|
+
hint: `\u53EF\u8FD0\u884C focalapi task status ${taskId} --wait \u7EE7\u7EED\u7B49\u5F85\uFF0C\u6216 focalapi task status ${taskId} \u67E5\u770B\u5F53\u524D\u72B6\u6001\u3002`
|
|
1619
1656
|
});
|
|
1620
1657
|
}
|
|
1621
1658
|
await new Promise((r) => setTimeout(r, intervalMs));
|
|
@@ -1630,6 +1667,46 @@ var EXT_BY_CONTENT_TYPE = {
|
|
|
1630
1667
|
"audio/mpeg": ".mp3",
|
|
1631
1668
|
"audio/wav": ".wav"
|
|
1632
1669
|
};
|
|
1670
|
+
function extractTaskArtifactURL(raw) {
|
|
1671
|
+
if (!raw || typeof raw !== "object") return void 0;
|
|
1672
|
+
const obj = raw;
|
|
1673
|
+
const data = obj.data;
|
|
1674
|
+
const candidates = [obj.url, data?.url, data?.data?.url, data?.data?.video_url];
|
|
1675
|
+
for (const candidate of candidates) {
|
|
1676
|
+
if (typeof candidate === "string" && /^https?:\/\//.test(candidate)) return candidate;
|
|
1677
|
+
}
|
|
1678
|
+
return void 0;
|
|
1679
|
+
}
|
|
1680
|
+
async function downloadTaskArtifact(baseUrl, apiKey, taskId, outDir, opts) {
|
|
1681
|
+
let upstreamURL;
|
|
1682
|
+
if (opts?.direct) {
|
|
1683
|
+
const info2 = await fetchTask(baseUrl, apiKey, taskId);
|
|
1684
|
+
upstreamURL = extractTaskArtifactURL(info2.raw);
|
|
1685
|
+
if (upstreamURL) {
|
|
1686
|
+
try {
|
|
1687
|
+
const res = await fetch(upstreamURL, {
|
|
1688
|
+
signal: AbortSignal.timeout(6e5)
|
|
1689
|
+
});
|
|
1690
|
+
if (res.ok && res.body) {
|
|
1691
|
+
const contentType = res.headers.get("content-type")?.split(";")[0]?.trim() ?? "";
|
|
1692
|
+
const ext = EXT_BY_CONTENT_TYPE[contentType] ?? extFromURL(upstreamURL) ?? ".bin";
|
|
1693
|
+
const dir = resolve(outDir);
|
|
1694
|
+
await mkdir(dir, { recursive: true });
|
|
1695
|
+
const filePath = join2(dir, `${opts?.filenameBase ?? `task-${taskId}`}${ext}`);
|
|
1696
|
+
await pipeline(Readable.fromWeb(res.body), createWriteStream(filePath));
|
|
1697
|
+
return { file: filePath, source: "upstream" };
|
|
1698
|
+
}
|
|
1699
|
+
} catch {
|
|
1700
|
+
}
|
|
1701
|
+
}
|
|
1702
|
+
}
|
|
1703
|
+
const file = await downloadTaskContent(baseUrl, apiKey, taskId, outDir, opts?.filenameBase);
|
|
1704
|
+
return { file, source: "proxy" };
|
|
1705
|
+
}
|
|
1706
|
+
function extFromURL(url) {
|
|
1707
|
+
const match = /\.(mp4|webm|png|jpe?g|webp|mp3|wav)(?:[?#]|$)/i.exec(url);
|
|
1708
|
+
return match ? match[1].toLowerCase() === "jpeg" ? ".jpg" : `.${match[1].toLowerCase()}` : void 0;
|
|
1709
|
+
}
|
|
1633
1710
|
async function downloadTaskContent(baseUrl, apiKey, taskId, outDir, filenameBase) {
|
|
1634
1711
|
const res = await rawRequest({
|
|
1635
1712
|
baseUrl,
|
|
@@ -1653,6 +1730,20 @@ async function downloadTaskContent(baseUrl, apiKey, taskId, outDir, filenameBase
|
|
|
1653
1730
|
var MAX_IMAGE_N = 128;
|
|
1654
1731
|
var MAX_TASK_DURATION_SECONDS = 3600;
|
|
1655
1732
|
var DEFAULT_OUT_DIR = "focalapi-out";
|
|
1733
|
+
function resolveIdempotencyKey(provided) {
|
|
1734
|
+
const key = provided?.trim() || randomUUID();
|
|
1735
|
+
if (!/^[\x21-\x7e]{8,128}$/.test(key)) {
|
|
1736
|
+
throw new ApiError("invalid_request", "--idempotency-key \u5FC5\u987B\u662F 8\u2013128 \u4E2A\u53EF\u6253\u5370 ASCII \u5B57\u7B26\uFF08\u6536\u5230\uFF1A" + provided + "\uFF09");
|
|
1737
|
+
}
|
|
1738
|
+
return key;
|
|
1739
|
+
}
|
|
1740
|
+
function warnDoubleEncodedReferenceURLs(label, urls) {
|
|
1741
|
+
for (const url of urls) {
|
|
1742
|
+
if (url && /%25[0-9a-f]{2}/i.test(url)) {
|
|
1743
|
+
info(`\u8B66\u544A\uFF1A${label} \u7684 URL \u7591\u4F3C\u88AB\u4E8C\u6B21\u7F16\u7801\uFF08\u5305\u542B %25XX\uFF09\uFF1A${url.slice(0, 100)} \u2014\u2014 \u9884\u7B7E\u540D URL \u5FC5\u987B\u539F\u6837\u4F20\u9012\uFF0C\u5426\u5219\u4E0A\u6E38\u4F1A\u8FD4\u56DE 403 invalid_reference_url`);
|
|
1744
|
+
}
|
|
1745
|
+
}
|
|
1746
|
+
}
|
|
1656
1747
|
function clampInt(value, min, max, name) {
|
|
1657
1748
|
if (!Number.isInteger(value) || value < min || value > max) {
|
|
1658
1749
|
throw new ApiError("invalid_request", `${name} \u5FC5\u987B\u662F ${min}\u2013${max} \u7684\u6574\u6570\uFF08\u6536\u5230\uFF1A${value}\uFF09`);
|
|
@@ -1726,6 +1817,93 @@ function parseJsonArray(raw, option) {
|
|
|
1726
1817
|
throw new ApiError("invalid_request", `--${option} must be a JSON array`);
|
|
1727
1818
|
}
|
|
1728
1819
|
}
|
|
1820
|
+
var MAX_CONTENT_FILE_BYTES = 64 * 1024 * 1024;
|
|
1821
|
+
async function readContentArgument(raw) {
|
|
1822
|
+
if (raw === "-") {
|
|
1823
|
+
const chunks = [];
|
|
1824
|
+
for await (const chunk of process.stdin) {
|
|
1825
|
+
chunks.push(Buffer.from(chunk));
|
|
1826
|
+
if (Buffer.concat(chunks).byteLength > MAX_CONTENT_FILE_BYTES) {
|
|
1827
|
+
throw new ApiError("invalid_request", "--content stdin \u8F93\u5165\u8D85\u8FC7 64MB \u4E0A\u9650");
|
|
1828
|
+
}
|
|
1829
|
+
}
|
|
1830
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
1831
|
+
}
|
|
1832
|
+
if (raw.startsWith("@")) {
|
|
1833
|
+
const path = raw.slice(1);
|
|
1834
|
+
let content;
|
|
1835
|
+
try {
|
|
1836
|
+
content = await readFile(path);
|
|
1837
|
+
} catch (err) {
|
|
1838
|
+
throw new ApiError("invalid_request", `\u8BFB\u53D6 --content \u6587\u4EF6\u5931\u8D25 @${path}\uFF1A${err.message}`);
|
|
1839
|
+
}
|
|
1840
|
+
if (content.byteLength > MAX_CONTENT_FILE_BYTES) {
|
|
1841
|
+
throw new ApiError("invalid_request", `--content \u6587\u4EF6 ${(content.byteLength / 1048576).toFixed(1)}MB \u8D85\u8FC7 64MB \u4E0A\u9650`);
|
|
1842
|
+
}
|
|
1843
|
+
return content.toString("utf8");
|
|
1844
|
+
}
|
|
1845
|
+
return raw;
|
|
1846
|
+
}
|
|
1847
|
+
var MAX_LOCAL_MEDIA_BYTES = 8 * 1024 * 1024;
|
|
1848
|
+
var MAX_TOTAL_LOCAL_MEDIA_BYTES = 12 * 1024 * 1024;
|
|
1849
|
+
var MEDIA_MIME_BY_EXT = {
|
|
1850
|
+
".jpg": "image/jpeg",
|
|
1851
|
+
".jpeg": "image/jpeg",
|
|
1852
|
+
".png": "image/png",
|
|
1853
|
+
".webp": "image/webp",
|
|
1854
|
+
".gif": "image/gif",
|
|
1855
|
+
".mp4": "video/mp4",
|
|
1856
|
+
".webm": "video/webm",
|
|
1857
|
+
".mov": "video/quicktime",
|
|
1858
|
+
".mp3": "audio/mpeg",
|
|
1859
|
+
".wav": "audio/wav"
|
|
1860
|
+
};
|
|
1861
|
+
async function resolveMediaInputs(sources) {
|
|
1862
|
+
let totalLocalBytes = 0;
|
|
1863
|
+
const resolved = [];
|
|
1864
|
+
for (const source of sources) {
|
|
1865
|
+
if (!source) continue;
|
|
1866
|
+
if (!source.startsWith("@")) {
|
|
1867
|
+
const lowered = source.toLowerCase();
|
|
1868
|
+
if (!source.startsWith("data:") && !lowered.startsWith("http://") && !lowered.startsWith("https://")) {
|
|
1869
|
+
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`, {
|
|
1870
|
+
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"
|
|
1871
|
+
});
|
|
1872
|
+
}
|
|
1873
|
+
resolved.push(source);
|
|
1874
|
+
continue;
|
|
1875
|
+
}
|
|
1876
|
+
const path = source.slice(1);
|
|
1877
|
+
let content;
|
|
1878
|
+
try {
|
|
1879
|
+
content = await readFile(path);
|
|
1880
|
+
} catch (err) {
|
|
1881
|
+
throw new ApiError("invalid_request", `\u8BFB\u53D6\u672C\u5730\u5A92\u4F53\u5931\u8D25 @${path}\uFF1A${err.message}`, {
|
|
1882
|
+
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"
|
|
1883
|
+
});
|
|
1884
|
+
}
|
|
1885
|
+
const ext = extname2(path).toLowerCase();
|
|
1886
|
+
const mime = MEDIA_MIME_BY_EXT[ext];
|
|
1887
|
+
if (!mime) {
|
|
1888
|
+
throw new ApiError("invalid_request", `\u4E0D\u652F\u6301\u7684\u672C\u5730\u5A92\u4F53\u7C7B\u578B ${ext}\uFF08@${path}\uFF09`, {
|
|
1889
|
+
hint: `\u652F\u6301\uFF1A${Object.keys(MEDIA_MIME_BY_EXT).join(" ")}`
|
|
1890
|
+
});
|
|
1891
|
+
}
|
|
1892
|
+
if (content.byteLength > MAX_LOCAL_MEDIA_BYTES) {
|
|
1893
|
+
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`, {
|
|
1894
|
+
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"
|
|
1895
|
+
});
|
|
1896
|
+
}
|
|
1897
|
+
totalLocalBytes += content.byteLength;
|
|
1898
|
+
if (totalLocalBytes > MAX_TOTAL_LOCAL_MEDIA_BYTES) {
|
|
1899
|
+
throw new ApiError("invalid_request", `\u672C\u5730\u5A92\u4F53\u603B\u91CF\u8D85\u8FC7 ${MAX_TOTAL_LOCAL_MEDIA_BYTES / 1048576}MB \u4E0A\u9650`, {
|
|
1900
|
+
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"
|
|
1901
|
+
});
|
|
1902
|
+
}
|
|
1903
|
+
resolved.push(`data:${mime};base64,${content.toString("base64")}`);
|
|
1904
|
+
}
|
|
1905
|
+
return resolved;
|
|
1906
|
+
}
|
|
1729
1907
|
function parseGeminiResponseModalities(raw) {
|
|
1730
1908
|
const modalities = raw.split(",").map((value) => value.trim().toUpperCase()).filter(Boolean);
|
|
1731
1909
|
const unique = new Set(modalities);
|
|
@@ -1769,7 +1947,7 @@ function extractGeminiImageItems(response) {
|
|
|
1769
1947
|
}
|
|
1770
1948
|
function registerGen(program) {
|
|
1771
1949
|
const gen = program.command("gen").description("\u56FE\u50CF / \u89C6\u9891\u751F\u6210");
|
|
1772
|
-
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
|
|
1950
|
+
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) => {
|
|
1773
1951
|
const g = cmd.optsWithGlobals();
|
|
1774
1952
|
const auth = resolveAuth(g);
|
|
1775
1953
|
const model = opts.model ?? (await resolveCreativeModel(auth, "image")).model.id;
|
|
@@ -1777,6 +1955,8 @@ function registerGen(program) {
|
|
|
1777
1955
|
const n = clampInt(opts.n, 1, MAX_IMAGE_N, "n");
|
|
1778
1956
|
const styleReferences = opts.styleReferences ? parseJsonArray(opts.styleReferences, "style-references") : void 0;
|
|
1779
1957
|
const moodboards = opts.moodboards ? parseJsonArray(opts.moodboards, "moodboards") : void 0;
|
|
1958
|
+
const referenceImages = await resolveMediaInputs(opts.image ?? []);
|
|
1959
|
+
const maskImage = (await resolveMediaInputs(opts.mask ? [opts.mask] : []))[0];
|
|
1780
1960
|
validateImageGeneration(model, {
|
|
1781
1961
|
n,
|
|
1782
1962
|
size: opts.size,
|
|
@@ -1794,8 +1974,8 @@ function registerGen(program) {
|
|
|
1794
1974
|
styleReferenceCount: styleReferences?.length,
|
|
1795
1975
|
moodboardCount: moodboards?.length,
|
|
1796
1976
|
responseFormat: opts.responseFormat,
|
|
1797
|
-
imageCount:
|
|
1798
|
-
hasMask: Boolean(
|
|
1977
|
+
imageCount: referenceImages.length,
|
|
1978
|
+
hasMask: Boolean(maskImage)
|
|
1799
1979
|
});
|
|
1800
1980
|
if (opts.wait === false && opts.responseFormat === "b64_json") {
|
|
1801
1981
|
throw new ApiError("invalid_request", "--response-format b64_json cannot be used with --no-wait; use url");
|
|
@@ -1815,15 +1995,20 @@ function registerGen(program) {
|
|
|
1815
1995
|
if (opts.watermark !== void 0) body.watermark = opts.watermark;
|
|
1816
1996
|
if (opts.outputFormat) body.output_format = opts.outputFormat.toLowerCase();
|
|
1817
1997
|
if (opts.optimizePrompt) body.optimize_prompt_options = { thinking: opts.optimizePrompt.toLowerCase() };
|
|
1818
|
-
if (
|
|
1819
|
-
if (
|
|
1998
|
+
if (referenceImages.length > 0) body.image = referenceImages;
|
|
1999
|
+
if (maskImage) body.mask = maskImage;
|
|
1820
2000
|
if (opts.responseFormat) body.response_format = opts.responseFormat;
|
|
2001
|
+
const idempotencyKey = opts.wait === false ? resolveIdempotencyKey(opts.idempotencyKey) : void 0;
|
|
2002
|
+
warnDoubleEncodedReferenceURLs("--image", referenceImages);
|
|
1821
2003
|
const res = await withProgress(opts.wait === false ? "\u6B63\u5728\u63D0\u4EA4\u56FE\u50CF\u4EFB\u52A1" : "\u6B63\u5728\u751F\u6210\u56FE\u50CF", () => request({
|
|
1822
2004
|
baseUrl: auth.baseUrl,
|
|
1823
2005
|
path: "/v1/images/generations",
|
|
1824
2006
|
apiKey: auth.apiKey,
|
|
1825
2007
|
body,
|
|
1826
|
-
headers:
|
|
2008
|
+
headers: {
|
|
2009
|
+
...opts.wait === false ? { Prefer: "respond-async" } : {},
|
|
2010
|
+
...idempotencyKey ? { "Idempotency-Key": idempotencyKey } : {}
|
|
2011
|
+
},
|
|
1827
2012
|
timeoutMs: 6e5
|
|
1828
2013
|
}));
|
|
1829
2014
|
if (opts.wait === false) {
|
|
@@ -1832,8 +2017,9 @@ function registerGen(program) {
|
|
|
1832
2017
|
throw new ApiError("bad_response", "\u5F02\u6B65\u56FE\u50CF\u4EFB\u52A1\u54CD\u5E94\u4E2D\u672A\u627E\u5230 task_id", { body: res });
|
|
1833
2018
|
}
|
|
1834
2019
|
info(`task_id=${taskId}`);
|
|
2020
|
+
if (idempotencyKey) info(`idempotency_key=${idempotencyKey}`);
|
|
1835
2021
|
if (g.json) {
|
|
1836
|
-
printJson({ model, task_id: taskId, status: res.status ?? "queued", submitted: true, next_command: `focalapi task status ${taskId} --json` });
|
|
2022
|
+
printJson({ model, task_id: taskId, status: res.status ?? "queued", submitted: true, ...res.idempotent_replay ? { idempotent_replay: true } : {}, next_command: `focalapi task status ${taskId} --json` });
|
|
1837
2023
|
} else {
|
|
1838
2024
|
process.stdout.write(taskId + "\n");
|
|
1839
2025
|
info(`\u4EFB\u52A1\u5DF2\u63D0\u4EA4\u3002\u67E5\u8BE2\uFF1Afocalapi task status ${taskId}`);
|
|
@@ -1857,18 +2043,19 @@ function registerGen(program) {
|
|
|
1857
2043
|
for (const f of files) info(`\u2713 ${f}`);
|
|
1858
2044
|
}
|
|
1859
2045
|
});
|
|
1860
|
-
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
|
|
2046
|
+
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) => {
|
|
1861
2047
|
const g = cmd.optsWithGlobals();
|
|
1862
2048
|
const auth = resolveAuth(g);
|
|
2049
|
+
const geminiReferenceImages = await resolveMediaInputs(opts.image ?? []);
|
|
1863
2050
|
validateGeminiImageGeneration(opts.model, {
|
|
2051
|
+
referenceImageCount: geminiReferenceImages.length,
|
|
2052
|
+
nonDataUriReferenceCount: geminiReferenceImages.filter((source) => !source.startsWith("data:")).length,
|
|
1864
2053
|
aspectRatio: opts.aspectRatio,
|
|
1865
2054
|
imageSize: opts.imageSize,
|
|
1866
2055
|
seed: opts.seed,
|
|
1867
2056
|
thinkingLevel: opts.thinkingLevel,
|
|
1868
2057
|
temperature: opts.temperature,
|
|
1869
|
-
topP: opts.topP
|
|
1870
|
-
referenceImageCount: opts.image?.length,
|
|
1871
|
-
nonDataUriReferenceCount: opts.image?.filter((source) => !source.trim().startsWith("data:")).length
|
|
2058
|
+
topP: opts.topP
|
|
1872
2059
|
});
|
|
1873
2060
|
const suppliedConfig = parseGenerationConfig(opts.config);
|
|
1874
2061
|
const suppliedResponseFormat = suppliedConfig.responseFormat;
|
|
@@ -1893,7 +2080,7 @@ function registerGen(program) {
|
|
|
1893
2080
|
path: `/v1beta/models/${encodeURIComponent(opts.model)}:generateContent`,
|
|
1894
2081
|
apiKey: auth.apiKey,
|
|
1895
2082
|
body: {
|
|
1896
|
-
contents: [{ role: "user", parts: [{ text: promptParts.join(" ") }, ...
|
|
2083
|
+
contents: [{ role: "user", parts: [{ text: promptParts.join(" ") }, ...geminiReferenceImages.map(geminiImagePart)] }],
|
|
1897
2084
|
...opts.system ? { systemInstruction: { parts: [{ text: opts.system }] } } : {},
|
|
1898
2085
|
generationConfig
|
|
1899
2086
|
},
|
|
@@ -1958,7 +2145,7 @@ function registerGen(program) {
|
|
|
1958
2145
|
if (res.id) info(`\u4EA4\u4E92 ID\uFF1A${res.id}`);
|
|
1959
2146
|
}
|
|
1960
2147
|
});
|
|
1961
|
-
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\
|
|
2148
|
+
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(
|
|
1962
2149
|
async (promptParts, opts, cmd) => {
|
|
1963
2150
|
const g = cmd.optsWithGlobals();
|
|
1964
2151
|
const auth = resolveAuth(g);
|
|
@@ -1975,8 +2162,10 @@ function registerGen(program) {
|
|
|
1975
2162
|
body.duration = secondsValue;
|
|
1976
2163
|
}
|
|
1977
2164
|
if (opts.size) body.size = opts.size;
|
|
1978
|
-
|
|
1979
|
-
|
|
2165
|
+
const videoReferenceImages = await resolveMediaInputs(opts.image ?? []);
|
|
2166
|
+
const firstFrameImage = (await resolveMediaInputs(opts.firstFrame ? [opts.firstFrame] : []))[0];
|
|
2167
|
+
if (videoReferenceImages.length > 0) body.images = videoReferenceImages;
|
|
2168
|
+
if (firstFrameImage) body.image = firstFrameImage;
|
|
1980
2169
|
if (opts.resolution) metadata.resolution = opts.resolution.toLowerCase();
|
|
1981
2170
|
if (opts.ratio) metadata.ratio = opts.ratio;
|
|
1982
2171
|
if (opts.aspectRatio) metadata.ratio = opts.aspectRatio;
|
|
@@ -1991,8 +2180,9 @@ function registerGen(program) {
|
|
|
1991
2180
|
if (opts.returnLastFrame !== void 0) metadata.return_last_frame = opts.returnLastFrame;
|
|
1992
2181
|
if (opts.executionExpiresAfter !== void 0) metadata.execution_expires_after = opts.executionExpiresAfter;
|
|
1993
2182
|
if (opts.safetyIdentifier) metadata.safety_identifier = opts.safetyIdentifier;
|
|
1994
|
-
if (opts.content) metadata.content = parseJsonArray(opts.content, "content");
|
|
2183
|
+
if (opts.content) metadata.content = parseJsonArray(await readContentArgument(opts.content), "content");
|
|
1995
2184
|
validateVideoGeneration(model, {
|
|
2185
|
+
promptRunes: [...promptParts.join(" ")].length,
|
|
1996
2186
|
seconds,
|
|
1997
2187
|
resolution: opts.resolution,
|
|
1998
2188
|
ratio: opts.ratio,
|
|
@@ -2004,17 +2194,20 @@ function registerGen(program) {
|
|
|
2004
2194
|
priority: opts.priority,
|
|
2005
2195
|
executionExpiresAfter: opts.executionExpiresAfter,
|
|
2006
2196
|
safetyIdentifier: opts.safetyIdentifier,
|
|
2007
|
-
imageCount:
|
|
2008
|
-
firstFrameCount:
|
|
2197
|
+
imageCount: videoReferenceImages.length,
|
|
2198
|
+
firstFrameCount: firstFrameImage ? 1 : 0,
|
|
2009
2199
|
generateAudio: opts.generateAudio,
|
|
2010
2200
|
watermark: opts.watermark
|
|
2011
2201
|
});
|
|
2012
2202
|
if (Object.keys(metadata).length > 0) body.metadata = metadata;
|
|
2203
|
+
const idempotencyKey = resolveIdempotencyKey(opts.idempotencyKey);
|
|
2204
|
+
warnDoubleEncodedReferenceURLs("--image/--first-frame", [...videoReferenceImages, firstFrameImage]);
|
|
2013
2205
|
const created = await withProgress("\u6B63\u5728\u63D0\u4EA4\u89C6\u9891\u4EFB\u52A1", () => request({
|
|
2014
2206
|
baseUrl: auth.baseUrl,
|
|
2015
2207
|
path: "/v1/video/generations",
|
|
2016
2208
|
apiKey: auth.apiKey,
|
|
2017
2209
|
body,
|
|
2210
|
+
headers: { "Idempotency-Key": idempotencyKey },
|
|
2018
2211
|
timeoutMs: 12e4
|
|
2019
2212
|
}));
|
|
2020
2213
|
const taskId = extractTaskId(created);
|
|
@@ -2023,8 +2216,10 @@ function registerGen(program) {
|
|
|
2023
2216
|
}
|
|
2024
2217
|
if (opts.wait === false) {
|
|
2025
2218
|
info(`task_id=${taskId}`);
|
|
2219
|
+
info(`idempotency_key=${idempotencyKey}`);
|
|
2220
|
+
if (created.idempotent_replay) info("\u5DF2\u56DE\u653E\u539F\u4EFB\u52A1\uFF08idempotent_replay\uFF0C\u672A\u91CD\u590D\u8BA1\u8D39\uFF09");
|
|
2026
2221
|
if (g.json) {
|
|
2027
|
-
printJson({ model, task_id: taskId, submitted: true, next_command: `focalapi task status ${taskId} --json` });
|
|
2222
|
+
printJson({ model, task_id: taskId, submitted: true, ...created.idempotent_replay ? { idempotent_replay: true } : {}, next_command: `focalapi task status ${taskId} --json` });
|
|
2028
2223
|
} else {
|
|
2029
2224
|
process.stdout.write(taskId + "\n");
|
|
2030
2225
|
info(`\u4EFB\u52A1\u5DF2\u63D0\u4EA4\u3002\u7EED\u53D6\uFF1Afocalapi task status ${taskId} / focalapi task download ${taskId}\uFF1B\u6392\u961F\u4E2D\u53EF\u53D6\u6D88\uFF1Afocalapi task cancel ${taskId}`);
|
|
@@ -2052,42 +2247,102 @@ function registerGen(program) {
|
|
|
2052
2247
|
}
|
|
2053
2248
|
|
|
2054
2249
|
// src/commands/task.ts
|
|
2055
|
-
var TASK_ID_HINT = "task_id \u533A\u5206\u5927\u5C0F\u5199\u4E14\u6DF7\u6709\u5C0F\u5199 l / \u6570\u5B57 1 / \u5927\u5199 I\uFF08O \u4E0E 0 \u540C\u7406\uFF09\uFF0C\u5FC5\u987B\u9010\u5B57\u590D\u5236\u3002\u82E5\u63D0\u4EA4\u65F6\u7684\u8F93\u51FA\u5DF2\u4E22\u5931\uFF0C\u5148\
|
|
2250
|
+
var TASK_ID_HINT = "task_id \u533A\u5206\u5927\u5C0F\u5199\u4E14\u6DF7\u6709\u5C0F\u5199 l / \u6570\u5B57 1 / \u5927\u5199 I\uFF08O \u4E0E 0 \u540C\u7406\uFF09\uFF0C\u5FC5\u987B\u9010\u5B57\u590D\u5236\u3002\u82E5\u63D0\u4EA4\u65F6\u7684\u8F93\u51FA\u5DF2\u4E22\u5931\uFF0C\u5148\u8FD0\u884C focalapi task list \u627E\u56DE\u6700\u8FD1\u7684\u4EFB\u52A1\uFF0C\u4E0D\u8981\u76F2\u76EE\u91CD\u65B0\u63D0\u4EA4\u2014\u2014\u91CD\u590D\u63D0\u4EA4\u4F1A\u91CD\u590D\u6263\u8D39\u3002";
|
|
2056
2251
|
function withTaskIdHint(err) {
|
|
2057
2252
|
if (err instanceof ApiError && err.status === 404) {
|
|
2058
2253
|
return new ApiError(err.code, err.message, { status: err.status, hint: TASK_ID_HINT, body: err.body, upstreamCode: err.upstreamCode, requestId: err.requestId });
|
|
2059
2254
|
}
|
|
2060
2255
|
return err;
|
|
2061
2256
|
}
|
|
2257
|
+
function formatElapsed(seconds) {
|
|
2258
|
+
if (!seconds || seconds < 0) return "-";
|
|
2259
|
+
if (seconds < 90) return `${seconds} \u79D2`;
|
|
2260
|
+
return `${Math.floor(seconds / 60)} \u5206 ${seconds % 60} \u79D2`;
|
|
2261
|
+
}
|
|
2262
|
+
function taskAgeSeconds(createdAt) {
|
|
2263
|
+
if (!createdAt) return void 0;
|
|
2264
|
+
return Math.max(0, Math.floor(Date.now() / 1e3 - createdAt));
|
|
2265
|
+
}
|
|
2062
2266
|
function registerTask(program) {
|
|
2063
2267
|
const task = program.command("task").description("\u4EFB\u52A1\u67E5\u8BE2\u3001\u53D6\u6D88\u4E0E\u4EA7\u7269\u4E0B\u8F7D\uFF08\u89C6\u9891\u7B49\u4EFB\u52A1\u5236\u80FD\u529B\uFF09");
|
|
2064
|
-
task.command("status").description("\u67E5\u8BE2\u4EFB\u52A1\u72B6\u6001").argument("<task_id>", "\u4EFB\u52A1 ID").action(async (taskId,
|
|
2268
|
+
task.command("status").description("\u67E5\u8BE2\u4EFB\u52A1\u72B6\u6001\uFF1B--wait \u5185\u7F6E\u8F6E\u8BE2\u7B49\u5F85\u7EC8\u6001\uFF0C\u65E0\u9700\u81EA\u5199\u8F6E\u8BE2\u811A\u672C").argument("<task_id>", "\u4EFB\u52A1 ID").option("--wait", "\u7B49\u5F85\u4EFB\u52A1\u5230\u8FBE\u7EC8\u6001\uFF08\u6210\u529F / \u5931\u8D25 / \u53D6\u6D88\uFF09\u540E\u518D\u8FD4\u56DE").option("--timeout <minutes>", "--wait \u7684\u6700\u957F\u7B49\u5F85\u5206\u949F", (v) => Number.parseInt(v, 10), 30).option("--poll-interval <ms>", "--wait \u7684\u8F6E\u8BE2\u95F4\u9694\u6BEB\u79D2", (v) => Number.parseInt(v, 10), 5e3).option("--download", "--wait \u6210\u529F\u540E\u81EA\u52A8\u4E0B\u8F7D\u4EA7\u7269\uFF08\u7B49\u4EF7\u4E8E\u63A5\u7740\u6267\u884C task download\uFF09").option("-o, --out <dir>", "--download \u7684\u8F93\u51FA\u76EE\u5F55", "focalapi-out").action(async (taskId, opts, cmd) => {
|
|
2065
2269
|
const g = cmd.optsWithGlobals();
|
|
2066
2270
|
const auth = resolveAuth(g);
|
|
2067
2271
|
try {
|
|
2068
|
-
|
|
2272
|
+
let info_ = await fetchTask(auth.baseUrl, auth.apiKey, taskId);
|
|
2273
|
+
if (opts.wait && info_.status !== "success" && info_.status !== "failed" && info_.status !== "cancelled") {
|
|
2274
|
+
info_ = await pollTask(auth.baseUrl, auth.apiKey, taskId, {
|
|
2275
|
+
intervalMs: opts.pollInterval,
|
|
2276
|
+
timeoutMs: opts.timeout * 6e4,
|
|
2277
|
+
onUpdate: (t) => {
|
|
2278
|
+
if (!g.json) {
|
|
2279
|
+
const elapsed2 = formatElapsed(taskAgeSeconds(t.createdAt));
|
|
2280
|
+
info(` \u72B6\u6001\uFF1A${t.rawStatus || t.status}${t.progress !== void 0 ? `\uFF08${t.progress}%\uFF09` : ""}\uFF0C\u5DF2\u8017\u65F6 ${elapsed2}`);
|
|
2281
|
+
}
|
|
2282
|
+
}
|
|
2283
|
+
});
|
|
2284
|
+
}
|
|
2285
|
+
const elapsed = taskAgeSeconds(info_.createdAt);
|
|
2286
|
+
let file;
|
|
2287
|
+
if (opts.download && info_.status === "success") {
|
|
2288
|
+
file = (await downloadTaskArtifact(auth.baseUrl, auth.apiKey, taskId, opts.out)).file;
|
|
2289
|
+
}
|
|
2069
2290
|
if (g.json) {
|
|
2070
|
-
printJson({
|
|
2291
|
+
printJson({
|
|
2292
|
+
task_id: taskId,
|
|
2293
|
+
status: info_.status,
|
|
2294
|
+
raw_status: info_.rawStatus,
|
|
2295
|
+
progress: info_.progress,
|
|
2296
|
+
...elapsed !== void 0 ? { elapsed_seconds: elapsed } : {},
|
|
2297
|
+
...file ? { file } : {},
|
|
2298
|
+
raw: info_.raw
|
|
2299
|
+
});
|
|
2071
2300
|
} else {
|
|
2072
2301
|
printTable(
|
|
2073
2302
|
["\u5B57\u6BB5", "\u503C"],
|
|
2074
2303
|
[
|
|
2075
2304
|
["\u4EFB\u52A1 ID", taskId],
|
|
2076
2305
|
["\u72B6\u6001", `${info_.status}${info_.rawStatus && info_.rawStatus !== info_.status ? `\uFF08\u4E0A\u6E38\uFF1A${info_.rawStatus}\uFF09` : ""}`],
|
|
2077
|
-
["\u8FDB\u5EA6", info_.progress !== void 0 ? `${info_.progress}%` : "-"]
|
|
2306
|
+
["\u8FDB\u5EA6", info_.progress !== void 0 ? `${info_.progress}%` : "-"],
|
|
2307
|
+
["\u5DF2\u8017\u65F6", formatElapsed(elapsed)]
|
|
2078
2308
|
]
|
|
2079
2309
|
);
|
|
2080
|
-
if (
|
|
2310
|
+
if (file) {
|
|
2311
|
+
info(`\u2713 ${file}`);
|
|
2312
|
+
} else if (info_.status === "success") {
|
|
2081
2313
|
info(`\u4EA7\u7269\u4E0B\u8F7D\uFF1Afocalapi task download ${taskId}`);
|
|
2082
2314
|
}
|
|
2083
2315
|
if (info_.status === "pending" || info_.status === "running") {
|
|
2084
|
-
info(`\
|
|
2316
|
+
info(`\u7B49\u5F85\u5B8C\u6210\uFF1Afocalapi task status ${taskId} --wait\uFF1B\u6392\u961F\u4E2D\u53EF\u53D6\u6D88\uFF1Afocalapi task cancel ${taskId}`);
|
|
2085
2317
|
}
|
|
2086
2318
|
}
|
|
2087
2319
|
} catch (err) {
|
|
2088
2320
|
throw withTaskIdHint(err);
|
|
2089
2321
|
}
|
|
2090
2322
|
});
|
|
2323
|
+
task.command("list").description("\u5217\u51FA\u5F53\u524D Key \u7684\u8FD1\u671F\u4EFB\u52A1\uFF08\u63D0\u4EA4\u8F93\u51FA\u4E22\u5931\u65F6\u7528\u5B83\u627E\u56DE task_id\uFF09").option("--status <status>", "\u6309\u72B6\u6001\u8FC7\u6EE4\uFF1Aqueued\u3001in_progress\u3001completed\u3001failed\u3001cancelled").option("--action <action>", "\u6309\u4EFB\u52A1\u7C7B\u578B\u8FC7\u6EE4\uFF0C\u5982 generate\u3001image_generation").option("--limit <n>", "\u6BCF\u9875\u6570\u91CF\uFF081\u2013100\uFF09", (v) => Number.parseInt(v, 10), 20).option("--offset <n>", "\u504F\u79FB\u91CF", (v) => Number.parseInt(v, 10), 0).action(async (opts, cmd) => {
|
|
2324
|
+
const g = cmd.optsWithGlobals();
|
|
2325
|
+
const auth = resolveAuth(g);
|
|
2326
|
+
const items = await listTasks(auth.baseUrl, auth.apiKey, opts);
|
|
2327
|
+
if (g.json) {
|
|
2328
|
+
printJson({ object: "list", data: items });
|
|
2329
|
+
} else if (items.length === 0) {
|
|
2330
|
+
info("\u5F53\u524D Key \u6682\u65E0\u4EFB\u52A1\u8BB0\u5F55\u3002");
|
|
2331
|
+
} else {
|
|
2332
|
+
printTable(
|
|
2333
|
+
["\u4EFB\u52A1 ID", "\u6A21\u578B", "\u72B6\u6001", "\u8FDB\u5EA6", "\u5DF2\u8017\u65F6", "\u989D\u5EA6"],
|
|
2334
|
+
items.map((item) => [
|
|
2335
|
+
item.task_id,
|
|
2336
|
+
item.model ?? "-",
|
|
2337
|
+
item.status ?? "-",
|
|
2338
|
+
item.progress !== void 0 ? `${item.progress}%` : "-",
|
|
2339
|
+
formatElapsed(item.created_at ? Math.max(0, Math.floor(Date.now() / 1e3 - item.created_at)) : void 0),
|
|
2340
|
+
item.quota !== void 0 ? String(item.quota) : "-"
|
|
2341
|
+
])
|
|
2342
|
+
);
|
|
2343
|
+
info("\u7EED\u53D6\uFF1Afocalapi task status <task_id> --wait");
|
|
2344
|
+
}
|
|
2345
|
+
});
|
|
2091
2346
|
task.command("cancel").description("\u53D6\u6D88\u6392\u961F\u4E2D\u7684\u4EFB\u52A1\uFF08\u8FD0\u884C\u4E2D\u7684\u4EFB\u52A1\u4E0D\u53EF\u53D6\u6D88\uFF1B\u53D6\u6D88\u540E\u8D39\u7528\u81EA\u52A8\u9000\u8FD8\uFF09").argument("<task_id>", "\u4EFB\u52A1 ID").action(async (taskId, _opts, cmd) => {
|
|
2092
2347
|
const g = cmd.optsWithGlobals();
|
|
2093
2348
|
const auth = resolveAuth(g);
|
|
@@ -2098,14 +2353,14 @@ function registerTask(program) {
|
|
|
2098
2353
|
info(`\u2713 \u4EFB\u52A1 ${taskId} \u5DF2\u53D6\u6D88`);
|
|
2099
2354
|
}
|
|
2100
2355
|
});
|
|
2101
|
-
task.command("download").description("\u4E0B\u8F7D\u4EFB\u52A1\u4EA7\u7269\uFF08\u7ECF focalapi \u5185\u5BB9\u4EE3\u7406\
|
|
2356
|
+
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) => {
|
|
2102
2357
|
const g = cmd.optsWithGlobals();
|
|
2103
2358
|
const auth = resolveAuth(g);
|
|
2104
|
-
const filePath = await
|
|
2359
|
+
const { file: filePath, source } = await downloadTaskArtifact(auth.baseUrl, auth.apiKey, taskId, opts.out, { direct: opts.direct });
|
|
2105
2360
|
if (g.json) {
|
|
2106
|
-
printJson({ task_id: taskId, file: filePath });
|
|
2361
|
+
printJson({ task_id: taskId, file: filePath, files: [filePath], source });
|
|
2107
2362
|
} else {
|
|
2108
|
-
info(`\u2713 ${filePath}`);
|
|
2363
|
+
info(`\u2713 ${filePath}${source === "upstream" ? "\uFF08\u76F4\u8FDE\u4EA7\u7269 URL\uFF09" : ""}`);
|
|
2109
2364
|
}
|
|
2110
2365
|
});
|
|
2111
2366
|
}
|
|
@@ -2210,7 +2465,7 @@ function registerUsage(program) {
|
|
|
2210
2465
|
}
|
|
2211
2466
|
|
|
2212
2467
|
// src/commands/connect.ts
|
|
2213
|
-
import { createHash, randomUUID } from "crypto";
|
|
2468
|
+
import { createHash, randomUUID as randomUUID2 } from "crypto";
|
|
2214
2469
|
import {
|
|
2215
2470
|
cpSync,
|
|
2216
2471
|
existsSync as existsSync2,
|
|
@@ -2369,7 +2624,7 @@ function installTo(skillsDir, agents, skills, srcDir) {
|
|
|
2369
2624
|
}
|
|
2370
2625
|
mkdirSync2(skillsDir, { recursive: true });
|
|
2371
2626
|
const oldManifest = readManifest(skillsDir);
|
|
2372
|
-
const transactionRoot = join4(skillsDir, `.focalapi-install-${
|
|
2627
|
+
const transactionRoot = join4(skillsDir, `.focalapi-install-${randomUUID2()}`);
|
|
2373
2628
|
const stageRoot = join4(transactionRoot, "stage");
|
|
2374
2629
|
const backupRoot = join4(transactionRoot, "backup");
|
|
2375
2630
|
mkdirSync2(stageRoot, { recursive: true });
|
package/package.json
CHANGED
package/skills/focalapi/SKILL.md
CHANGED
|
@@ -26,8 +26,8 @@ focalapi gen image "<user prompt>" -o ./focalapi-out --json
|
|
|
26
26
|
focalapi gen video "<user prompt>" --no-wait -o ./focalapi-out --json
|
|
27
27
|
|
|
28
28
|
# Continue an asynchronous video task.
|
|
29
|
-
focalapi task status <task-id> --json
|
|
30
|
-
focalapi task
|
|
29
|
+
focalapi task status <task-id> --wait --json # built-in polling; never write your own poll script
|
|
30
|
+
focalapi task list --json # recover recent task IDs after a lost output
|
|
31
31
|
focalapi task cancel <task-id> --json # queued tasks only; cancelled tasks are refunded
|
|
32
32
|
```
|
|
33
33
|
|
|
@@ -38,7 +38,7 @@ focalapi task cancel <task-id> --json # queued tasks only; cancelled tasks are
|
|
|
38
38
|
| Generate or edit images; create from reference images | `focalapi gen image` | focalapi-gen |
|
|
39
39
|
| Generate video; animate images or reference media | `focalapi gen video` | focalapi-gen |
|
|
40
40
|
| Select, compare, or inspect model parameters | `focalapi models resolve/get/search` | focalapi-models |
|
|
41
|
-
| Inspect progress or failures; cancel queued tasks; download results | `focalapi task status/cancel/download` | focalapi-task |
|
|
41
|
+
| Inspect progress or failures; wait, list, cancel queued tasks; download results | `focalapi task status/wait/list/cancel/download` | focalapi-task |
|
|
42
42
|
| Resolve key, sign-in, or 401 issues | `focalapi auth status/login` | focalapi-auth |
|
|
43
43
|
| Inspect quota, usage, or service failures | `focalapi usage/doctor` | focalapi-usage |
|
|
44
44
|
| Provide text assistance explicitly requested by the user | `focalapi chat` | focalapi-chat |
|
|
@@ -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`.
|
|
@@ -13,8 +13,10 @@ metadata:
|
|
|
13
13
|
After a generation command returns `task_id`, prefer the response's `next_command`:
|
|
14
14
|
|
|
15
15
|
```bash
|
|
16
|
-
focalapi task status <task-id> --json
|
|
17
|
-
focalapi task
|
|
16
|
+
focalapi task status <task-id> --json # one-shot status check
|
|
17
|
+
focalapi task status <task-id> --wait # built-in polling until a terminal state — do not write your own poller
|
|
18
|
+
focalapi task status <task-id> --wait --download -o ./focalapi-out --json
|
|
19
|
+
focalapi task list --status in_progress --json # reconcile recent tasks after losing a submission output
|
|
18
20
|
focalapi task cancel <task-id> --json
|
|
19
21
|
```
|
|
20
22
|
|
|
@@ -24,7 +26,9 @@ focalapi task cancel <task-id> --json
|
|
|
24
26
|
- `cancelled`: the task was stopped; a cancelled queued task is refunded. Do not download or resubmit unless the user asks for a new attempt.
|
|
25
27
|
- `unknown`: preserve the raw response and run `focalapi doctor --json`; never fabricate a success state.
|
|
26
28
|
|
|
27
|
-
Task IDs are case-sensitive and mix `l`/`1`/`I` and `O`/`0`. Copy them exactly from the submission output — stdout JSON or the stderr `task_id=` breadcrumb — instead of retyping.
|
|
29
|
+
Task IDs are case-sensitive and mix `l`/`1`/`I` and `O`/`0`. Copy them exactly from the submission output — stdout JSON or the stderr `task_id=` breadcrumb — instead of retyping. If a submission output is lost entirely, run `task list` to recover recent task IDs before considering any resubmission.
|
|
30
|
+
|
|
31
|
+
Generation commands print the submission's `idempotency_key=` on stderr. When retrying a submission whose outcome is uncertain (timeout, crash, lost output), reuse that same key via `--idempotency-key` so the retry replays the original task instead of double-charging; a fresh key means a deliberate new generation.
|
|
28
32
|
|
|
29
33
|
`task cancel` only works while a task is still queued (`pending`). A 409 `task_already_running` means generation already started and cannot be stopped — keep tracking with `task status`. Cancellation failures return explicit codes (`task_already_finished`, `task_cancel_failed`); follow `error.hint` instead of retrying blindly.
|
|
30
34
|
|