@wavespeed/cli 0.4.26 → 0.4.27

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.
@@ -2,6 +2,7 @@ import chalk from 'chalk';
2
2
  import ora from 'ora';
3
3
  import { fetchHistory, PREDICTION_STATUSES } from '../lib/api.js';
4
4
  import { toIso } from '../lib/time.js';
5
+ import { describeOutput } from '../lib/output.js';
5
6
  function statusBadge(status) {
6
7
  const map = {
7
8
  completed: chalk.bgGreen.black,
@@ -40,7 +41,7 @@ export function beyondWindow(since, until) {
40
41
  function firstOutput(item) {
41
42
  const out = item.outputs?.[0];
42
43
  if (typeof out === 'string')
43
- return out;
44
+ return describeOutput(out);
44
45
  return '';
45
46
  }
46
47
  export function registerHistory(program) {
@@ -11,6 +11,73 @@ function extFromUrl(url, fallback) {
11
11
  return m[1].toLowerCase();
12
12
  return fallback;
13
13
  }
14
+ // With enable_base64_output=true each URL output comes back as naked base64,
15
+ // but an output over 64 MiB or one the API failed to fetch stays a URL, and
16
+ // text/JSON outputs are unchanged — so the kind is decided per output.
17
+ const BASE64_RE = /^[A-Za-z0-9+/]+={0,2}$/;
18
+ const DATA_URI_RE = /^data:([^;,]*)(?:;[^,]*)?;base64,(.*)$/s;
19
+ export function isHttpUrl(value) {
20
+ return /^https?:\/\//i.test(value);
21
+ }
22
+ /** Naked base64 as returned for enable_base64_output (short strings are treated as text). */
23
+ export function isBase64Output(value) {
24
+ return value.length >= 64 && value.length % 4 === 0 && BASE64_RE.test(value);
25
+ }
26
+ const MAGIC = [
27
+ [[0x89, 0x50, 0x4e, 0x47], 'png'],
28
+ [[0xff, 0xd8, 0xff], 'jpg'],
29
+ [[0x47, 0x49, 0x46, 0x38], 'gif'],
30
+ [[0x1a, 0x45, 0xdf, 0xa3], 'webm'],
31
+ [[0x49, 0x44, 0x33], 'mp3'],
32
+ [[0x66, 0x4c, 0x61, 0x43], 'flac'],
33
+ [[0x4f, 0x67, 0x67, 0x53], 'ogg'],
34
+ [[0x67, 0x6c, 0x54, 0x46], 'glb'],
35
+ [[0x50, 0x4b, 0x03, 0x04], 'zip'],
36
+ [[0x25, 0x50, 0x44, 0x46], 'pdf'],
37
+ ];
38
+ export function extFromBytes(buf, fallback) {
39
+ for (const [sig, ext] of MAGIC) {
40
+ if (sig.every((b, i) => buf[i] === b))
41
+ return ext;
42
+ }
43
+ const at = (o, s) => buf.subarray(o, o + s.length).toString('latin1') === s;
44
+ if (at(0, 'RIFF') && at(8, 'WEBP'))
45
+ return 'webp';
46
+ if (at(0, 'RIFF') && at(8, 'WAVE'))
47
+ return 'wav';
48
+ if (at(4, 'ftyp'))
49
+ return at(8, 'M4A') ? 'm4a' : at(8, 'qt') ? 'mov' : 'mp4';
50
+ if (buf.length >= 2 && buf[0] === 0xff && (buf[1] & 0xe0) === 0xe0)
51
+ return 'mp3';
52
+ return fallback;
53
+ }
54
+ const EXT_FROM_MIME = {
55
+ 'image/jpeg': 'jpg', 'image/png': 'png', 'image/webp': 'webp', 'image/gif': 'gif',
56
+ 'video/mp4': 'mp4', 'video/webm': 'webm', 'audio/mpeg': 'mp3', 'audio/wav': 'wav',
57
+ };
58
+ /** Bytes of an inline (base64 or data URI) output, or null when it is not inline. */
59
+ function inlineBytes(value, fallbackExt) {
60
+ const m = value.match(DATA_URI_RE);
61
+ if (m) {
62
+ const buf = Buffer.from(m[2], 'base64');
63
+ return { buf, ext: EXT_FROM_MIME[m[1].toLowerCase()] ?? extFromBytes(buf, fallbackExt) };
64
+ }
65
+ if (!isBase64Output(value))
66
+ return null;
67
+ const buf = Buffer.from(value, 'base64');
68
+ return { buf, ext: extFromBytes(buf, fallbackExt) };
69
+ }
70
+ /** Short label for an output: URLs as-is, inline data summarised instead of dumped. */
71
+ export function describeOutput(value) {
72
+ if (isHttpUrl(value))
73
+ return value;
74
+ const m = value.match(DATA_URI_RE);
75
+ const b64 = m ? m[2] : isBase64Output(value) ? value : null;
76
+ if (b64 === null)
77
+ return value;
78
+ const bytes = Math.floor((b64.length * 3) / 4);
79
+ return `(base64 data, ${bytes >= 1024 * 1024 ? (bytes / 1024 / 1024).toFixed(1) + ' MB' : Math.ceil(bytes / 1024) + ' KB'})`;
80
+ }
14
81
  function slug(input) {
15
82
  return input
16
83
  .toLowerCase()
@@ -31,7 +98,8 @@ export async function downloadOutputs(urls, opts = {}) {
31
98
  const saved = [];
32
99
  for (let i = 0; i < urls.length; i++) {
33
100
  const url = urls[i];
34
- const ext = extFromUrl(url, opts.ext || 'bin');
101
+ const inline = isHttpUrl(url) ? null : inlineBytes(url, opts.ext || 'bin');
102
+ const ext = inline ? inline.ext : extFromUrl(url, opts.ext || 'bin');
35
103
  let filepath;
36
104
  if (opts.template) {
37
105
  const rendered = renderTemplate(opts.template, {
@@ -53,6 +121,14 @@ export async function downloadOutputs(urls, opts = {}) {
53
121
  filepath = path.join(baseDir, `${ts}-${tag}${suffix}.${ext}`);
54
122
  }
55
123
  fs.mkdirSync(path.dirname(filepath), { recursive: true });
124
+ if (inline) {
125
+ fs.writeFileSync(filepath, inline.buf);
126
+ saved.push(filepath);
127
+ continue;
128
+ }
129
+ if (!isHttpUrl(url)) {
130
+ throw new Error(`Output ${i + 1} is neither a URL nor base64 data, so it cannot be downloaded: ${url.slice(0, 80)}`);
131
+ }
56
132
  const res = await fetch(url);
57
133
  if (!res.ok || !res.body) {
58
134
  throw new Error(`Download failed for ${url}: ${res.status} ${res.statusText}`);
@@ -72,6 +148,6 @@ export function printOutputs(saved, remote) {
72
148
  }
73
149
  console.log(chalk.gray(saved.length ? 'Source URLs:' : 'URLs:'));
74
150
  for (const u of remote)
75
- console.log(' ' + chalk.cyan(u));
151
+ console.log(' ' + chalk.cyan(describeOutput(u)));
76
152
  console.log();
77
153
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wavespeed/cli",
3
- "version": "0.4.26",
3
+ "version": "0.4.27",
4
4
  "description": "WaveSpeed CLI — one command to run any WaveSpeed AI model from your terminal.",
5
5
  "publishConfig": {
6
6
  "access": "public"