@flatkey-ai/cli 0.1.17 → 0.1.19
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/package.json +1 -1
- package/src/api.js +27 -1
- package/src/artifacts.js +6 -1
- package/src/cli.js +162 -5
- package/src/help.js +17 -6
package/package.json
CHANGED
package/src/api.js
CHANGED
|
@@ -14,6 +14,16 @@ export async function generateImage(options) {
|
|
|
14
14
|
return requestJsonFromPlan(options, planImageRequest(options));
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
export async function uploadTempMediaImage(options) {
|
|
18
|
+
const form = new FormData();
|
|
19
|
+
form.append("file", new Blob([options.file]), options.filename ?? "image");
|
|
20
|
+
return requestJsonFromPlan(options, planRequest(options, "/v1/temp-media/images", {
|
|
21
|
+
method: "POST",
|
|
22
|
+
headers: authHeaders(options.apiKey),
|
|
23
|
+
body: form,
|
|
24
|
+
}));
|
|
25
|
+
}
|
|
26
|
+
|
|
17
27
|
export function planImageRequest(options) {
|
|
18
28
|
const model = options.model ?? "nano-banana-pro-preview";
|
|
19
29
|
if (model.startsWith("gpt")) {
|
|
@@ -43,6 +53,10 @@ export function generateVideo(options) {
|
|
|
43
53
|
return requestJsonFromPlan(options, planVideoRequest(options));
|
|
44
54
|
}
|
|
45
55
|
|
|
56
|
+
export function getVideo(options, taskId) {
|
|
57
|
+
return requestJsonFromPlan(options, planVideoStatusRequest(options, taskId));
|
|
58
|
+
}
|
|
59
|
+
|
|
46
60
|
export function planVideoRequest(options) {
|
|
47
61
|
const model = options.model ?? "seedance-2.0-pro";
|
|
48
62
|
const imageUrls = [
|
|
@@ -84,6 +98,10 @@ export function planVideoRequest(options) {
|
|
|
84
98
|
return planJsonPost(options, "/v1/video/generations", basePayload);
|
|
85
99
|
}
|
|
86
100
|
|
|
101
|
+
export function planVideoStatusRequest(options, taskId) {
|
|
102
|
+
return planRequest(options, `/v1/videos/${encodeURIComponent(taskId)}`);
|
|
103
|
+
}
|
|
104
|
+
|
|
87
105
|
export function generateAudio(options) {
|
|
88
106
|
return requestBinaryArtifactFromPlan(options, planAudioRequest(options));
|
|
89
107
|
}
|
|
@@ -215,7 +233,11 @@ async function requestJsonFromPlan(options, plan) {
|
|
|
215
233
|
const response = await fetchImpl(plan.url, {
|
|
216
234
|
method: plan.method,
|
|
217
235
|
headers: plan.headers,
|
|
218
|
-
body: plan.body === undefined
|
|
236
|
+
body: plan.body === undefined
|
|
237
|
+
? undefined
|
|
238
|
+
: isFormData(plan.body)
|
|
239
|
+
? plan.body
|
|
240
|
+
: JSON.stringify(plan.body),
|
|
219
241
|
});
|
|
220
242
|
const body = await readJson(response);
|
|
221
243
|
if (!response.ok) {
|
|
@@ -226,6 +248,10 @@ async function requestJsonFromPlan(options, plan) {
|
|
|
226
248
|
return body;
|
|
227
249
|
}
|
|
228
250
|
|
|
251
|
+
function isFormData(value) {
|
|
252
|
+
return typeof FormData !== "undefined" && value instanceof FormData;
|
|
253
|
+
}
|
|
254
|
+
|
|
229
255
|
async function requestBinaryArtifactFromPlan(options, plan) {
|
|
230
256
|
const fetchImpl = options.fetch ?? fetch;
|
|
231
257
|
const response = await fetchImpl(plan.url, {
|
package/src/artifacts.js
CHANGED
|
@@ -47,7 +47,7 @@ function expandHomePath(path) {
|
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
async function persistItem({ kind, item, outDir, output, index, fetchImpl }) {
|
|
50
|
-
const dataUrl = getString(item, ["url", "data_url", "dataUrl"]);
|
|
50
|
+
const dataUrl = getString(item, ["url", "video_url", "data_url", "dataUrl"]);
|
|
51
51
|
if (dataUrl?.startsWith("data:")) {
|
|
52
52
|
const parsed = parseDataUrl(dataUrl);
|
|
53
53
|
const path = artifactPath({ kind, outDir, output, index, extension: parsed.extension });
|
|
@@ -90,15 +90,20 @@ async function downloadArtifact({ url, path, fetchImpl }) {
|
|
|
90
90
|
function extractItems(response) {
|
|
91
91
|
if (Array.isArray(response?.data)) return response.data;
|
|
92
92
|
if (Array.isArray(response?.artifacts)) return response.artifacts;
|
|
93
|
+
if (Array.isArray(response?.content)) return response.content;
|
|
93
94
|
if (Array.isArray(response?.candidates)) {
|
|
94
95
|
return response.candidates.flatMap((candidate) => candidate.content?.parts ?? []);
|
|
95
96
|
}
|
|
97
|
+
if (typeof response?.metadata?.url === "string") return [{ url: response.metadata.url }];
|
|
98
|
+
if (typeof response?.url === "string") return [response];
|
|
99
|
+
if (typeof response?.video_url?.url === "string") return [{ url: response.video_url.url }];
|
|
96
100
|
return [];
|
|
97
101
|
}
|
|
98
102
|
|
|
99
103
|
function getString(item, keys) {
|
|
100
104
|
for (const key of keys) {
|
|
101
105
|
if (typeof item?.[key] === "string") return item[key];
|
|
106
|
+
if (typeof item?.[key]?.url === "string") return item[key].url;
|
|
102
107
|
if (typeof item?.inlineData?.[key] === "string") return item.inlineData[key];
|
|
103
108
|
if (typeof item?.inline_data?.[key] === "string") return item.inline_data[key];
|
|
104
109
|
}
|
package/src/cli.js
CHANGED
|
@@ -16,7 +16,7 @@ const COMMANDS = new Set([
|
|
|
16
16
|
|
|
17
17
|
const GROUP_ACTIONS = new Set(["audio", "auth", "image", "text", "video"]);
|
|
18
18
|
const GLOBAL_OPTIONS = new Set(["api_key", "base_url", "dry_run", "help", "json", "output", "out"]);
|
|
19
|
-
const REPEATABLE_OPTIONS = new Set(["image_url", "video_url"]);
|
|
19
|
+
const REPEATABLE_OPTIONS = new Set(["image", "image_url", "video_url"]);
|
|
20
20
|
const COMMAND_OPTIONS = {
|
|
21
21
|
"audio generate": new Set(["model", "prompt", "similarity_boost", "stability", "style", "voice_id"]),
|
|
22
22
|
"audio music": new Set(["music_length_ms", "prompt"]),
|
|
@@ -27,6 +27,7 @@ const COMMAND_OPTIONS = {
|
|
|
27
27
|
help: new Set(["ai", "command"]),
|
|
28
28
|
image: new Set([]),
|
|
29
29
|
"image generate": new Set(["model", "n", "prompt", "quality", "size"]),
|
|
30
|
+
"image upload": new Set(["file"]),
|
|
30
31
|
login: new Set(["console_url", "no_open", "open"]),
|
|
31
32
|
logout: new Set([]),
|
|
32
33
|
models: new Set(["type"]),
|
|
@@ -36,7 +37,7 @@ const COMMAND_OPTIONS = {
|
|
|
36
37
|
"text generate": new Set(["model", "prompt"]),
|
|
37
38
|
version: new Set([]),
|
|
38
39
|
video: new Set([]),
|
|
39
|
-
"video generate": new Set(["aspect", "duration", "first_frame_url", "fps", "image_url", "last_frame_url", "model", "prompt", "ratio", "resolution", "video_url"]),
|
|
40
|
+
"video generate": new Set(["aspect", "duration", "first_frame", "first_frame_url", "fps", "image", "image_url", "last_frame", "last_frame_url", "model", "prompt", "ratio", "resolution", "video_url"]),
|
|
40
41
|
};
|
|
41
42
|
|
|
42
43
|
export function parseArgv(argv) {
|
|
@@ -214,10 +215,15 @@ export async function runCommand(command, deps = {}) {
|
|
|
214
215
|
}
|
|
215
216
|
const validAction = command.group === "audio"
|
|
216
217
|
? ["generate", "sfx", "music"].includes(command.action)
|
|
217
|
-
: command.
|
|
218
|
+
: command.group === "image"
|
|
219
|
+
? ["generate", "upload"].includes(command.action)
|
|
220
|
+
: command.action === "generate";
|
|
218
221
|
if (!validAction) {
|
|
219
222
|
throw new Error(`Unknown action for ${command.group}: ${command.action}`);
|
|
220
223
|
}
|
|
224
|
+
if (command.group === "image" && command.action === "upload") {
|
|
225
|
+
return handleImageUpload(command, deps);
|
|
226
|
+
}
|
|
221
227
|
return handleGenerate(command, { ...deps, stdout, stderr });
|
|
222
228
|
}
|
|
223
229
|
|
|
@@ -394,6 +400,46 @@ async function handleAuthStatus(command, deps) {
|
|
|
394
400
|
return command.options.json ? status : formatAuthStatus(status);
|
|
395
401
|
}
|
|
396
402
|
|
|
403
|
+
async function handleImageUpload(command, deps) {
|
|
404
|
+
const { basename } = await import("node:path");
|
|
405
|
+
const { readFile } = await import("node:fs/promises");
|
|
406
|
+
const { resolveApiKey, resolveOrigins } = await import("./config.js");
|
|
407
|
+
const { uploadTempMediaImage } = await import("./api.js");
|
|
408
|
+
const file = command.options.file;
|
|
409
|
+
if (typeof file !== "string" || file.trim() === "") {
|
|
410
|
+
throw new Error("Missing --file value. Run `flatkey image upload --help` to see supported options.");
|
|
411
|
+
}
|
|
412
|
+
const apiKey = await resolveApiKey({
|
|
413
|
+
apiKey: command.options.api_key,
|
|
414
|
+
env: deps.env ?? process.env,
|
|
415
|
+
configDir: deps.configDir,
|
|
416
|
+
});
|
|
417
|
+
const { routerOrigin } = await resolveOrigins({
|
|
418
|
+
baseUrl: command.options.base_url,
|
|
419
|
+
env: deps.env ?? process.env,
|
|
420
|
+
configDir: deps.configDir,
|
|
421
|
+
});
|
|
422
|
+
const response = await uploadTempMediaImage({
|
|
423
|
+
apiKey,
|
|
424
|
+
baseUrl: routerOrigin,
|
|
425
|
+
file: await readFile(await expandHomePath(file)),
|
|
426
|
+
filename: basename(file),
|
|
427
|
+
fetch: deps.fetch,
|
|
428
|
+
});
|
|
429
|
+
const data = response?.data ?? response;
|
|
430
|
+
if (!data?.signed_url) {
|
|
431
|
+
throw new Error("Image upload failed: missing signed_url.");
|
|
432
|
+
}
|
|
433
|
+
return {
|
|
434
|
+
kind: "upload",
|
|
435
|
+
url: data?.signed_url,
|
|
436
|
+
objectKey: data?.object_key,
|
|
437
|
+
expiresAt: data?.expires_at,
|
|
438
|
+
expiresIn: data?.expires_in,
|
|
439
|
+
response,
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
|
|
397
443
|
async function handleGenerate(command, deps) {
|
|
398
444
|
const { resolveApiKey, resolveOrigins } = await import("./config.js");
|
|
399
445
|
const {
|
|
@@ -403,12 +449,14 @@ async function handleGenerate(command, deps) {
|
|
|
403
449
|
generateImage,
|
|
404
450
|
generateText,
|
|
405
451
|
generateVideo,
|
|
452
|
+
getVideo,
|
|
406
453
|
planAudioMusicRequest,
|
|
407
454
|
planAudioRequest,
|
|
408
455
|
planAudioSfxRequest,
|
|
409
456
|
planImageRequest,
|
|
410
457
|
planTextRequest,
|
|
411
458
|
planVideoRequest,
|
|
459
|
+
uploadTempMediaImage,
|
|
412
460
|
} = await import("./api.js");
|
|
413
461
|
const { persistArtifacts } = await import("./artifacts.js");
|
|
414
462
|
const { createAnimation } = await import("./animation.js");
|
|
@@ -428,8 +476,12 @@ async function handleGenerate(command, deps) {
|
|
|
428
476
|
...command.options,
|
|
429
477
|
apiKey,
|
|
430
478
|
baseUrl: routerOrigin,
|
|
479
|
+
env: deps.env ?? process.env,
|
|
431
480
|
fetch: deps.fetch,
|
|
432
481
|
};
|
|
482
|
+
if (command.group === "video" && !command.options.dry_run) {
|
|
483
|
+
await uploadVideoLocalImages(options, { uploadTempMediaImage });
|
|
484
|
+
}
|
|
433
485
|
if (command.options.dry_run) {
|
|
434
486
|
const request = command.group === "image"
|
|
435
487
|
? planImageRequest(options)
|
|
@@ -461,6 +513,9 @@ async function handleGenerate(command, deps) {
|
|
|
461
513
|
? await generateAudioMusic(options)
|
|
462
514
|
: await generateAudio(options)
|
|
463
515
|
: await generateText(options);
|
|
516
|
+
const artifactResponse = command.group === "video"
|
|
517
|
+
? await waitForVideoResult(response, { ...options, getVideo, sleep: deps.sleep })
|
|
518
|
+
: response;
|
|
464
519
|
if (command.group === "text") {
|
|
465
520
|
const text = extractText(response);
|
|
466
521
|
const output = await writeTextOutput(text, command.options.output);
|
|
@@ -468,17 +523,111 @@ async function handleGenerate(command, deps) {
|
|
|
468
523
|
}
|
|
469
524
|
const artifacts = await persistArtifacts({
|
|
470
525
|
kind: command.group,
|
|
471
|
-
response,
|
|
526
|
+
response: artifactResponse,
|
|
472
527
|
outDir: command.options.out ?? "flatkey-output",
|
|
473
528
|
output: command.options.output,
|
|
474
529
|
fetch: deps.fetch,
|
|
475
530
|
});
|
|
476
|
-
return { kind: command.group, artifacts, response: scrubArtifactResponse(
|
|
531
|
+
return { kind: command.group, artifacts, response: scrubArtifactResponse(artifactResponse) };
|
|
477
532
|
} finally {
|
|
478
533
|
animation.stop();
|
|
479
534
|
}
|
|
480
535
|
}
|
|
481
536
|
|
|
537
|
+
async function uploadVideoLocalImages(options, deps) {
|
|
538
|
+
const localImages = asArray(options.image);
|
|
539
|
+
const firstFrame = options.first_frame;
|
|
540
|
+
const lastFrame = options.last_frame;
|
|
541
|
+
if (localImages.length === 0 && !firstFrame && !lastFrame) return;
|
|
542
|
+
|
|
543
|
+
const uploads = [];
|
|
544
|
+
for (const file of localImages) {
|
|
545
|
+
uploads.push(await uploadLocalImage(file, options, deps));
|
|
546
|
+
}
|
|
547
|
+
if (uploads.length > 0) {
|
|
548
|
+
options.image_url = [...asArray(options.image_url), ...uploads];
|
|
549
|
+
}
|
|
550
|
+
if (firstFrame) {
|
|
551
|
+
options.first_frame_url = await uploadLocalImage(firstFrame, options, deps);
|
|
552
|
+
}
|
|
553
|
+
if (lastFrame) {
|
|
554
|
+
options.last_frame_url = await uploadLocalImage(lastFrame, options, deps);
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
async function uploadLocalImage(file, options, deps) {
|
|
559
|
+
const { basename } = await import("node:path");
|
|
560
|
+
const { readFile } = await import("node:fs/promises");
|
|
561
|
+
const response = await deps.uploadTempMediaImage({
|
|
562
|
+
apiKey: options.apiKey,
|
|
563
|
+
baseUrl: options.baseUrl,
|
|
564
|
+
file: await readFile(await expandHomePath(file)),
|
|
565
|
+
filename: basename(file),
|
|
566
|
+
fetch: options.fetch,
|
|
567
|
+
});
|
|
568
|
+
const url = response?.data?.signed_url ?? response?.signed_url;
|
|
569
|
+
if (!url) throw new Error(`Image upload failed: missing signed_url for ${file}`);
|
|
570
|
+
return url;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
async function waitForVideoResult(response, options) {
|
|
574
|
+
if (hasArtifact(response)) return response;
|
|
575
|
+
const taskId = videoTaskId(response);
|
|
576
|
+
if (!taskId) return response;
|
|
577
|
+
|
|
578
|
+
const timeoutMs = parsePositiveInteger(
|
|
579
|
+
options.video_wait_timeout_ms ?? options.env?.FLATKEY_VIDEO_WAIT_TIMEOUT_MS,
|
|
580
|
+
600_000,
|
|
581
|
+
);
|
|
582
|
+
const intervalMs = parsePositiveInteger(
|
|
583
|
+
options.video_poll_interval_ms ?? options.env?.FLATKEY_VIDEO_POLL_INTERVAL_MS,
|
|
584
|
+
5_000,
|
|
585
|
+
);
|
|
586
|
+
const deadline = Date.now() + timeoutMs;
|
|
587
|
+
let last = response;
|
|
588
|
+
|
|
589
|
+
while (Date.now() <= deadline) {
|
|
590
|
+
last = await options.getVideo(options, taskId);
|
|
591
|
+
if (hasArtifact(last) || isVideoDone(last)) return last;
|
|
592
|
+
if (isVideoFailed(last)) {
|
|
593
|
+
const message = last?.error?.message ?? last?.message ?? `Video generation failed: ${taskId}`;
|
|
594
|
+
throw new Error(message);
|
|
595
|
+
}
|
|
596
|
+
await delay(intervalMs, { sleep: options.sleep });
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
throw new Error(`Video task still processing after ${Math.round(timeoutMs / 1000)}s: ${taskId}`);
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
function hasArtifact(response) {
|
|
603
|
+
return Boolean(
|
|
604
|
+
response?.metadata?.url
|
|
605
|
+
|| response?.url
|
|
606
|
+
|| response?.video_url?.url
|
|
607
|
+
|| response?.data?.some?.((item) => item?.url || item?.b64_json || item?.base64 || item?.data)
|
|
608
|
+
|| response?.artifacts?.some?.((item) => item?.url || item?.path)
|
|
609
|
+
|| response?.content?.some?.((item) => item?.url || item?.video_url?.url || item?.data_url || item?.b64_json || item?.base64 || item?.data),
|
|
610
|
+
);
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
function videoTaskId(response) {
|
|
614
|
+
return firstNonEmpty(response?.id, response?.task_id, response?.taskId, response?.data?.id, response?.data?.task_id);
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
function isVideoDone(response) {
|
|
618
|
+
return ["completed", "succeeded", "success"].includes(String(response?.status ?? "").toLowerCase());
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
function isVideoFailed(response) {
|
|
622
|
+
return ["failed", "failure", "cancelled", "canceled"].includes(String(response?.status ?? "").toLowerCase())
|
|
623
|
+
|| Boolean(response?.error);
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
function parsePositiveInteger(value, fallback) {
|
|
627
|
+
const parsed = Number.parseInt(value, 10);
|
|
628
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
|
629
|
+
}
|
|
630
|
+
|
|
482
631
|
function scrubArtifactResponse(value) {
|
|
483
632
|
if (Array.isArray(value)) return value.map(scrubArtifactResponse);
|
|
484
633
|
if (!value || typeof value !== "object") {
|
|
@@ -606,6 +755,7 @@ function formatHuman(result) {
|
|
|
606
755
|
if (typeof result === "string") return result;
|
|
607
756
|
if (!result || typeof result !== "object") return String(result);
|
|
608
757
|
if (result.dryRun && result.request) return formatDryRun(result);
|
|
758
|
+
if (result.kind === "upload") return formatUploadResult(result);
|
|
609
759
|
if (result.kind) return formatGenerationResult(result);
|
|
610
760
|
if (Array.isArray(result.models)) return formatModels(result.models);
|
|
611
761
|
if (Array.isArray(result.voices)) return formatVoices(result.voices);
|
|
@@ -625,6 +775,13 @@ function formatDryRun(result) {
|
|
|
625
775
|
return lines.join("\n");
|
|
626
776
|
}
|
|
627
777
|
|
|
778
|
+
function formatUploadResult(result) {
|
|
779
|
+
const lines = ["Image uploaded:"];
|
|
780
|
+
if (result.url) lines.push(`URL: ${result.url}`);
|
|
781
|
+
if (result.expiresIn) lines.push(`Expires in: ${result.expiresIn}s`);
|
|
782
|
+
return lines.join("\n");
|
|
783
|
+
}
|
|
784
|
+
|
|
628
785
|
function formatGenerationResult(result) {
|
|
629
786
|
if (result.kind === "text") {
|
|
630
787
|
const lines = [result.text || "(empty text)"];
|
package/src/help.js
CHANGED
|
@@ -52,6 +52,7 @@ Commands:
|
|
|
52
52
|
auth status Show saved auth state
|
|
53
53
|
onboard --api-key <key> Save Flatkey API key from https://console.flatkey.ai/keys
|
|
54
54
|
image generate --prompt <txt> Generate image
|
|
55
|
+
image upload --file <path> Upload local image and return a temporary URL
|
|
55
56
|
video generate --prompt <txt> Generate video
|
|
56
57
|
audio generate --prompt <txt> Generate speech with ElevenLabs voices
|
|
57
58
|
audio sfx --prompt <txt> Generate sound effects
|
|
@@ -141,14 +142,18 @@ Options:
|
|
|
141
142
|
|
|
142
143
|
Options:
|
|
143
144
|
--ai Print agent-focused usage guide`,
|
|
144
|
-
image: `Usage: flatkey image
|
|
145
|
+
image: `Usage: flatkey image <command> [options]
|
|
146
|
+
|
|
147
|
+
Commands:
|
|
148
|
+
generate --prompt <txt> Generate image
|
|
149
|
+
upload --file <path> Upload local image and return a temporary URL
|
|
145
150
|
|
|
146
151
|
Options:
|
|
147
|
-
--
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
--
|
|
152
|
+
--json Print machine-readable JSON`,
|
|
153
|
+
"image upload": `Usage: flatkey image upload --file <path> [options]
|
|
154
|
+
|
|
155
|
+
Options:
|
|
156
|
+
--file <path> Local image file to upload
|
|
152
157
|
--json Print machine-readable JSON`,
|
|
153
158
|
"image generate": `Usage: flatkey image generate --prompt <txt> [options]
|
|
154
159
|
|
|
@@ -212,9 +217,12 @@ Options:
|
|
|
212
217
|
--duration <seconds> Video duration
|
|
213
218
|
--ratio <value> 16:9, 9:16, 4:3, 3:4, 21:9, or 1:1
|
|
214
219
|
--resolution <value> 480p, 720p, or 1080p
|
|
220
|
+
--image <file> Upload local reference image, repeatable
|
|
215
221
|
--image-url <url> Add reference image URL, repeatable
|
|
222
|
+
--first-frame <file> Upload local first frame image
|
|
216
223
|
--video-url <url> Add reference video URL, repeatable
|
|
217
224
|
--first-frame-url <url> Add first frame image URL
|
|
225
|
+
--last-frame <file> Upload local last frame image
|
|
218
226
|
--last-frame-url <url> Add last frame image URL
|
|
219
227
|
--fps <fps> Frames per second
|
|
220
228
|
--output, -o <file> Write video file
|
|
@@ -226,9 +234,12 @@ Options:
|
|
|
226
234
|
--duration <seconds> Video duration
|
|
227
235
|
--ratio <value> 16:9, 9:16, 4:3, 3:4, 21:9, or 1:1
|
|
228
236
|
--resolution <value> 480p, 720p, or 1080p
|
|
237
|
+
--image <file> Upload local reference image, repeatable
|
|
229
238
|
--image-url <url> Add reference image URL, repeatable
|
|
239
|
+
--first-frame <file> Upload local first frame image
|
|
230
240
|
--video-url <url> Add reference video URL, repeatable
|
|
231
241
|
--first-frame-url <url> Add first frame image URL
|
|
242
|
+
--last-frame <file> Upload local last frame image
|
|
232
243
|
--last-frame-url <url> Add last frame image URL
|
|
233
244
|
--fps <fps> Frames per second
|
|
234
245
|
--output, -o <file> Write video file
|