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.
Files changed (3) hide show
  1. package/README.md +6 -4
  2. package/index.ts +75 -5
  3. 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. **Zero runtime
17
- dependencies** — `fetch`, `Blob` and `crypto.getRandomValues` are standard in both, so the
18
- client installs without dragging a transitive tree behind it.
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
- * Zero dependencies: fetch and Blob are standard in Node 18+ and every browser, so the
11
- * client stays installable anywhere without dragging a transitive tree behind it.
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.12";
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, model = "cv-enhance-v3",
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.12",
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
  }