@flatkey-ai/cli 0.3.5 → 0.3.6
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 +20 -0
- package/package.json +1 -1
- package/src/api.js +43 -0
- package/src/artifacts.js +36 -16
- package/src/cli.js +3 -1
- package/src/help.js +13 -0
package/README.md
CHANGED
|
@@ -127,6 +127,26 @@ Reference images (`--file` / `--image-url`) are sent as an image edit: gpt
|
|
|
127
127
|
models go through `/v1/images/edits`, Gemini models receive them as native
|
|
128
128
|
`fileData` parts, so the input image is preserved instead of being ignored.
|
|
129
129
|
|
|
130
|
+
Animated GIFs (gpt-image-2.6):
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
flatkey image generate \
|
|
134
|
+
--prompt "a paper crane folding itself, soft studio light" \
|
|
135
|
+
--model gpt-image-2.6 \
|
|
136
|
+
--output-format gif \
|
|
137
|
+
--frame-count 9 \
|
|
138
|
+
--frame-duration-ms 200 \
|
|
139
|
+
--quality high \
|
|
140
|
+
--complexity low \
|
|
141
|
+
-o crane.gif
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
`--output-format` accepts `png`, `jpeg`, `webp`, or `gif`. `--frame-count`,
|
|
145
|
+
`--frame-duration-ms`, `--quality`, and `--complexity` shape the animation.
|
|
146
|
+
These options are gpt image model only; passing them with a Gemini model fails
|
|
147
|
+
instead of silently returning a still image. When no `-o` path is given, saved
|
|
148
|
+
artifacts use the requested format's extension (e.g. `flatkey-output/image-01.gif`).
|
|
149
|
+
|
|
130
150
|
Upload a local image and get a temporary signed URL:
|
|
131
151
|
|
|
132
152
|
```bash
|
package/package.json
CHANGED
package/src/api.js
CHANGED
|
@@ -3,6 +3,7 @@ import { withCliRequestHeaders } from "./requestHeaders.js";
|
|
|
3
3
|
export const DEFAULT_BASE_URL = "https://router.flatkey.ai";
|
|
4
4
|
export const DEFAULT_MODELS_BASE_URL = "https://console.flatkey.ai";
|
|
5
5
|
export const DEFAULT_CONSOLE_URL = "https://console.flatkey.ai";
|
|
6
|
+
export const IMAGE_OUTPUT_FORMATS = ["png", "jpeg", "webp", "gif"];
|
|
6
7
|
|
|
7
8
|
export class FlatkeyError extends Error {
|
|
8
9
|
constructor(message, { status, code } = {}) {
|
|
@@ -53,6 +54,21 @@ export function planImageRequest(options) {
|
|
|
53
54
|
size: options.size,
|
|
54
55
|
n: parseOptionalInteger(options.n),
|
|
55
56
|
quality: options.quality,
|
|
57
|
+
// Animated GIF output and its frame controls are gpt-image-2.6 features.
|
|
58
|
+
output_format: validateOptionalValue(
|
|
59
|
+
optionValue(options, "output_format", "outputFormat"),
|
|
60
|
+
IMAGE_OUTPUT_FORMATS,
|
|
61
|
+
"output format",
|
|
62
|
+
),
|
|
63
|
+
frame_count: parseOptionalPositiveInteger(
|
|
64
|
+
optionValue(options, "frame_count", "frameCount"),
|
|
65
|
+
"frame count",
|
|
66
|
+
),
|
|
67
|
+
frame_duration_ms: parseOptionalPositiveInteger(
|
|
68
|
+
optionValue(options, "frame_duration_ms", "frameDurationMs"),
|
|
69
|
+
"frame duration",
|
|
70
|
+
),
|
|
71
|
+
complexity: options.complexity,
|
|
56
72
|
response_format: responseFormat,
|
|
57
73
|
temp_url: tempUrl ?? true,
|
|
58
74
|
});
|
|
@@ -67,6 +83,7 @@ export function planImageRequest(options) {
|
|
|
67
83
|
});
|
|
68
84
|
}
|
|
69
85
|
|
|
86
|
+
assertGptImageOnlyOptions(options, model);
|
|
70
87
|
const path = `/v1beta/models/${encodeURIComponent(model)}:generateContent?key=${encodeURIComponent(options.apiKey)}`;
|
|
71
88
|
return planRequest(options, path, {
|
|
72
89
|
method: "POST",
|
|
@@ -89,6 +106,23 @@ export function planImageRequest(options) {
|
|
|
89
106
|
});
|
|
90
107
|
}
|
|
91
108
|
|
|
109
|
+
// gpt-image animation and complexity controls have no Gemini equivalent, so
|
|
110
|
+
// silently dropping them would hand back a still PNG with a .gif name.
|
|
111
|
+
const GPT_IMAGE_ONLY_OPTIONS = [
|
|
112
|
+
["output_format", "outputFormat"],
|
|
113
|
+
["frame_count", "frameCount"],
|
|
114
|
+
["frame_duration_ms", "frameDurationMs"],
|
|
115
|
+
["complexity"],
|
|
116
|
+
];
|
|
117
|
+
|
|
118
|
+
function assertGptImageOnlyOptions(options, model) {
|
|
119
|
+
for (const aliases of GPT_IMAGE_ONLY_OPTIONS) {
|
|
120
|
+
if (optionValue(options, ...aliases) === undefined) continue;
|
|
121
|
+
const flag = `--${aliases[0].replaceAll("_", "-")}`;
|
|
122
|
+
throw new Error(`${flag} is only supported by gpt image models, not ${model}.`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
92
126
|
function imageMimeType(entry) {
|
|
93
127
|
if (entry.mime) return entry.mime;
|
|
94
128
|
const path = String(entry.url ?? "").split(/[?#]/)[0].toLowerCase();
|
|
@@ -411,6 +445,15 @@ function parseOptionalInteger(value) {
|
|
|
411
445
|
return Number.parseInt(value, 10);
|
|
412
446
|
}
|
|
413
447
|
|
|
448
|
+
function parseOptionalPositiveInteger(value, name) {
|
|
449
|
+
if (value === undefined) return undefined;
|
|
450
|
+
const parsed = Number.parseInt(value, 10);
|
|
451
|
+
if (!Number.isInteger(parsed) || parsed < 1) {
|
|
452
|
+
throw new Error(`Invalid ${name}: ${value}. Expected a positive integer.`);
|
|
453
|
+
}
|
|
454
|
+
return parsed;
|
|
455
|
+
}
|
|
456
|
+
|
|
414
457
|
function parseOptionalFloat(value) {
|
|
415
458
|
if (value === undefined) return undefined;
|
|
416
459
|
return Number.parseFloat(value);
|
package/src/artifacts.js
CHANGED
|
@@ -16,39 +16,65 @@ const MIME_EXTENSIONS = {
|
|
|
16
16
|
"audio/mpeg": "mp3",
|
|
17
17
|
"audio/mp3": "mp3",
|
|
18
18
|
"audio/wav": "wav",
|
|
19
|
+
"image/gif": "gif",
|
|
19
20
|
"image/jpeg": "jpg",
|
|
20
21
|
"image/png": "png",
|
|
22
|
+
"image/webp": "webp",
|
|
21
23
|
"video/mp4": "mp4",
|
|
22
24
|
};
|
|
23
25
|
|
|
26
|
+
const FORMAT_EXTENSIONS = {
|
|
27
|
+
gif: "gif",
|
|
28
|
+
jpeg: "jpg",
|
|
29
|
+
jpg: "jpg",
|
|
30
|
+
png: "png",
|
|
31
|
+
webp: "webp",
|
|
32
|
+
};
|
|
33
|
+
|
|
24
34
|
export async function persistArtifacts({
|
|
25
35
|
kind,
|
|
26
36
|
response,
|
|
27
37
|
outDir = "flatkey-output",
|
|
28
38
|
output,
|
|
39
|
+
format,
|
|
29
40
|
fetch: fetchImpl = fetch,
|
|
30
41
|
}) {
|
|
31
42
|
output = expandHomePath(output);
|
|
32
43
|
const items = extractItems(response);
|
|
33
44
|
const artifacts = [];
|
|
45
|
+
const extension = artifactExtension({ kind, format });
|
|
34
46
|
await mkdir(output ? dirname(output) : outDir, { recursive: true });
|
|
35
|
-
const startIndex = output ? 0 : await nextArtifactIndex({ kind, outDir });
|
|
47
|
+
const startIndex = output ? 0 : await nextArtifactIndex({ kind, outDir, format });
|
|
36
48
|
|
|
37
49
|
for (const [index, item] of items.entries()) {
|
|
38
|
-
const artifact = await persistItem({
|
|
50
|
+
const artifact = await persistItem({
|
|
51
|
+
kind,
|
|
52
|
+
item,
|
|
53
|
+
outDir,
|
|
54
|
+
output,
|
|
55
|
+
index: index + startIndex,
|
|
56
|
+
extension,
|
|
57
|
+
fetchImpl,
|
|
58
|
+
});
|
|
39
59
|
if (artifact) artifacts.push(artifact);
|
|
40
60
|
}
|
|
41
61
|
|
|
42
62
|
return artifacts;
|
|
43
63
|
}
|
|
44
64
|
|
|
45
|
-
export async function nextArtifactPath({ kind, outDir = "flatkey-output" }) {
|
|
46
|
-
const extension =
|
|
47
|
-
const index = await nextArtifactIndex({ kind, outDir });
|
|
65
|
+
export async function nextArtifactPath({ kind, outDir = "flatkey-output", format }) {
|
|
66
|
+
const extension = artifactExtension({ kind, format });
|
|
67
|
+
const index = await nextArtifactIndex({ kind, outDir, format });
|
|
48
68
|
const number = String(index + 1).padStart(2, "0");
|
|
49
69
|
return join(outDir, `${kind}-${number}.${extension}`);
|
|
50
70
|
}
|
|
51
71
|
|
|
72
|
+
function artifactExtension({ kind, format }) {
|
|
73
|
+
return FORMAT_EXTENSIONS[String(format ?? "").toLowerCase()]
|
|
74
|
+
?? DEFAULT_EXTENSIONS[kind]
|
|
75
|
+
?? "bin";
|
|
76
|
+
}
|
|
77
|
+
|
|
52
78
|
function expandHomePath(path) {
|
|
53
79
|
if (typeof path !== "string") return path;
|
|
54
80
|
if (path === "~") return homedir();
|
|
@@ -56,7 +82,7 @@ function expandHomePath(path) {
|
|
|
56
82
|
return path;
|
|
57
83
|
}
|
|
58
84
|
|
|
59
|
-
async function persistItem({ kind, item, outDir, output, index, fetchImpl }) {
|
|
85
|
+
async function persistItem({ kind, item, outDir, output, index, extension, fetchImpl }) {
|
|
60
86
|
const dataUrl = getString(item, ["url", "video_url", "data_url", "dataUrl", "temp_url"]);
|
|
61
87
|
if (dataUrl?.startsWith("data:")) {
|
|
62
88
|
const parsed = parseDataUrl(dataUrl);
|
|
@@ -66,13 +92,7 @@ async function persistItem({ kind, item, outDir, output, index, fetchImpl }) {
|
|
|
66
92
|
}
|
|
67
93
|
if (dataUrl?.startsWith("http://") || dataUrl?.startsWith("https://")) {
|
|
68
94
|
if (output) {
|
|
69
|
-
const path = artifactPath({
|
|
70
|
-
kind,
|
|
71
|
-
outDir,
|
|
72
|
-
output,
|
|
73
|
-
index,
|
|
74
|
-
extension: DEFAULT_EXTENSIONS[kind] ?? "bin",
|
|
75
|
-
});
|
|
95
|
+
const path = artifactPath({ kind, outDir, output, index, extension });
|
|
76
96
|
await downloadArtifact({ url: dataUrl, path, fetchImpl });
|
|
77
97
|
return { path };
|
|
78
98
|
}
|
|
@@ -81,7 +101,7 @@ async function persistItem({ kind, item, outDir, output, index, fetchImpl }) {
|
|
|
81
101
|
|
|
82
102
|
const base64 = getString(item, ["b64_json", "base64", "data"]);
|
|
83
103
|
if (base64) {
|
|
84
|
-
const path = artifactPath({ kind, outDir, output, index, extension
|
|
104
|
+
const path = artifactPath({ kind, outDir, output, index, extension });
|
|
85
105
|
await writeFile(path, Buffer.from(base64, "base64"));
|
|
86
106
|
return { path };
|
|
87
107
|
}
|
|
@@ -145,8 +165,8 @@ function artifactPath({ kind, outDir, output, index, extension }) {
|
|
|
145
165
|
return join(outDir, `${kind}-${number}.${extension}`);
|
|
146
166
|
}
|
|
147
167
|
|
|
148
|
-
async function nextArtifactIndex({ kind, outDir }) {
|
|
149
|
-
const extension =
|
|
168
|
+
async function nextArtifactIndex({ kind, outDir, format }) {
|
|
169
|
+
const extension = artifactExtension({ kind, format });
|
|
150
170
|
const pattern = new RegExp(`^${escapeRegExp(kind)}-(\\d+)\\.${escapeRegExp(extension)}$`);
|
|
151
171
|
let max = 0;
|
|
152
172
|
try {
|
package/src/cli.js
CHANGED
|
@@ -32,7 +32,7 @@ const COMMAND_OPTIONS = {
|
|
|
32
32
|
credits: new Set([]),
|
|
33
33
|
help: new Set(["ai", "command"]),
|
|
34
34
|
image: new Set([]),
|
|
35
|
-
"image generate": new Set(["file", "image", "image_url", "model", "n", "prompt", "quality", "size"]),
|
|
35
|
+
"image generate": new Set(["complexity", "file", "frame_count", "frame_duration_ms", "image", "image_url", "model", "n", "output_format", "prompt", "quality", "size"]),
|
|
36
36
|
"image upload": new Set(["file"]),
|
|
37
37
|
login: new Set(["console_url", "no_open", "open"]),
|
|
38
38
|
logout: new Set([]),
|
|
@@ -669,6 +669,7 @@ async function handleGenerate(command, deps) {
|
|
|
669
669
|
return { kind: command.group, text, output, response };
|
|
670
670
|
}
|
|
671
671
|
const outDir = command.options.out ?? "flatkey-output";
|
|
672
|
+
const format = command.group === "image" ? command.options.output_format : undefined;
|
|
672
673
|
const output = command.options.output
|
|
673
674
|
?? (command.group === "video" ? await nextArtifactPath({ kind: command.group, outDir }) : undefined);
|
|
674
675
|
const artifacts = await persistArtifacts({
|
|
@@ -676,6 +677,7 @@ async function handleGenerate(command, deps) {
|
|
|
676
677
|
response: artifactResponse,
|
|
677
678
|
outDir,
|
|
678
679
|
output,
|
|
680
|
+
format,
|
|
679
681
|
fetch: deps.fetch,
|
|
680
682
|
});
|
|
681
683
|
return {
|
package/src/help.js
CHANGED
|
@@ -15,6 +15,7 @@ JSON mode:
|
|
|
15
15
|
|
|
16
16
|
Commands:
|
|
17
17
|
- flatkey image generate --prompt "<prompt>" --json [--model <model>] [--file <path>] [--image-url <url>] [--output image.png]
|
|
18
|
+
- flatkey image generate --prompt "<prompt>" --json --model gpt-image-2.6 --output-format gif [--frame-count 9] [--frame-duration-ms 200] [--quality high] [--complexity low] [--output loop.gif]
|
|
18
19
|
- flatkey video generate --prompt "<prompt>" --json [--model seedance2] [--file <path>] [--image-url <url>] [--video-url <url>] [--ratio 16:9] [--resolution 720p] [--output video.mp4]
|
|
19
20
|
- flatkey video copy --source input.mp4 --json [--model seedance2] [--frame-ratio 80] [--depth-resolution 480p] [--output video.mp4]
|
|
20
21
|
- flatkey audio generate --prompt "<text>" --json [--voice-id <voice_id>] [--model eleven_multilingual_v2] [--output speech.mp3]
|
|
@@ -83,6 +84,14 @@ Environment:
|
|
|
83
84
|
Video options:
|
|
84
85
|
--ratio <value> 16:9, 9:16, 4:3, 3:4, 21:9, 1:1, or adaptive
|
|
85
86
|
--resolution <value> 480p, 720p, or 1080p
|
|
87
|
+
|
|
88
|
+
Image options (gpt image models):
|
|
89
|
+
--size <size> Image size, e.g. 1024x1024
|
|
90
|
+
--quality <quality> Image quality, e.g. high
|
|
91
|
+
--output-format <format> png, jpeg, webp, or gif
|
|
92
|
+
--frame-count <count> Animation frames for gif output
|
|
93
|
+
--frame-duration-ms <ms> Milliseconds per gif frame
|
|
94
|
+
--complexity <level> Animation complexity, e.g. low
|
|
86
95
|
`;
|
|
87
96
|
}
|
|
88
97
|
|
|
@@ -168,6 +177,10 @@ Options:
|
|
|
168
177
|
--size <size> Image size for OpenAI-style image models
|
|
169
178
|
--quality <quality> Image quality for supported models
|
|
170
179
|
--n <count> Number of images for supported models
|
|
180
|
+
--output-format <format> png, jpeg, webp, or gif; gpt image models only
|
|
181
|
+
--frame-count <count> Animation frames for gif output, gpt image models only
|
|
182
|
+
--frame-duration-ms <ms> Milliseconds per gif frame, gpt image models only
|
|
183
|
+
--complexity <level> Animation complexity, e.g. low; gpt image models only
|
|
171
184
|
--file <path> Local image input, repeatable
|
|
172
185
|
--image-url <url> Remote image input, repeatable
|
|
173
186
|
--output, -o <file> Write image file
|