@flatkey-ai/cli 0.1.17 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flatkey-ai/cli",
3
- "version": "0.1.17",
3
+ "version": "0.1.18",
4
4
  "description": "Flatkey media generation CLI for image, video, audio, credits, status, and models.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/api.js CHANGED
@@ -43,6 +43,10 @@ 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) {
47
51
  const model = options.model ?? "seedance-2.0-pro";
48
52
  const imageUrls = [
@@ -84,6 +88,10 @@ export function planVideoRequest(options) {
84
88
  return planJsonPost(options, "/v1/video/generations", basePayload);
85
89
  }
86
90
 
91
+ export function planVideoStatusRequest(options, taskId) {
92
+ return planRequest(options, `/v1/videos/${encodeURIComponent(taskId)}`);
93
+ }
94
+
87
95
  export function generateAudio(options) {
88
96
  return requestBinaryArtifactFromPlan(options, planAudioRequest(options));
89
97
  }
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
@@ -403,6 +403,7 @@ async function handleGenerate(command, deps) {
403
403
  generateImage,
404
404
  generateText,
405
405
  generateVideo,
406
+ getVideo,
406
407
  planAudioMusicRequest,
407
408
  planAudioRequest,
408
409
  planAudioSfxRequest,
@@ -428,6 +429,7 @@ async function handleGenerate(command, deps) {
428
429
  ...command.options,
429
430
  apiKey,
430
431
  baseUrl: routerOrigin,
432
+ env: deps.env ?? process.env,
431
433
  fetch: deps.fetch,
432
434
  };
433
435
  if (command.options.dry_run) {
@@ -461,6 +463,9 @@ async function handleGenerate(command, deps) {
461
463
  ? await generateAudioMusic(options)
462
464
  : await generateAudio(options)
463
465
  : await generateText(options);
466
+ const artifactResponse = command.group === "video"
467
+ ? await waitForVideoResult(response, { ...options, getVideo, sleep: deps.sleep })
468
+ : response;
464
469
  if (command.group === "text") {
465
470
  const text = extractText(response);
466
471
  const output = await writeTextOutput(text, command.options.output);
@@ -468,17 +473,75 @@ async function handleGenerate(command, deps) {
468
473
  }
469
474
  const artifacts = await persistArtifacts({
470
475
  kind: command.group,
471
- response,
476
+ response: artifactResponse,
472
477
  outDir: command.options.out ?? "flatkey-output",
473
478
  output: command.options.output,
474
479
  fetch: deps.fetch,
475
480
  });
476
- return { kind: command.group, artifacts, response: scrubArtifactResponse(response) };
481
+ return { kind: command.group, artifacts, response: scrubArtifactResponse(artifactResponse) };
477
482
  } finally {
478
483
  animation.stop();
479
484
  }
480
485
  }
481
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
+
482
545
  function scrubArtifactResponse(value) {
483
546
  if (Array.isArray(value)) return value.map(scrubArtifactResponse);
484
547
  if (!value || typeof value !== "object") {