@flatkey-ai/cli 0.1.16 → 0.1.18
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 +70 -3
- package/src/artifacts.js +6 -1
- package/src/cli.js +80 -11
- package/src/help.js +10 -2
package/package.json
CHANGED
package/src/api.js
CHANGED
|
@@ -43,7 +43,20 @@ export function generateVideo(options) {
|
|
|
43
43
|
return requestJsonFromPlan(options, planVideoRequest(options));
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
export function getVideo(options, taskId) {
|
|
47
|
+
return requestJsonFromPlan(options, planVideoStatusRequest(options, taskId));
|
|
48
|
+
}
|
|
49
|
+
|
|
46
50
|
export function planVideoRequest(options) {
|
|
51
|
+
const model = options.model ?? "seedance-2.0-pro";
|
|
52
|
+
const imageUrls = [
|
|
53
|
+
...arrayOption(options.image_url),
|
|
54
|
+
...arrayOption(options.imageUrl),
|
|
55
|
+
...arrayOption(options.first_frame_url),
|
|
56
|
+
...arrayOption(options.firstFrameUrl),
|
|
57
|
+
...arrayOption(options.last_frame_url),
|
|
58
|
+
...arrayOption(options.lastFrameUrl),
|
|
59
|
+
];
|
|
47
60
|
const ratio = validateOptionalValue(
|
|
48
61
|
optionValue(options, "ratio", "aspect"),
|
|
49
62
|
["16:9", "9:16", "4:3", "3:4", "21:9", "1:1"],
|
|
@@ -54,8 +67,8 @@ export function planVideoRequest(options) {
|
|
|
54
67
|
["480p", "720p", "1080p"],
|
|
55
68
|
"resolution",
|
|
56
69
|
);
|
|
57
|
-
|
|
58
|
-
model
|
|
70
|
+
const basePayload = cleanObject({
|
|
71
|
+
model,
|
|
59
72
|
prompt: options.prompt,
|
|
60
73
|
duration: parseOptionalInteger(options.duration),
|
|
61
74
|
aspect: ratio,
|
|
@@ -63,7 +76,20 @@ export function planVideoRequest(options) {
|
|
|
63
76
|
resolution,
|
|
64
77
|
quality: resolution,
|
|
65
78
|
fps: parseOptionalInteger(options.fps),
|
|
66
|
-
|
|
79
|
+
images: !isSeedanceModel(model) && imageUrls.length > 0 ? imageUrls : undefined,
|
|
80
|
+
});
|
|
81
|
+
const seedanceContent = buildSeedanceContent(options);
|
|
82
|
+
if (isSeedanceModel(model) && seedanceContent.length > 0) {
|
|
83
|
+
return planJsonPost(options, "/v1/video/generations", cleanObject({
|
|
84
|
+
...basePayload,
|
|
85
|
+
content: seedanceContent,
|
|
86
|
+
}));
|
|
87
|
+
}
|
|
88
|
+
return planJsonPost(options, "/v1/video/generations", basePayload);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function planVideoStatusRequest(options, taskId) {
|
|
92
|
+
return planRequest(options, `/v1/videos/${encodeURIComponent(taskId)}`);
|
|
67
93
|
}
|
|
68
94
|
|
|
69
95
|
export function generateAudio(options) {
|
|
@@ -275,6 +301,47 @@ function validateOptionalValue(value, allowed, name) {
|
|
|
275
301
|
throw new Error(`Invalid ${name}: ${value}. Allowed values: ${allowed.join(", ")}`);
|
|
276
302
|
}
|
|
277
303
|
|
|
304
|
+
function isSeedanceModel(model) {
|
|
305
|
+
return /seedance/i.test(model);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function buildSeedanceContent(options) {
|
|
309
|
+
const content = [];
|
|
310
|
+
if (options.prompt !== undefined) {
|
|
311
|
+
content.push({ type: "text", text: options.prompt });
|
|
312
|
+
}
|
|
313
|
+
for (const url of arrayOption(options.image_url)) {
|
|
314
|
+
content.push({ type: "image_url", image_url: { url }, role: "reference_image" });
|
|
315
|
+
}
|
|
316
|
+
for (const url of arrayOption(options.imageUrl)) {
|
|
317
|
+
content.push({ type: "image_url", image_url: { url }, role: "reference_image" });
|
|
318
|
+
}
|
|
319
|
+
for (const url of arrayOption(options.first_frame_url)) {
|
|
320
|
+
content.push({ type: "image_url", image_url: { url }, role: "first_frame" });
|
|
321
|
+
}
|
|
322
|
+
for (const url of arrayOption(options.firstFrameUrl)) {
|
|
323
|
+
content.push({ type: "image_url", image_url: { url }, role: "first_frame" });
|
|
324
|
+
}
|
|
325
|
+
for (const url of arrayOption(options.last_frame_url)) {
|
|
326
|
+
content.push({ type: "image_url", image_url: { url }, role: "last_frame" });
|
|
327
|
+
}
|
|
328
|
+
for (const url of arrayOption(options.lastFrameUrl)) {
|
|
329
|
+
content.push({ type: "image_url", image_url: { url }, role: "last_frame" });
|
|
330
|
+
}
|
|
331
|
+
for (const url of arrayOption(options.video_url)) {
|
|
332
|
+
content.push({ type: "video_url", video_url: { url }, role: "reference_video" });
|
|
333
|
+
}
|
|
334
|
+
for (const url of arrayOption(options.videoUrl)) {
|
|
335
|
+
content.push({ type: "video_url", video_url: { url }, role: "reference_video" });
|
|
336
|
+
}
|
|
337
|
+
return content;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function arrayOption(value) {
|
|
341
|
+
if (value === undefined) return [];
|
|
342
|
+
return Array.isArray(value) ? value : [value];
|
|
343
|
+
}
|
|
344
|
+
|
|
278
345
|
function cleanObject(value) {
|
|
279
346
|
return Object.fromEntries(
|
|
280
347
|
Object.entries(value).filter(([, entry]) => entry !== undefined),
|
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,6 +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
20
|
const COMMAND_OPTIONS = {
|
|
20
21
|
"audio generate": new Set(["model", "prompt", "similarity_boost", "stability", "style", "voice_id"]),
|
|
21
22
|
"audio music": new Set(["music_length_ms", "prompt"]),
|
|
@@ -35,7 +36,7 @@ const COMMAND_OPTIONS = {
|
|
|
35
36
|
"text generate": new Set(["model", "prompt"]),
|
|
36
37
|
version: new Set([]),
|
|
37
38
|
video: new Set([]),
|
|
38
|
-
"video generate": new Set(["aspect", "duration", "fps", "model", "prompt", "ratio", "resolution"]),
|
|
39
|
+
"video generate": new Set(["aspect", "duration", "first_frame_url", "fps", "image_url", "last_frame_url", "model", "prompt", "ratio", "resolution", "video_url"]),
|
|
39
40
|
};
|
|
40
41
|
|
|
41
42
|
export function parseArgv(argv) {
|
|
@@ -115,12 +116,21 @@ function parseOptions(tokens) {
|
|
|
115
116
|
options[name] = true;
|
|
116
117
|
continue;
|
|
117
118
|
}
|
|
118
|
-
|
|
119
|
+
if (REPEATABLE_OPTIONS.has(name)) {
|
|
120
|
+
options[name] = [...asArray(options[name]), next];
|
|
121
|
+
} else {
|
|
122
|
+
options[name] = next;
|
|
123
|
+
}
|
|
119
124
|
index += 1;
|
|
120
125
|
}
|
|
121
126
|
return options;
|
|
122
127
|
}
|
|
123
128
|
|
|
129
|
+
function asArray(value) {
|
|
130
|
+
if (value === undefined) return [];
|
|
131
|
+
return Array.isArray(value) ? value : [value];
|
|
132
|
+
}
|
|
133
|
+
|
|
124
134
|
function validateCommandOptions(command) {
|
|
125
135
|
const key = command.action ? `${command.group} ${command.action}` : command.group;
|
|
126
136
|
const allowed = COMMAND_OPTIONS[key] ?? new Set();
|
|
@@ -215,15 +225,11 @@ export async function runCommand(command, deps = {}) {
|
|
|
215
225
|
}
|
|
216
226
|
|
|
217
227
|
async function handleLogin(command, deps) {
|
|
218
|
-
const { ensureDeviceId, readConfig,
|
|
219
|
-
const { createDeviceAuthorization, pollDeviceAuthorization } = await import("./api.js");
|
|
228
|
+
const { ensureDeviceId, readConfig, writeAuthConfig } = await import("./config.js");
|
|
229
|
+
const { DEFAULT_CONSOLE_URL, createDeviceAuthorization, pollDeviceAuthorization } = await import("./api.js");
|
|
220
230
|
const deviceId = await ensureDeviceId({ configDir: deps.configDir });
|
|
221
231
|
const version = await readPackageVersion();
|
|
222
|
-
const
|
|
223
|
-
consoleUrl: command.options.console_url,
|
|
224
|
-
env: deps.env ?? process.env,
|
|
225
|
-
configDir: deps.configDir,
|
|
226
|
-
});
|
|
232
|
+
const consoleOrigin = firstNonEmpty(command.options.console_url, DEFAULT_CONSOLE_URL);
|
|
227
233
|
const authorization = await createDeviceAuthorization({
|
|
228
234
|
consoleUrl: consoleOrigin,
|
|
229
235
|
deviceId,
|
|
@@ -397,6 +403,7 @@ async function handleGenerate(command, deps) {
|
|
|
397
403
|
generateImage,
|
|
398
404
|
generateText,
|
|
399
405
|
generateVideo,
|
|
406
|
+
getVideo,
|
|
400
407
|
planAudioMusicRequest,
|
|
401
408
|
planAudioRequest,
|
|
402
409
|
planAudioSfxRequest,
|
|
@@ -422,6 +429,7 @@ async function handleGenerate(command, deps) {
|
|
|
422
429
|
...command.options,
|
|
423
430
|
apiKey,
|
|
424
431
|
baseUrl: routerOrigin,
|
|
432
|
+
env: deps.env ?? process.env,
|
|
425
433
|
fetch: deps.fetch,
|
|
426
434
|
};
|
|
427
435
|
if (command.options.dry_run) {
|
|
@@ -455,6 +463,9 @@ async function handleGenerate(command, deps) {
|
|
|
455
463
|
? await generateAudioMusic(options)
|
|
456
464
|
: await generateAudio(options)
|
|
457
465
|
: await generateText(options);
|
|
466
|
+
const artifactResponse = command.group === "video"
|
|
467
|
+
? await waitForVideoResult(response, { ...options, getVideo, sleep: deps.sleep })
|
|
468
|
+
: response;
|
|
458
469
|
if (command.group === "text") {
|
|
459
470
|
const text = extractText(response);
|
|
460
471
|
const output = await writeTextOutput(text, command.options.output);
|
|
@@ -462,17 +473,75 @@ async function handleGenerate(command, deps) {
|
|
|
462
473
|
}
|
|
463
474
|
const artifacts = await persistArtifacts({
|
|
464
475
|
kind: command.group,
|
|
465
|
-
response,
|
|
476
|
+
response: artifactResponse,
|
|
466
477
|
outDir: command.options.out ?? "flatkey-output",
|
|
467
478
|
output: command.options.output,
|
|
468
479
|
fetch: deps.fetch,
|
|
469
480
|
});
|
|
470
|
-
return { kind: command.group, artifacts, response: scrubArtifactResponse(
|
|
481
|
+
return { kind: command.group, artifacts, response: scrubArtifactResponse(artifactResponse) };
|
|
471
482
|
} finally {
|
|
472
483
|
animation.stop();
|
|
473
484
|
}
|
|
474
485
|
}
|
|
475
486
|
|
|
487
|
+
async function waitForVideoResult(response, options) {
|
|
488
|
+
if (hasArtifact(response)) return response;
|
|
489
|
+
const taskId = videoTaskId(response);
|
|
490
|
+
if (!taskId) return response;
|
|
491
|
+
|
|
492
|
+
const timeoutMs = parsePositiveInteger(
|
|
493
|
+
options.video_wait_timeout_ms ?? options.env?.FLATKEY_VIDEO_WAIT_TIMEOUT_MS,
|
|
494
|
+
600_000,
|
|
495
|
+
);
|
|
496
|
+
const intervalMs = parsePositiveInteger(
|
|
497
|
+
options.video_poll_interval_ms ?? options.env?.FLATKEY_VIDEO_POLL_INTERVAL_MS,
|
|
498
|
+
5_000,
|
|
499
|
+
);
|
|
500
|
+
const deadline = Date.now() + timeoutMs;
|
|
501
|
+
let last = response;
|
|
502
|
+
|
|
503
|
+
while (Date.now() <= deadline) {
|
|
504
|
+
last = await options.getVideo(options, taskId);
|
|
505
|
+
if (hasArtifact(last) || isVideoDone(last)) return last;
|
|
506
|
+
if (isVideoFailed(last)) {
|
|
507
|
+
const message = last?.error?.message ?? last?.message ?? `Video generation failed: ${taskId}`;
|
|
508
|
+
throw new Error(message);
|
|
509
|
+
}
|
|
510
|
+
await delay(intervalMs, { sleep: options.sleep });
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
throw new Error(`Video task still processing after ${Math.round(timeoutMs / 1000)}s: ${taskId}`);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
function hasArtifact(response) {
|
|
517
|
+
return Boolean(
|
|
518
|
+
response?.metadata?.url
|
|
519
|
+
|| response?.url
|
|
520
|
+
|| response?.video_url?.url
|
|
521
|
+
|| response?.data?.some?.((item) => item?.url || item?.b64_json || item?.base64 || item?.data)
|
|
522
|
+
|| response?.artifacts?.some?.((item) => item?.url || item?.path)
|
|
523
|
+
|| response?.content?.some?.((item) => item?.url || item?.video_url?.url || item?.data_url || item?.b64_json || item?.base64 || item?.data),
|
|
524
|
+
);
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
function videoTaskId(response) {
|
|
528
|
+
return firstNonEmpty(response?.id, response?.task_id, response?.taskId, response?.data?.id, response?.data?.task_id);
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
function isVideoDone(response) {
|
|
532
|
+
return ["completed", "succeeded", "success"].includes(String(response?.status ?? "").toLowerCase());
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
function isVideoFailed(response) {
|
|
536
|
+
return ["failed", "failure", "cancelled", "canceled"].includes(String(response?.status ?? "").toLowerCase())
|
|
537
|
+
|| Boolean(response?.error);
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
function parsePositiveInteger(value, fallback) {
|
|
541
|
+
const parsed = Number.parseInt(value, 10);
|
|
542
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
|
543
|
+
}
|
|
544
|
+
|
|
476
545
|
function scrubArtifactResponse(value) {
|
|
477
546
|
if (Array.isArray(value)) return value.map(scrubArtifactResponse);
|
|
478
547
|
if (!value || typeof value !== "object") {
|
package/src/help.js
CHANGED
|
@@ -208,20 +208,28 @@ Options:
|
|
|
208
208
|
video: `Usage: flatkey video generate --prompt <txt> [options]
|
|
209
209
|
|
|
210
210
|
Options:
|
|
211
|
-
--model <model> Model id, default
|
|
211
|
+
--model <model> Model id, default seedance-2.0-pro
|
|
212
212
|
--duration <seconds> Video duration
|
|
213
213
|
--ratio <value> 16:9, 9:16, 4:3, 3:4, 21:9, or 1:1
|
|
214
214
|
--resolution <value> 480p, 720p, or 1080p
|
|
215
|
+
--image-url <url> Add reference image URL, repeatable
|
|
216
|
+
--video-url <url> Add reference video URL, repeatable
|
|
217
|
+
--first-frame-url <url> Add first frame image URL
|
|
218
|
+
--last-frame-url <url> Add last frame image URL
|
|
215
219
|
--fps <fps> Frames per second
|
|
216
220
|
--output, -o <file> Write video file
|
|
217
221
|
--json Print machine-readable JSON`,
|
|
218
222
|
"video generate": `Usage: flatkey video generate --prompt <txt> [options]
|
|
219
223
|
|
|
220
224
|
Options:
|
|
221
|
-
--model <model> Model id, default
|
|
225
|
+
--model <model> Model id, default seedance-2.0-pro
|
|
222
226
|
--duration <seconds> Video duration
|
|
223
227
|
--ratio <value> 16:9, 9:16, 4:3, 3:4, 21:9, or 1:1
|
|
224
228
|
--resolution <value> 480p, 720p, or 1080p
|
|
229
|
+
--image-url <url> Add reference image URL, repeatable
|
|
230
|
+
--video-url <url> Add reference video URL, repeatable
|
|
231
|
+
--first-frame-url <url> Add first frame image URL
|
|
232
|
+
--last-frame-url <url> Add last frame image URL
|
|
225
233
|
--fps <fps> Frames per second
|
|
226
234
|
--output, -o <file> Write video file
|
|
227
235
|
--json Print machine-readable JSON`,
|