@flatkey-ai/cli 0.2.9 → 0.3.3
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 +39 -1
- package/package.json +4 -2
- package/scripts/install-video-runtime.js +14 -0
- package/src/api.js +123 -41
- package/src/artifacts.js +30 -2
- package/src/cli.js +340 -63
- package/src/help.js +33 -12
- package/src/video-copy.js +251 -0
- package/src/video-runtime.js +119 -0
package/src/cli.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { join } from "node:path";
|
|
2
2
|
import { DEFAULT_BASE_URL } from "./api.js";
|
|
3
|
+
import { nextArtifactPath, persistArtifacts } from "./artifacts.js";
|
|
3
4
|
|
|
4
5
|
const COMMANDS = new Set([
|
|
5
6
|
"audio",
|
|
@@ -20,6 +21,8 @@ const COMMANDS = new Set([
|
|
|
20
21
|
const GROUP_ACTIONS = new Set(["audio", "auth", "image", "text", "video"]);
|
|
21
22
|
const GLOBAL_OPTIONS = new Set(["api_key", "base_url", "console_url", "dry_run", "help", "json", "output", "out", "verbose"]);
|
|
22
23
|
const REPEATABLE_OPTIONS = new Set(["image", "image_url", "video_url"]);
|
|
24
|
+
const MEDIA_INPUT_OPTIONS = new Set(["file", "image", "image_url", "video_url", "first_frame", "first_frame_url", "last_frame", "last_frame_url"]);
|
|
25
|
+
const MAX_REFERENCE_IMAGES = 5;
|
|
23
26
|
const COMMAND_OPTIONS = {
|
|
24
27
|
"audio generate": new Set(["model", "prompt", "similarity_boost", "stability", "style", "voice_id"]),
|
|
25
28
|
"audio music": new Set(["music_length_ms", "prompt"]),
|
|
@@ -29,7 +32,7 @@ const COMMAND_OPTIONS = {
|
|
|
29
32
|
credits: new Set([]),
|
|
30
33
|
help: new Set(["ai", "command"]),
|
|
31
34
|
image: new Set([]),
|
|
32
|
-
"image generate": new Set(["model", "n", "prompt", "quality", "size"]),
|
|
35
|
+
"image generate": new Set(["file", "image", "image_url", "model", "n", "prompt", "quality", "size"]),
|
|
33
36
|
"image upload": new Set(["file"]),
|
|
34
37
|
login: new Set(["console_url", "no_open", "open"]),
|
|
35
38
|
logout: new Set([]),
|
|
@@ -40,7 +43,8 @@ const COMMAND_OPTIONS = {
|
|
|
40
43
|
"text generate": new Set(["model", "prompt"]),
|
|
41
44
|
version: new Set([]),
|
|
42
45
|
video: new Set([]),
|
|
43
|
-
"video generate": new Set(["aspect", "duration", "first_frame", "first_frame_url", "fps", "generate_audio", "image", "image_url", "last_frame", "last_frame_url", "model", "prompt", "ratio", "resolution", "video_url"]),
|
|
46
|
+
"video generate": new Set(["aspect", "duration", "file", "first_frame", "first_frame_url", "fps", "generate_audio", "image", "image_url", "last_frame", "last_frame_url", "model", "prompt", "ratio", "resolution", "video_url"]),
|
|
47
|
+
"video copy": new Set(["aspect", "depth_location", "depth_model", "depth_resolution", "duration", "fps", "frame_ratio", "generate_audio", "keep_depth", "model", "prompt", "ratio", "resolution", "source"]),
|
|
44
48
|
};
|
|
45
49
|
|
|
46
50
|
export function parseArgv(argv) {
|
|
@@ -61,7 +65,7 @@ export function parseArgv(argv) {
|
|
|
61
65
|
return validateCommandOptions({
|
|
62
66
|
group: "help",
|
|
63
67
|
action: undefined,
|
|
64
|
-
options: { command: maybeAction, ...parseOptions(rest) },
|
|
68
|
+
options: { command: maybeAction, ...parseOptions(rest, repeatableOptionsFor("help")) },
|
|
65
69
|
});
|
|
66
70
|
}
|
|
67
71
|
|
|
@@ -81,7 +85,7 @@ export function parseArgv(argv) {
|
|
|
81
85
|
group,
|
|
82
86
|
action,
|
|
83
87
|
options: {
|
|
84
|
-
...parseOptions(optionTokens.filter((token) => token !== "help" && token !== "--help" && token !== "-h")),
|
|
88
|
+
...parseOptions(optionTokens.filter((token) => token !== "help" && token !== "--help" && token !== "-h"), repeatableOptionsFor(group, action)),
|
|
85
89
|
help: true,
|
|
86
90
|
},
|
|
87
91
|
});
|
|
@@ -90,7 +94,7 @@ export function parseArgv(argv) {
|
|
|
90
94
|
return validateCommandOptions({
|
|
91
95
|
group,
|
|
92
96
|
action,
|
|
93
|
-
options: parseOptions(optionTokens),
|
|
97
|
+
options: parseOptions(optionTokens, repeatableOptionsFor(group, action)),
|
|
94
98
|
});
|
|
95
99
|
}
|
|
96
100
|
|
|
@@ -98,8 +102,9 @@ function isHelpToken(token) {
|
|
|
98
102
|
return token === "help" || token === "--help" || token === "-h";
|
|
99
103
|
}
|
|
100
104
|
|
|
101
|
-
function parseOptions(tokens) {
|
|
105
|
+
function parseOptions(tokens, repeatableOptions = REPEATABLE_OPTIONS) {
|
|
102
106
|
const options = {};
|
|
107
|
+
const mediaInputs = [];
|
|
103
108
|
for (let index = 0; index < tokens.length; index += 1) {
|
|
104
109
|
const token = tokens[index];
|
|
105
110
|
if (token === "-o") {
|
|
@@ -120,16 +125,39 @@ function parseOptions(tokens) {
|
|
|
120
125
|
options[name] = true;
|
|
121
126
|
continue;
|
|
122
127
|
}
|
|
123
|
-
if (
|
|
128
|
+
if (repeatableOptions.has(name)) {
|
|
124
129
|
options[name] = [...asArray(options[name]), next];
|
|
125
130
|
} else {
|
|
126
131
|
options[name] = next;
|
|
127
132
|
}
|
|
133
|
+
if (MEDIA_INPUT_OPTIONS.has(name)) {
|
|
134
|
+
mediaInputs.push({ name, value: next });
|
|
135
|
+
}
|
|
128
136
|
index += 1;
|
|
129
137
|
}
|
|
138
|
+
if (mediaInputs.length > 0) {
|
|
139
|
+
Object.defineProperty(options, "__media_inputs", {
|
|
140
|
+
value: mediaInputs,
|
|
141
|
+
enumerable: false,
|
|
142
|
+
writable: true,
|
|
143
|
+
});
|
|
144
|
+
}
|
|
130
145
|
return options;
|
|
131
146
|
}
|
|
132
147
|
|
|
148
|
+
function repeatableOptionsFor(group, action) {
|
|
149
|
+
if (group === "image" && action === "generate") {
|
|
150
|
+
return new Set([...REPEATABLE_OPTIONS, "file"]);
|
|
151
|
+
}
|
|
152
|
+
if (group === "video" && action === "generate") {
|
|
153
|
+
return new Set([...REPEATABLE_OPTIONS, "file"]);
|
|
154
|
+
}
|
|
155
|
+
if (group === "video" && action === "copy") {
|
|
156
|
+
return REPEATABLE_OPTIONS;
|
|
157
|
+
}
|
|
158
|
+
return REPEATABLE_OPTIONS;
|
|
159
|
+
}
|
|
160
|
+
|
|
133
161
|
function asArray(value) {
|
|
134
162
|
if (value === undefined) return [];
|
|
135
163
|
return Array.isArray(value) ? value : [value];
|
|
@@ -139,6 +167,7 @@ function validateCommandOptions(command) {
|
|
|
139
167
|
const key = command.action ? `${command.group} ${command.action}` : command.group;
|
|
140
168
|
const allowed = COMMAND_OPTIONS[key] ?? new Set();
|
|
141
169
|
for (const option of Object.keys(command.options)) {
|
|
170
|
+
if (option.startsWith("_")) continue;
|
|
142
171
|
if (GLOBAL_OPTIONS.has(option) || allowed.has(option)) continue;
|
|
143
172
|
const flag = `--${option.replaceAll("_", "-")}`;
|
|
144
173
|
const helpCommand = command.action
|
|
@@ -224,7 +253,9 @@ export async function runCommand(command, deps = {}) {
|
|
|
224
253
|
? ["generate", "sfx", "music"].includes(command.action)
|
|
225
254
|
: command.group === "image"
|
|
226
255
|
? ["generate", "upload"].includes(command.action)
|
|
227
|
-
: command.
|
|
256
|
+
: command.group === "video"
|
|
257
|
+
? ["generate", "copy"].includes(command.action)
|
|
258
|
+
: command.action === "generate";
|
|
228
259
|
if (!validAction) {
|
|
229
260
|
throw new Error(`Unknown action for ${command.group}: ${command.action}`);
|
|
230
261
|
}
|
|
@@ -238,13 +269,17 @@ export async function runCommand(command, deps = {}) {
|
|
|
238
269
|
}
|
|
239
270
|
|
|
240
271
|
async function handleLogin(command, deps) {
|
|
241
|
-
const { ensureDeviceId, readConfig, writeAuthConfig } = await import("./config.js");
|
|
272
|
+
const { ensureDeviceId, readConfig, resolveOrigins, writeAuthConfig } = await import("./config.js");
|
|
242
273
|
const { DEFAULT_CONSOLE_URL, createDeviceAuthorization, pollDeviceAuthorization } = await import("./api.js");
|
|
243
274
|
const deviceId = await ensureDeviceId({ configDir: deps.configDir });
|
|
244
275
|
const version = await readPackageVersion();
|
|
245
|
-
const consoleOrigin =
|
|
276
|
+
const { consoleOrigin } = await resolveOrigins({
|
|
277
|
+
consoleUrl: command.options.console_url,
|
|
278
|
+
env: deps.env ?? process.env,
|
|
279
|
+
configDir: deps.configDir,
|
|
280
|
+
});
|
|
246
281
|
const requestOptions = {
|
|
247
|
-
consoleUrl: consoleOrigin,
|
|
282
|
+
consoleUrl: consoleOrigin ?? DEFAULT_CONSOLE_URL,
|
|
248
283
|
fetch: deps.fetch,
|
|
249
284
|
verbose: command.options.verbose,
|
|
250
285
|
verboseLog: verboseLogger(deps.stderr, command.options.verbose),
|
|
@@ -426,6 +461,10 @@ async function handleImageUpload(command, deps) {
|
|
|
426
461
|
if (typeof file !== "string" || file.trim() === "") {
|
|
427
462
|
throw new Error("Missing --file value. Run `flatkey image upload --help` to see supported options.");
|
|
428
463
|
}
|
|
464
|
+
const media = mediaInfoFromPath(file);
|
|
465
|
+
if (!media || media.kind !== "image") {
|
|
466
|
+
throw new Error(`Unsupported image file type: ${file}`);
|
|
467
|
+
}
|
|
429
468
|
const apiKey = await resolveApiKey({
|
|
430
469
|
apiKey: command.options.api_key,
|
|
431
470
|
env: deps.env ?? process.env,
|
|
@@ -441,13 +480,13 @@ async function handleImageUpload(command, deps) {
|
|
|
441
480
|
baseUrl: consoleOrigin ?? DEFAULT_CONSOLE_URL,
|
|
442
481
|
file: await readFile(await expandHomePath(file)),
|
|
443
482
|
filename: basename(file),
|
|
444
|
-
contentType:
|
|
483
|
+
contentType: media.contentType,
|
|
445
484
|
fetch: deps.fetch,
|
|
446
485
|
verbose: command.options.verbose,
|
|
447
486
|
verboseLog: verboseLogger(deps.stderr, command.options.verbose),
|
|
448
487
|
});
|
|
449
488
|
const data = response?.data ?? response;
|
|
450
|
-
const signedUrl =
|
|
489
|
+
const signedUrl = extractUploadedMediaUrl(response);
|
|
451
490
|
if (!signedUrl) {
|
|
452
491
|
throw new Error("Image upload failed: missing signed_url.");
|
|
453
492
|
}
|
|
@@ -479,9 +518,10 @@ async function handleGenerate(command, deps) {
|
|
|
479
518
|
planTextRequest,
|
|
480
519
|
planVideoRequest,
|
|
481
520
|
uploadTempMediaImage,
|
|
521
|
+
uploadTempMediaVideo,
|
|
482
522
|
} = await import("./api.js");
|
|
483
|
-
const { persistArtifacts } = await import("./artifacts.js");
|
|
484
523
|
const { createAnimation } = await import("./animation.js");
|
|
524
|
+
const { prepareVideoCopy } = await import("./video-copy.js");
|
|
485
525
|
const apiKey = command.options.dry_run
|
|
486
526
|
? (command.options.api_key ?? "FLATKEY_API_KEY")
|
|
487
527
|
: await resolveApiKey({
|
|
@@ -509,8 +549,46 @@ async function handleGenerate(command, deps) {
|
|
|
509
549
|
fetch: deps.fetch,
|
|
510
550
|
verboseLog: verboseLogger(deps.stderr, command.options.verbose),
|
|
511
551
|
};
|
|
512
|
-
if (
|
|
513
|
-
|
|
552
|
+
if (Array.isArray(command.options.__media_inputs)) {
|
|
553
|
+
options.__media_inputs = command.options.__media_inputs;
|
|
554
|
+
}
|
|
555
|
+
let videoCopy;
|
|
556
|
+
if (command.group === "video" && command.action === "copy") {
|
|
557
|
+
if (!command.options.source) {
|
|
558
|
+
throw new Error("Missing --source value. Run `flatkey video copy --help` to see supported options.");
|
|
559
|
+
}
|
|
560
|
+
videoCopy = await (deps.prepareVideoCopy ?? prepareVideoCopy)(command.options, deps);
|
|
561
|
+
options.prompt ??= "Recreate the reference video as faithfully as possible while following the supplied depth motion.";
|
|
562
|
+
options.duration = videoCopy.duration;
|
|
563
|
+
options.__media_inputs = [{
|
|
564
|
+
name: "depth_video",
|
|
565
|
+
value: videoCopy.depthPath,
|
|
566
|
+
kind: "local",
|
|
567
|
+
optionName: "file",
|
|
568
|
+
role: "reference_video",
|
|
569
|
+
}];
|
|
570
|
+
options.__copy_reference_mode = "depth-video";
|
|
571
|
+
}
|
|
572
|
+
if (command.group === "image" || command.group === "video") {
|
|
573
|
+
try {
|
|
574
|
+
await prepareMediaInputs(command, options, {
|
|
575
|
+
apiKey,
|
|
576
|
+
baseUrl: options.baseUrl,
|
|
577
|
+
tempMediaBaseUrl: tempMediaBaseUrl ?? DEFAULT_CONSOLE_URL,
|
|
578
|
+
fetch: deps.fetch,
|
|
579
|
+
verbose: command.options.verbose,
|
|
580
|
+
verboseLog: verboseLogger(deps.stderr, command.options.verbose),
|
|
581
|
+
uploadTempMediaImage,
|
|
582
|
+
uploadTempMediaVideo,
|
|
583
|
+
});
|
|
584
|
+
} catch (error) {
|
|
585
|
+
await videoCopy?.cleanup?.();
|
|
586
|
+
throw error;
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
if (command.group === "video" && command.action === "copy"
|
|
590
|
+
&& !options.__media_inputs?.some((entry) => entry.kind === "video")) {
|
|
591
|
+
throw new Error("Video copy failed to prepare its depth-video reference.");
|
|
514
592
|
}
|
|
515
593
|
if (command.options.dry_run) {
|
|
516
594
|
const request = command.group === "image"
|
|
@@ -524,7 +602,14 @@ async function handleGenerate(command, deps) {
|
|
|
524
602
|
? planAudioMusicRequest(options)
|
|
525
603
|
: planAudioRequest(options)
|
|
526
604
|
: planTextRequest(options);
|
|
527
|
-
|
|
605
|
+
await videoCopy?.cleanup?.();
|
|
606
|
+
return {
|
|
607
|
+
dryRun: true,
|
|
608
|
+
kind: command.group,
|
|
609
|
+
request: redactRequest(request),
|
|
610
|
+
videoCopy: scrubVideoCopy(videoCopy),
|
|
611
|
+
copyReferenceMode: options.__copy_reference_mode,
|
|
612
|
+
};
|
|
528
613
|
}
|
|
529
614
|
const animation = createAnimation({
|
|
530
615
|
json: Boolean(command.options.json),
|
|
@@ -532,20 +617,52 @@ async function handleGenerate(command, deps) {
|
|
|
532
617
|
});
|
|
533
618
|
animation.start(command.group);
|
|
534
619
|
try {
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
? await
|
|
539
|
-
: command.group === "
|
|
540
|
-
?
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
? await
|
|
544
|
-
:
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
620
|
+
let response;
|
|
621
|
+
try {
|
|
622
|
+
response = command.group === "image"
|
|
623
|
+
? await generateImage(options)
|
|
624
|
+
: command.group === "video"
|
|
625
|
+
? await generateVideo(options)
|
|
626
|
+
: command.group === "audio"
|
|
627
|
+
? command.action === "sfx"
|
|
628
|
+
? await generateAudioSfx(options)
|
|
629
|
+
: command.action === "music"
|
|
630
|
+
? await generateAudioMusic(options)
|
|
631
|
+
: await generateAudio(options)
|
|
632
|
+
: await generateText(options);
|
|
633
|
+
response = command.group === "video"
|
|
634
|
+
? await waitForVideoResult(response, { ...options, getVideo, sleep: deps.sleep })
|
|
635
|
+
: response;
|
|
636
|
+
} catch (error) {
|
|
637
|
+
if (!videoCopy || options.__copy_reference_mode !== "depth-video" || !isDepthVideoFetchFailure(error)) {
|
|
638
|
+
throw error;
|
|
639
|
+
}
|
|
640
|
+
options.__media_inputs = (videoCopy.referencePaths ?? []).map((value, index) => ({
|
|
641
|
+
name: `depth_frame_${String(index + 1).padStart(2, "0")}`,
|
|
642
|
+
value,
|
|
643
|
+
kind: "local",
|
|
644
|
+
optionName: "image",
|
|
645
|
+
role: index === 0
|
|
646
|
+
? "first_frame"
|
|
647
|
+
: index === (videoCopy.referencePaths.length - 1)
|
|
648
|
+
? "last_frame"
|
|
649
|
+
: "reference_image",
|
|
650
|
+
}));
|
|
651
|
+
await prepareMediaInputs(command, options, {
|
|
652
|
+
apiKey,
|
|
653
|
+
baseUrl: options.baseUrl,
|
|
654
|
+
tempMediaBaseUrl: tempMediaBaseUrl ?? DEFAULT_CONSOLE_URL,
|
|
655
|
+
fetch: deps.fetch,
|
|
656
|
+
verbose: command.options.verbose,
|
|
657
|
+
verboseLog: verboseLogger(deps.stderr, command.options.verbose),
|
|
658
|
+
uploadTempMediaImage,
|
|
659
|
+
uploadTempMediaVideo,
|
|
660
|
+
});
|
|
661
|
+
options.__copy_reference_mode = "depth-keyframes-fallback";
|
|
662
|
+
response = await generateVideo(options);
|
|
663
|
+
response = await waitForVideoResult(response, { ...options, getVideo, sleep: deps.sleep });
|
|
664
|
+
}
|
|
665
|
+
const artifactResponse = response;
|
|
549
666
|
if (command.group === "text") {
|
|
550
667
|
const text = extractText(response);
|
|
551
668
|
const output = await writeTextOutput(text, command.options.output);
|
|
@@ -553,7 +670,7 @@ async function handleGenerate(command, deps) {
|
|
|
553
670
|
}
|
|
554
671
|
const outDir = command.options.out ?? "flatkey-output";
|
|
555
672
|
const output = command.options.output
|
|
556
|
-
?? (command.group === "video" ?
|
|
673
|
+
?? (command.group === "video" ? await nextArtifactPath({ kind: command.group, outDir }) : undefined);
|
|
557
674
|
const artifacts = await persistArtifacts({
|
|
558
675
|
kind: command.group,
|
|
559
676
|
response: artifactResponse,
|
|
@@ -561,64 +678,201 @@ async function handleGenerate(command, deps) {
|
|
|
561
678
|
output,
|
|
562
679
|
fetch: deps.fetch,
|
|
563
680
|
});
|
|
564
|
-
return {
|
|
681
|
+
return {
|
|
682
|
+
kind: command.group,
|
|
683
|
+
artifacts,
|
|
684
|
+
response: scrubArtifactResponse(artifactResponse),
|
|
685
|
+
...(videoCopy ? { copyReferenceMode: options.__copy_reference_mode } : {}),
|
|
686
|
+
};
|
|
565
687
|
} finally {
|
|
566
688
|
animation.stop();
|
|
689
|
+
await videoCopy?.cleanup?.();
|
|
567
690
|
}
|
|
568
691
|
}
|
|
569
692
|
|
|
570
|
-
async function
|
|
571
|
-
const
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
if (localImages.length === 0 && !firstFrame && !lastFrame) return;
|
|
693
|
+
async function prepareMediaInputs(command, options, deps) {
|
|
694
|
+
const mediaInputs = orderedMediaInputs(command, options);
|
|
695
|
+
if (mediaInputs.length === 0) return;
|
|
696
|
+
assertMediaReferenceLimit(mediaInputs, command.group, options.model);
|
|
575
697
|
|
|
576
|
-
const
|
|
577
|
-
for (const
|
|
578
|
-
|
|
698
|
+
const resolved = [];
|
|
699
|
+
for (const input of mediaInputs) {
|
|
700
|
+
if (input.kind === "local") {
|
|
701
|
+
resolved.push(await uploadLocalMedia(input.value, command.group, deps, input.optionName ?? input.name));
|
|
702
|
+
} else {
|
|
703
|
+
resolved.push(await confirmRemoteMediaUrl(input.value, command.group, deps, input.name));
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
options.__media_inputs = resolved;
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
function assertMediaReferenceLimit(mediaInputs, commandGroup, model) {
|
|
710
|
+
const imageInputs = mediaInputs.filter((entry) => entry.kind !== "video");
|
|
711
|
+
const videoInputs = mediaInputs.filter((entry) => entry.kind === "video");
|
|
712
|
+
if (isSeedance25Model(model)) {
|
|
713
|
+
if (imageInputs.length > 30) {
|
|
714
|
+
throw new Error(`Too many reference images for flatkey ${commandGroup} generate with Seedance 2.5: maximum 30.`);
|
|
715
|
+
}
|
|
716
|
+
if (videoInputs.length > 10) {
|
|
717
|
+
throw new Error(`Too many reference videos for flatkey ${commandGroup} generate with Seedance 2.5: maximum 10.`);
|
|
718
|
+
}
|
|
719
|
+
return;
|
|
579
720
|
}
|
|
580
|
-
if (
|
|
581
|
-
|
|
721
|
+
if (imageInputs.length > MAX_REFERENCE_IMAGES) {
|
|
722
|
+
throw new Error(`Too many reference images for flatkey ${commandGroup} generate: maximum ${MAX_REFERENCE_IMAGES}.`);
|
|
582
723
|
}
|
|
583
|
-
|
|
584
|
-
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
function isSeedance25Model(model) {
|
|
727
|
+
return String(model ?? "").toLowerCase().replaceAll(/[-_.]/g, "").includes("seedance25");
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
function orderedMediaInputs(command, options) {
|
|
731
|
+
const mediaInputs = Array.isArray(options.__media_inputs) ? options.__media_inputs : [];
|
|
732
|
+
if (mediaInputs.length > 0 && mediaInputs[0] && Object.prototype.hasOwnProperty.call(mediaInputs[0], "kind")) {
|
|
733
|
+
return mediaInputs;
|
|
585
734
|
}
|
|
586
|
-
if (
|
|
587
|
-
|
|
735
|
+
if (mediaInputs.length > 0) {
|
|
736
|
+
return mediaInputs.map((entry) => ({
|
|
737
|
+
name: entry.name,
|
|
738
|
+
value: entry.value,
|
|
739
|
+
kind: entry.name.endsWith("_url") ? "url" : "local",
|
|
740
|
+
}));
|
|
588
741
|
}
|
|
742
|
+
|
|
743
|
+
const legacyInputs = [];
|
|
744
|
+
const pushLocal = (name, value) => {
|
|
745
|
+
for (const file of asArray(value)) {
|
|
746
|
+
legacyInputs.push({ name, value: file, kind: "local" });
|
|
747
|
+
}
|
|
748
|
+
};
|
|
749
|
+
const pushUrl = (name, value) => {
|
|
750
|
+
for (const url of asArray(value)) {
|
|
751
|
+
legacyInputs.push({ name, value: url, kind: "url" });
|
|
752
|
+
}
|
|
753
|
+
};
|
|
754
|
+
|
|
755
|
+
pushLocal("file", options.file);
|
|
756
|
+
pushLocal("image", options.image);
|
|
757
|
+
pushUrl("image_url", options.image_url);
|
|
758
|
+
if (command.group === "video") {
|
|
759
|
+
pushUrl("video_url", options.video_url);
|
|
760
|
+
pushLocal("first_frame", options.first_frame);
|
|
761
|
+
pushUrl("first_frame_url", options.first_frame_url);
|
|
762
|
+
pushLocal("last_frame", options.last_frame);
|
|
763
|
+
pushUrl("last_frame_url", options.last_frame_url);
|
|
764
|
+
}
|
|
765
|
+
return legacyInputs;
|
|
589
766
|
}
|
|
590
767
|
|
|
591
|
-
async function
|
|
768
|
+
async function uploadLocalMedia(file, commandGroup, deps, optionName) {
|
|
592
769
|
const { basename } = await import("node:path");
|
|
593
770
|
const { readFile } = await import("node:fs/promises");
|
|
594
|
-
const
|
|
595
|
-
|
|
596
|
-
|
|
771
|
+
const media = mediaInfoFromPath(file);
|
|
772
|
+
const allowedKinds = allowedMediaKindsForOption(commandGroup, optionName);
|
|
773
|
+
if (!media || !allowedKinds.has(media.kind)) {
|
|
774
|
+
throw new Error(`Unsupported --${optionName.replaceAll("_", "-")} file type for flatkey ${commandGroup} generate: ${file}`);
|
|
775
|
+
}
|
|
776
|
+
const uploader = media.kind === "video" ? deps.uploadTempMediaVideo : deps.uploadTempMediaImage;
|
|
777
|
+
const response = await uploader({
|
|
778
|
+
apiKey: deps.apiKey,
|
|
779
|
+
baseUrl: deps.tempMediaBaseUrl ?? deps.baseUrl,
|
|
597
780
|
file: await readFile(await expandHomePath(file)),
|
|
598
781
|
filename: basename(file),
|
|
599
|
-
contentType:
|
|
600
|
-
fetch:
|
|
601
|
-
verbose:
|
|
602
|
-
verboseLog:
|
|
782
|
+
contentType: media.contentType,
|
|
783
|
+
fetch: deps.fetch,
|
|
784
|
+
verbose: deps.verbose,
|
|
785
|
+
verboseLog: deps.verboseLog,
|
|
603
786
|
});
|
|
604
|
-
const url =
|
|
605
|
-
if (!url) throw new Error(`
|
|
606
|
-
return
|
|
787
|
+
const url = extractUploadedMediaUrl(response);
|
|
788
|
+
if (!url) throw new Error(`Media upload failed: missing signed_url for ${file}`);
|
|
789
|
+
return {
|
|
790
|
+
name: optionName,
|
|
791
|
+
value: url,
|
|
792
|
+
kind: media.kind,
|
|
793
|
+
role: mediaRoleFromOption(optionName, media.kind),
|
|
794
|
+
mime: media.contentType,
|
|
795
|
+
};
|
|
607
796
|
}
|
|
608
797
|
|
|
609
|
-
function
|
|
798
|
+
async function confirmRemoteMediaUrl(url, commandGroup, deps, optionName) {
|
|
799
|
+
const fetchImpl = deps.fetch ?? fetch;
|
|
800
|
+
const response = await fetchImpl(url, { method: "HEAD" });
|
|
801
|
+
const media = mediaInfoFromMime(headerValue(response?.headers, "content-type"));
|
|
802
|
+
const allowedKinds = allowedMediaKindsForOption(commandGroup, optionName);
|
|
803
|
+
if (!media || !allowedKinds.has(media.kind)) {
|
|
804
|
+
throw new Error(`Unsupported --${optionName.replaceAll("_", "-")} MIME type for flatkey ${commandGroup} generate: ${headerValue(response?.headers, "content-type") ?? "unknown"}`);
|
|
805
|
+
}
|
|
806
|
+
return {
|
|
807
|
+
name: optionName,
|
|
808
|
+
value: url,
|
|
809
|
+
kind: media.kind,
|
|
810
|
+
role: mediaRoleFromOption(optionName, media.kind),
|
|
811
|
+
mime: media.contentType,
|
|
812
|
+
};
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
function extractUploadedMediaUrl(response) {
|
|
610
816
|
const data = response?.data ?? response;
|
|
611
817
|
return data?.signed_url ?? data?.signedUrl ?? data?.url ?? response?.signed_url ?? response?.signedUrl ?? response?.url;
|
|
612
818
|
}
|
|
613
819
|
|
|
614
|
-
function
|
|
820
|
+
function mediaInfoFromPath(file) {
|
|
615
821
|
const lower = String(file).toLowerCase();
|
|
616
|
-
if (lower.endsWith(".jpg") || lower.endsWith(".jpeg")) return "image/jpeg";
|
|
617
|
-
if (lower.endsWith(".png")) return "image/png";
|
|
618
|
-
if (lower.endsWith(".webp")) return "image/webp";
|
|
822
|
+
if (lower.endsWith(".jpg") || lower.endsWith(".jpeg")) return { kind: "image", contentType: "image/jpeg" };
|
|
823
|
+
if (lower.endsWith(".png")) return { kind: "image", contentType: "image/png" };
|
|
824
|
+
if (lower.endsWith(".webp")) return { kind: "image", contentType: "image/webp" };
|
|
825
|
+
if (lower.endsWith(".mp4")) return { kind: "video", contentType: "video/mp4" };
|
|
826
|
+
if (lower.endsWith(".mov")) return { kind: "video", contentType: "video/quicktime" };
|
|
827
|
+
if (lower.endsWith(".bmp")) return { kind: "image", contentType: "image/bmp" };
|
|
828
|
+
if (lower.endsWith(".tif") || lower.endsWith(".tiff")) return { kind: "image", contentType: "image/tiff" };
|
|
829
|
+
if (lower.endsWith(".gif")) return { kind: "image", contentType: "image/gif" };
|
|
830
|
+
if (lower.endsWith(".heic")) return { kind: "image", contentType: "image/heic" };
|
|
831
|
+
if (lower.endsWith(".heif")) return { kind: "image", contentType: "image/heif" };
|
|
619
832
|
return undefined;
|
|
620
833
|
}
|
|
621
834
|
|
|
835
|
+
function mediaInfoFromMime(contentType) {
|
|
836
|
+
if (typeof contentType !== "string") return undefined;
|
|
837
|
+
const lower = contentType.toLowerCase();
|
|
838
|
+
if (lower.startsWith("image/jpeg")) return { kind: "image", contentType: "image/jpeg" };
|
|
839
|
+
if (lower.startsWith("image/png")) return { kind: "image", contentType: "image/png" };
|
|
840
|
+
if (lower.startsWith("image/webp")) return { kind: "image", contentType: "image/webp" };
|
|
841
|
+
if (lower.startsWith("video/mp4")) return { kind: "video", contentType: "video/mp4" };
|
|
842
|
+
if (lower.startsWith("video/quicktime")) return { kind: "video", contentType: "video/quicktime" };
|
|
843
|
+
if (lower.startsWith("image/bmp")) return { kind: "image", contentType: "image/bmp" };
|
|
844
|
+
if (lower.startsWith("image/tiff")) return { kind: "image", contentType: "image/tiff" };
|
|
845
|
+
if (lower.startsWith("image/gif")) return { kind: "image", contentType: "image/gif" };
|
|
846
|
+
if (lower.startsWith("image/heic")) return { kind: "image", contentType: "image/heic" };
|
|
847
|
+
if (lower.startsWith("image/heif")) return { kind: "image", contentType: "image/heif" };
|
|
848
|
+
return undefined;
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
function mediaRoleFromOption(optionName, kind) {
|
|
852
|
+
if (optionName === "first_frame" || optionName === "first_frame_url") return "first_frame";
|
|
853
|
+
if (optionName === "last_frame" || optionName === "last_frame_url") return "last_frame";
|
|
854
|
+
if (kind === "video") return "reference_video";
|
|
855
|
+
return "reference_image";
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
function allowedMediaKindsForOption(commandGroup, optionName) {
|
|
859
|
+
if (commandGroup === "image") return new Set(["image"]);
|
|
860
|
+
if (optionName === "file") return new Set(["image", "video"]);
|
|
861
|
+
if (optionName === "video_url") return new Set(["video"]);
|
|
862
|
+
if (optionName === "first_frame" || optionName === "first_frame_url" || optionName === "last_frame" || optionName === "last_frame_url" || optionName === "image" || optionName === "image_url") {
|
|
863
|
+
return new Set(["image"]);
|
|
864
|
+
}
|
|
865
|
+
return new Set(["image"]);
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
function headerValue(headers, name) {
|
|
869
|
+
if (!headers) return undefined;
|
|
870
|
+
if (typeof headers.get === "function") {
|
|
871
|
+
return headers.get(name) ?? headers.get(name.toLowerCase());
|
|
872
|
+
}
|
|
873
|
+
return headers[name] ?? headers[name.toLowerCase()];
|
|
874
|
+
}
|
|
875
|
+
|
|
622
876
|
async function waitForVideoResult(response, options) {
|
|
623
877
|
if (hasArtifact(response)) return response;
|
|
624
878
|
const taskId = videoTaskId(response);
|
|
@@ -715,6 +969,29 @@ function scrubArtifactResponse(value) {
|
|
|
715
969
|
);
|
|
716
970
|
}
|
|
717
971
|
|
|
972
|
+
function scrubVideoCopy(value) {
|
|
973
|
+
if (!value) return undefined;
|
|
974
|
+
return {
|
|
975
|
+
source: value.source,
|
|
976
|
+
depthPath: value.depthPath,
|
|
977
|
+
referencePaths: value.referencePaths,
|
|
978
|
+
duration: value.duration,
|
|
979
|
+
sourceInfo: value.sourceInfo,
|
|
980
|
+
helper: value.helper,
|
|
981
|
+
};
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
function isDepthVideoFetchFailure(error) {
|
|
985
|
+
const code = String(error?.code ?? "").toLowerCase();
|
|
986
|
+
const message = String(error?.message ?? error).toLowerCase();
|
|
987
|
+
return code.includes("fail_to_fetch")
|
|
988
|
+
|| code.includes("video_fetch")
|
|
989
|
+
|| message.includes("fail_to_fetch")
|
|
990
|
+
|| message.includes("failed to fetch")
|
|
991
|
+
|| message.includes("unable to fetch")
|
|
992
|
+
|| message.includes("video reference");
|
|
993
|
+
}
|
|
994
|
+
|
|
718
995
|
async function handleVoices(command, deps) {
|
|
719
996
|
const { resolveApiKey, resolveOrigins } = await import("./config.js");
|
|
720
997
|
const { getVoices } = await import("./api.js");
|