@flatkey-ai/cli 0.1.18 → 0.1.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +29 -0
- package/package.json +1 -1
- package/src/api.js +19 -1
- package/src/cli.js +97 -3
- package/src/help.js +17 -6
package/README.md
CHANGED
|
@@ -92,6 +92,12 @@ flatkey image generate \
|
|
|
92
92
|
-o cover.png
|
|
93
93
|
```
|
|
94
94
|
|
|
95
|
+
Upload a local image and get a temporary signed URL:
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
flatkey image upload --file ./reference.png
|
|
99
|
+
```
|
|
100
|
+
|
|
95
101
|
### Generate Video
|
|
96
102
|
|
|
97
103
|
```bash
|
|
@@ -106,6 +112,27 @@ flatkey video generate \
|
|
|
106
112
|
Video ratios: `16:9`, `9:16`, `4:3`, `3:4`, `21:9`, `1:1`.
|
|
107
113
|
Video resolutions: `480p`, `720p`, `1080p`.
|
|
108
114
|
|
|
115
|
+
Local reference images are uploaded through Flatkey temporary media first:
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
flatkey video generate \
|
|
119
|
+
--model Seedance2.0-pro \
|
|
120
|
+
--prompt "a sleepy kitten, soft window light" \
|
|
121
|
+
--image ./reference.png \
|
|
122
|
+
-o kitten.mp4
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
First/last frame:
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
flatkey video generate \
|
|
129
|
+
--model Seedance2.0-pro \
|
|
130
|
+
--prompt "slow camera push-in" \
|
|
131
|
+
--first-frame ./first.png \
|
|
132
|
+
--last-frame ./last.png \
|
|
133
|
+
-o transition.mp4
|
|
134
|
+
```
|
|
135
|
+
|
|
109
136
|
### Generate Audio
|
|
110
137
|
|
|
111
138
|
Text to speech:
|
|
@@ -196,6 +223,8 @@ Agent rules:
|
|
|
196
223
|
- Prefer `FLATKEY_API_KEY`.
|
|
197
224
|
- Always pass `--json` for machine-readable output.
|
|
198
225
|
- Use `--output` / `-o` when the generated file path matters.
|
|
226
|
+
- Use `flatkey image upload --file <path>` for a temporary signed image URL.
|
|
227
|
+
- Use `--image`, `--first-frame`, or `--last-frame` to pass local images into video generation.
|
|
199
228
|
- Call `flatkey models --json` before choosing a model.
|
|
200
229
|
- Call `flatkey audio voices --json` before choosing a TTS `voice_id`.
|
|
201
230
|
- Use `--dry-run` to inspect request shape without spending credits.
|
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")) {
|
|
@@ -223,7 +233,11 @@ async function requestJsonFromPlan(options, plan) {
|
|
|
223
233
|
const response = await fetchImpl(plan.url, {
|
|
224
234
|
method: plan.method,
|
|
225
235
|
headers: plan.headers,
|
|
226
|
-
body: plan.body === undefined
|
|
236
|
+
body: plan.body === undefined
|
|
237
|
+
? undefined
|
|
238
|
+
: isFormData(plan.body)
|
|
239
|
+
? plan.body
|
|
240
|
+
: JSON.stringify(plan.body),
|
|
227
241
|
});
|
|
228
242
|
const body = await readJson(response);
|
|
229
243
|
if (!response.ok) {
|
|
@@ -234,6 +248,10 @@ async function requestJsonFromPlan(options, plan) {
|
|
|
234
248
|
return body;
|
|
235
249
|
}
|
|
236
250
|
|
|
251
|
+
function isFormData(value) {
|
|
252
|
+
return typeof FormData !== "undefined" && value instanceof FormData;
|
|
253
|
+
}
|
|
254
|
+
|
|
237
255
|
async function requestBinaryArtifactFromPlan(options, plan) {
|
|
238
256
|
const fetchImpl = options.fetch ?? fetch;
|
|
239
257
|
const response = await fetchImpl(plan.url, {
|
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 {
|
|
@@ -410,6 +456,7 @@ async function handleGenerate(command, deps) {
|
|
|
410
456
|
planImageRequest,
|
|
411
457
|
planTextRequest,
|
|
412
458
|
planVideoRequest,
|
|
459
|
+
uploadTempMediaImage,
|
|
413
460
|
} = await import("./api.js");
|
|
414
461
|
const { persistArtifacts } = await import("./artifacts.js");
|
|
415
462
|
const { createAnimation } = await import("./animation.js");
|
|
@@ -432,6 +479,9 @@ async function handleGenerate(command, deps) {
|
|
|
432
479
|
env: deps.env ?? process.env,
|
|
433
480
|
fetch: deps.fetch,
|
|
434
481
|
};
|
|
482
|
+
if (command.group === "video" && !command.options.dry_run) {
|
|
483
|
+
await uploadVideoLocalImages(options, { uploadTempMediaImage });
|
|
484
|
+
}
|
|
435
485
|
if (command.options.dry_run) {
|
|
436
486
|
const request = command.group === "image"
|
|
437
487
|
? planImageRequest(options)
|
|
@@ -484,6 +534,42 @@ async function handleGenerate(command, deps) {
|
|
|
484
534
|
}
|
|
485
535
|
}
|
|
486
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
|
+
|
|
487
573
|
async function waitForVideoResult(response, options) {
|
|
488
574
|
if (hasArtifact(response)) return response;
|
|
489
575
|
const taskId = videoTaskId(response);
|
|
@@ -669,6 +755,7 @@ function formatHuman(result) {
|
|
|
669
755
|
if (typeof result === "string") return result;
|
|
670
756
|
if (!result || typeof result !== "object") return String(result);
|
|
671
757
|
if (result.dryRun && result.request) return formatDryRun(result);
|
|
758
|
+
if (result.kind === "upload") return formatUploadResult(result);
|
|
672
759
|
if (result.kind) return formatGenerationResult(result);
|
|
673
760
|
if (Array.isArray(result.models)) return formatModels(result.models);
|
|
674
761
|
if (Array.isArray(result.voices)) return formatVoices(result.voices);
|
|
@@ -688,6 +775,13 @@ function formatDryRun(result) {
|
|
|
688
775
|
return lines.join("\n");
|
|
689
776
|
}
|
|
690
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
|
+
|
|
691
785
|
function formatGenerationResult(result) {
|
|
692
786
|
if (result.kind === "text") {
|
|
693
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
|