craterview 0.3.12 → 0.3.16
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 +6 -4
- package/index.ts +75 -5
- package/package.json +5 -2
package/README.md
CHANGED
|
@@ -13,9 +13,11 @@ compiles this too: any bundler, `tsx` or `ts-node`, Bun, Deno, or Node 22.6 and
|
|
|
13
13
|
type stripping (`--experimental-strip-types`, which recent releases enable by default). A
|
|
14
14
|
plain JavaScript project invoking `node` directly cannot import it.
|
|
15
15
|
|
|
16
|
-
The code itself needs Node 18 or newer, or any modern browser. **
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
The code itself needs Node 18 or newer, or any modern browser. **One runtime dependency**,
|
|
17
|
+
[`image-size`](https://www.npmjs.com/package/image-size), pure JavaScript that runs in
|
|
18
|
+
both: it reads an image's dimensions from its header — nothing is decoded — so `upload()`
|
|
19
|
+
can tell the API how big your file is and `etaSeconds` is estimated for that file rather
|
|
20
|
+
than for a typical one. `fetch`, `Blob` and `crypto.getRandomValues` are standard in both.
|
|
19
21
|
|
|
20
22
|
## Quickstart
|
|
21
23
|
|
|
@@ -150,7 +152,7 @@ Every field the API publishes on a job is exposed here.
|
|
|
150
152
|
| `error` | Set when the job failed. Safe to show a user |
|
|
151
153
|
| `errorCode` | The same fact, as a stable identifier. Branch on this, show the other |
|
|
152
154
|
| `credits` | **What you were billed** |
|
|
153
|
-
| `etaSeconds` | Seconds until the job is expected to finish, recomputed on every read — it counts down while the job runs. Absent once the job has settled |
|
|
155
|
+
| `etaSeconds` | Seconds until the job is expected to finish, recomputed on every read — it counts down while the job runs. Absent once the job has settled. Estimated for your image's size when `upload()` could read it (or when you pass `inputMegapixels` to `submit()`), for a typical image otherwise |
|
|
154
156
|
| `community` | True when the job is on the community queue: served after priority work, always taking a share of it, so it never stalls behind paid work |
|
|
155
157
|
| `outputUrl`, `downloadUrl` | The result, presigned. One to display, one to save |
|
|
156
158
|
| `thumbUrl` | A small JPEG of the result, for listings. Null when none was drawn |
|
package/index.ts
CHANGED
|
@@ -7,14 +7,18 @@
|
|
|
7
7
|
* const job = await cv.run(file, { style: "photo" });
|
|
8
8
|
* const blob = await job.blob();
|
|
9
9
|
*
|
|
10
|
-
*
|
|
11
|
-
* client
|
|
10
|
+
* One dependency: `image-size`, which reads an image's dimensions from its header — no
|
|
11
|
+
* decoding — so the client can tell the API how big the file you sent is and get a wait
|
|
12
|
+
* estimated for that file rather than a typical one. fetch and Blob are standard in Node 18+
|
|
13
|
+
* and every browser, and `image-size` runs in both, so the client stays installable anywhere.
|
|
12
14
|
*/
|
|
13
15
|
|
|
16
|
+
import { imageSize } from "image-size";
|
|
17
|
+
|
|
14
18
|
// Mirrored from package.json, which is the number a release bumps. It cannot be imported
|
|
15
19
|
// from there — this ships as TypeScript, so the import would have to resolve in the
|
|
16
20
|
// consumer's toolchain — so test/version.test.ts asserts the two agree.
|
|
17
|
-
export const VERSION = "0.3.
|
|
21
|
+
export const VERSION = "0.3.16";
|
|
18
22
|
const DEFAULT_BASE_URL = "https://api.craterview.ai";
|
|
19
23
|
// The server rejects a longer wait outright, so asking for one costs a 422 rather than the
|
|
20
24
|
// wait you asked for. `run()` clamps to this rather than letting that happen.
|
|
@@ -327,6 +331,13 @@ export interface WebhookEvent extends Required<JobData> {}
|
|
|
327
331
|
|
|
328
332
|
export type JobStatus = "queued" | "running" | "succeeded" | "failed";
|
|
329
333
|
|
|
334
|
+
/** A job's standing with the public gallery: `pending` under review, `approved` once shown. */
|
|
335
|
+
export interface GalleryState {
|
|
336
|
+
status: "pending" | "approved";
|
|
337
|
+
/** The submission's id, which is what withdrawing it takes. */
|
|
338
|
+
post_id: string;
|
|
339
|
+
}
|
|
340
|
+
|
|
330
341
|
/** The API's job representation, exactly as it arrives. Snake case is the wire's. */
|
|
331
342
|
export interface JobData {
|
|
332
343
|
id: string;
|
|
@@ -362,6 +373,18 @@ export interface JobData {
|
|
|
362
373
|
eta_seconds?: number | null;
|
|
363
374
|
/** Retained past the ordinary expiry because its owner asked, links and all. */
|
|
364
375
|
kept?: boolean;
|
|
376
|
+
/**
|
|
377
|
+
* Whether the kept copy includes the image you sent as well as the result. It does when
|
|
378
|
+
* you kept the job while the original was still there; false when only the result is
|
|
379
|
+
* kept, and when nothing is.
|
|
380
|
+
*/
|
|
381
|
+
kept_original?: boolean;
|
|
382
|
+
/**
|
|
383
|
+
* Where this job stands with the public gallery, when you have offered it: present while
|
|
384
|
+
* it is being reviewed or shown, absent or null when it is not offered — including after
|
|
385
|
+
* you withdraw it.
|
|
386
|
+
*/
|
|
387
|
+
gallery?: GalleryState | null;
|
|
365
388
|
community?: boolean;
|
|
366
389
|
/**
|
|
367
390
|
* Whether an automated check thought this image may fall outside what the service
|
|
@@ -423,6 +446,12 @@ export class Job {
|
|
|
423
446
|
// checked, and `false` would say it was checked and cleared. The two are different
|
|
424
447
|
// answers and only one of them is true.
|
|
425
448
|
get flagged() { return this.data.flagged ?? null; }
|
|
449
|
+
/** Retained past the ordinary expiry because you asked. */
|
|
450
|
+
get kept() { return this.data.kept ?? false; }
|
|
451
|
+
/** Whether the kept copy holds the image you sent as well as the result. */
|
|
452
|
+
get keptOriginal() { return this.data.kept_original ?? false; }
|
|
453
|
+
/** Where this job stands with the public gallery, or null when it is not offered. */
|
|
454
|
+
get gallery() { return this.data.gallery ?? null; }
|
|
426
455
|
/** The whole answer, including where the file is when there is one. */
|
|
427
456
|
get result() { return this.data.result ?? null; }
|
|
428
457
|
/**
|
|
@@ -485,6 +514,23 @@ export interface CraterViewOptions {
|
|
|
485
514
|
baseUrl?: string;
|
|
486
515
|
}
|
|
487
516
|
|
|
517
|
+
/**
|
|
518
|
+
* Width × height ÷ 10⁶ from the file's header, or `undefined` for a file the reader does not
|
|
519
|
+
* know. Reads the first megabyte only — every format's dimensions sit near the front, and a
|
|
520
|
+
* Blob of any size costs one bounded copy — and never throws: a size the client cannot read
|
|
521
|
+
* is simply not declared.
|
|
522
|
+
*/
|
|
523
|
+
async function megapixelsOf(blob: Blob): Promise<number | undefined> {
|
|
524
|
+
try {
|
|
525
|
+
const head = new Uint8Array(await blob.slice(0, 1 << 20).arrayBuffer());
|
|
526
|
+
const { width, height } = imageSize(head);
|
|
527
|
+
if (!width || !height) return undefined;
|
|
528
|
+
return Math.round((width * height) / 1e6 * 1e4) / 1e4 || undefined;
|
|
529
|
+
} catch {
|
|
530
|
+
return undefined;
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
488
534
|
export interface SubmitOptions {
|
|
489
535
|
wait?: number;
|
|
490
536
|
idempotencyKey?: string;
|
|
@@ -500,6 +546,14 @@ export interface SubmitOptions {
|
|
|
500
546
|
* becomes a model parameter, is checked against the model's schema, and would be refused.
|
|
501
547
|
*/
|
|
502
548
|
customId?: string;
|
|
549
|
+
/**
|
|
550
|
+
* The size of the file you uploaded, in megapixels (width × height ÷ 1,000,000). Used
|
|
551
|
+
* only to estimate `eta_seconds` for your image rather than for a typical one — it does
|
|
552
|
+
* not affect the price, the queue or whether the job is accepted, all of which read the
|
|
553
|
+
* file itself. Left unset, the size `upload()` read from the file is sent for a key this
|
|
554
|
+
* client uploaded; set it to override or to supply one for a key uploaded elsewhere.
|
|
555
|
+
*/
|
|
556
|
+
inputMegapixels?: number;
|
|
503
557
|
model?: string;
|
|
504
558
|
[param: string]: unknown;
|
|
505
559
|
}
|
|
@@ -507,6 +561,10 @@ export interface SubmitOptions {
|
|
|
507
561
|
export class CraterView {
|
|
508
562
|
private readonly baseUrl: string;
|
|
509
563
|
private readonly headers: Record<string, string>;
|
|
564
|
+
// What `upload()` read from each file's header, by the key it came back with, so a later
|
|
565
|
+
// `submit()` of that key declares the size without the caller carrying it. Bounded: a
|
|
566
|
+
// long-lived client uploading thousands of files keeps the most recent few hundred.
|
|
567
|
+
private readonly sizes = new Map<string, number>();
|
|
510
568
|
|
|
511
569
|
constructor(options: CraterViewOptions = {}) {
|
|
512
570
|
this.baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/$/, "");
|
|
@@ -564,6 +622,11 @@ export class CraterView {
|
|
|
564
622
|
/**
|
|
565
623
|
* Put an image in storage and return its key. Bytes go straight to object storage on a
|
|
566
624
|
* presigned URL, never through the API.
|
|
625
|
+
*
|
|
626
|
+
* The image's size is read from its header on the way past and remembered against the
|
|
627
|
+
* key, so `submit()` can tell the API what you sent and `eta_seconds` is estimated for
|
|
628
|
+
* your image rather than a typical one. A file the reader does not know is uploaded just
|
|
629
|
+
* the same; only the estimate is less specific.
|
|
567
630
|
*/
|
|
568
631
|
async upload(image: Blob | ArrayBuffer | Uint8Array, contentType?: string): Promise<string> {
|
|
569
632
|
const blob = image instanceof Blob
|
|
@@ -581,6 +644,11 @@ export class CraterView {
|
|
|
581
644
|
method: "PUT", body: blob, headers: { "Content-Type": type },
|
|
582
645
|
});
|
|
583
646
|
if (!put.ok) throw new CraterViewError(`upload failed: ${put.status}`);
|
|
647
|
+
const megapixels = await megapixelsOf(blob);
|
|
648
|
+
if (megapixels) {
|
|
649
|
+
if (this.sizes.size >= 512) this.sizes.delete(this.sizes.keys().next().value!);
|
|
650
|
+
this.sizes.set(slot.input_key, megapixels);
|
|
651
|
+
}
|
|
584
652
|
return slot.input_key;
|
|
585
653
|
}
|
|
586
654
|
|
|
@@ -592,8 +660,9 @@ export class CraterView {
|
|
|
592
660
|
* job and is billed for both.
|
|
593
661
|
*/
|
|
594
662
|
async submit(inputKey: string, options: SubmitOptions = {}): Promise<Job> {
|
|
595
|
-
const { wait = 0, idempotencyKey, webhookUrl, customId,
|
|
596
|
-
...params } = options;
|
|
663
|
+
const { wait = 0, idempotencyKey, webhookUrl, customId, inputMegapixels,
|
|
664
|
+
model = "cv-enhance-v3", ...params } = options;
|
|
665
|
+
const declared = inputMegapixels ?? this.sizes.get(inputKey);
|
|
597
666
|
// Annotated, not inferred: without this the two branches unify to a type whose key
|
|
598
667
|
// may be `undefined`, which is not assignable to Record<string, string>.
|
|
599
668
|
const headers: Record<string, string> = idempotencyKey
|
|
@@ -603,6 +672,7 @@ export class CraterView {
|
|
|
603
672
|
model, input_key: inputKey, params,
|
|
604
673
|
...(webhookUrl ? { webhook_url: webhookUrl } : {}),
|
|
605
674
|
...(customId ? { custom_id: customId } : {}),
|
|
675
|
+
...(declared && declared > 0 ? { input_megapixels: declared } : {}),
|
|
606
676
|
}, headers);
|
|
607
677
|
return new Job(data);
|
|
608
678
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "craterview",
|
|
3
|
-
"version": "0.3.
|
|
4
|
-
"dependencies": {
|
|
3
|
+
"version": "0.3.16",
|
|
4
|
+
"dependencies": {
|
|
5
|
+
"image-size": "^2.0.4"
|
|
6
|
+
},
|
|
5
7
|
"description": "TypeScript client for the CraterView image enhancement API",
|
|
6
8
|
"type": "module",
|
|
7
9
|
"main": "index.ts",
|
|
@@ -24,6 +26,7 @@
|
|
|
24
26
|
},
|
|
25
27
|
"devDependencies": {
|
|
26
28
|
"@types/node": "^26.2.0",
|
|
29
|
+
"esbuild": "^0.25.0",
|
|
27
30
|
"typescript": "^7.0.2",
|
|
28
31
|
"vitest": "^4.1.11"
|
|
29
32
|
}
|