@blinkdotnew/cli 0.1.3 → 0.1.4
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/dist/cli.js +28 -4
- package/package.json +1 -1
- package/src/commands/ai.ts +32 -4
package/dist/cli.js
CHANGED
|
@@ -199,6 +199,22 @@ async function withSpinner(label, fn) {
|
|
|
199
199
|
|
|
200
200
|
// src/commands/ai.ts
|
|
201
201
|
import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, existsSync as existsSync2 } from "fs";
|
|
202
|
+
function extractImageUrls(result) {
|
|
203
|
+
const r = result;
|
|
204
|
+
const data = r?.result?.data;
|
|
205
|
+
if (Array.isArray(data)) return data.map((d) => d.url).filter(Boolean);
|
|
206
|
+
if (Array.isArray(r?.urls)) return r.urls;
|
|
207
|
+
if (r?.url) return [r.url];
|
|
208
|
+
return [];
|
|
209
|
+
}
|
|
210
|
+
function extractVideoUrl(result) {
|
|
211
|
+
const r = result;
|
|
212
|
+
const inner = r?.result;
|
|
213
|
+
if (inner?.url) return inner.url;
|
|
214
|
+
if (inner?.video_url) return inner.video_url;
|
|
215
|
+
if (inner?.video?.url) return inner.video.url;
|
|
216
|
+
return r?.url ?? r?.video_url ?? "";
|
|
217
|
+
}
|
|
202
218
|
function registerAiCommands(program2) {
|
|
203
219
|
const ai = program2.command("ai").description("AI generation \u2014 image, video, text, speech, transcription").addHelpText("after", `
|
|
204
220
|
Commands:
|
|
@@ -270,7 +286,11 @@ To edit an existing image, use: blink ai image-edit
|
|
|
270
286
|
})
|
|
271
287
|
);
|
|
272
288
|
if (isJsonMode()) return printJson(result);
|
|
273
|
-
const urls =
|
|
289
|
+
const urls = extractImageUrls(result);
|
|
290
|
+
if (!urls.length) {
|
|
291
|
+
console.log("No image URL in response");
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
274
294
|
for (const url of urls) {
|
|
275
295
|
if (opts.output && urls.length === 1) {
|
|
276
296
|
const res = await fetch(url);
|
|
@@ -296,7 +316,11 @@ Examples:
|
|
|
296
316
|
})
|
|
297
317
|
);
|
|
298
318
|
if (isJsonMode()) return printJson(result);
|
|
299
|
-
const url = result
|
|
319
|
+
const url = extractImageUrls(result)[0];
|
|
320
|
+
if (!url) {
|
|
321
|
+
console.log("No image URL in response");
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
300
324
|
if (opts.output) {
|
|
301
325
|
const res = await fetch(url);
|
|
302
326
|
writeFileSync2(opts.output, Buffer.from(await res.arrayBuffer()));
|
|
@@ -323,7 +347,7 @@ To animate an existing image, use: blink ai animate
|
|
|
323
347
|
})
|
|
324
348
|
);
|
|
325
349
|
if (isJsonMode()) return printJson(result);
|
|
326
|
-
printUrl("Video", result
|
|
350
|
+
printUrl("Video", extractVideoUrl(result));
|
|
327
351
|
});
|
|
328
352
|
ai.command("animate <prompt> <image-source>").description("Animate an image into video (accepts URL or local file path)").option("--model <model>", "Model (fal-ai/veo3.1/fast/image-to-video | fal-ai/sora-2/image-to-video/pro)", "fal-ai/veo3.1/fast/image-to-video").option("--duration <dur>", "Duration (e.g. 5s, 10s)", "5s").addHelpText("after", `
|
|
329
353
|
Examples:
|
|
@@ -355,7 +379,7 @@ Local files are automatically uploaded before animating.
|
|
|
355
379
|
})
|
|
356
380
|
);
|
|
357
381
|
if (isJsonMode()) return printJson(result);
|
|
358
|
-
printUrl("Video", result
|
|
382
|
+
printUrl("Video", extractVideoUrl(result));
|
|
359
383
|
});
|
|
360
384
|
ai.command("speech <text>").description("Convert text to speech and save as MP3").option("--voice <voice>", "Voice: alloy | echo | fable | onyx | nova | shimmer", "alloy").option("--model <model>", "TTS model", "tts-1").option("--output <file>", "Output file path", "speech.mp3").addHelpText("after", `
|
|
361
385
|
Examples:
|
package/package.json
CHANGED
package/src/commands/ai.ts
CHANGED
|
@@ -4,6 +4,32 @@ import { requireToken } from '../lib/auth.js'
|
|
|
4
4
|
import { printJson, printUrl, isJsonMode, withSpinner } from '../lib/output.js'
|
|
5
5
|
import { readFileSync, writeFileSync, existsSync } from 'node:fs'
|
|
6
6
|
|
|
7
|
+
// Response shape from blink-apis /api/v1/ai/* is:
|
|
8
|
+
// { result: { data: [{url}] } } for images
|
|
9
|
+
// { result: { url | video_url } } for video
|
|
10
|
+
// Helpers to extract the URL regardless of nesting
|
|
11
|
+
function extractImageUrls(result: unknown): string[] {
|
|
12
|
+
const r = result as Record<string, unknown>
|
|
13
|
+
// nested: result.result.data[].url
|
|
14
|
+
const data = (r?.result as Record<string, unknown>)?.data
|
|
15
|
+
if (Array.isArray(data)) return data.map((d: Record<string, unknown>) => d.url as string).filter(Boolean)
|
|
16
|
+
// flat: result.urls[] or result.url
|
|
17
|
+
if (Array.isArray(r?.urls)) return r.urls as string[]
|
|
18
|
+
if (r?.url) return [r.url as string]
|
|
19
|
+
return []
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function extractVideoUrl(result: unknown): string {
|
|
23
|
+
const r = result as Record<string, unknown>
|
|
24
|
+
// nested: result.result.url or result.result.video_url or result.result.video.url
|
|
25
|
+
const inner = r?.result as Record<string, unknown>
|
|
26
|
+
if (inner?.url) return inner.url as string
|
|
27
|
+
if (inner?.video_url) return inner.video_url as string
|
|
28
|
+
if ((inner?.video as Record<string, unknown>)?.url) return (inner.video as Record<string, unknown>).url as string
|
|
29
|
+
// flat fallback
|
|
30
|
+
return (r?.url ?? r?.video_url ?? '') as string
|
|
31
|
+
}
|
|
32
|
+
|
|
7
33
|
export function registerAiCommands(program: Command) {
|
|
8
34
|
const ai = program.command('ai')
|
|
9
35
|
.description('AI generation — image, video, text, speech, transcription')
|
|
@@ -88,7 +114,8 @@ To edit an existing image, use: blink ai image-edit
|
|
|
88
114
|
})
|
|
89
115
|
)
|
|
90
116
|
if (isJsonMode()) return printJson(result)
|
|
91
|
-
const urls
|
|
117
|
+
const urls = extractImageUrls(result)
|
|
118
|
+
if (!urls.length) { console.log('No image URL in response'); return }
|
|
92
119
|
for (const url of urls) {
|
|
93
120
|
if (opts.output && urls.length === 1) {
|
|
94
121
|
const res = await fetch(url)
|
|
@@ -119,7 +146,8 @@ Examples:
|
|
|
119
146
|
})
|
|
120
147
|
)
|
|
121
148
|
if (isJsonMode()) return printJson(result)
|
|
122
|
-
const url = result
|
|
149
|
+
const url = extractImageUrls(result)[0]
|
|
150
|
+
if (!url) { console.log('No image URL in response'); return }
|
|
123
151
|
if (opts.output) {
|
|
124
152
|
const res = await fetch(url)
|
|
125
153
|
writeFileSync(opts.output, Buffer.from(await res.arrayBuffer()))
|
|
@@ -153,7 +181,7 @@ To animate an existing image, use: blink ai animate
|
|
|
153
181
|
})
|
|
154
182
|
)
|
|
155
183
|
if (isJsonMode()) return printJson(result)
|
|
156
|
-
printUrl('Video', result
|
|
184
|
+
printUrl('Video', extractVideoUrl(result))
|
|
157
185
|
})
|
|
158
186
|
|
|
159
187
|
ai.command('animate <prompt> <image-source>')
|
|
@@ -189,7 +217,7 @@ Local files are automatically uploaded before animating.
|
|
|
189
217
|
})
|
|
190
218
|
)
|
|
191
219
|
if (isJsonMode()) return printJson(result)
|
|
192
|
-
printUrl('Video', result
|
|
220
|
+
printUrl('Video', extractVideoUrl(result))
|
|
193
221
|
})
|
|
194
222
|
|
|
195
223
|
ai.command('speech <text>')
|