craterview 0.3.12 → 0.3.13

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 +50 -5
  3. package/package.json +4 -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.13";
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.
@@ -485,6 +489,23 @@ export interface CraterViewOptions {
485
489
  baseUrl?: string;
486
490
  }
487
491
 
492
+ /**
493
+ * Width × height ÷ 10⁶ from the file's header, or `undefined` for a file the reader does not
494
+ * know. Reads the first megabyte only — every format's dimensions sit near the front, and a
495
+ * Blob of any size costs one bounded copy — and never throws: a size the client cannot read
496
+ * is simply not declared.
497
+ */
498
+ async function megapixelsOf(blob: Blob): Promise<number | undefined> {
499
+ try {
500
+ const head = new Uint8Array(await blob.slice(0, 1 << 20).arrayBuffer());
501
+ const { width, height } = imageSize(head);
502
+ if (!width || !height) return undefined;
503
+ return Math.round((width * height) / 1e6 * 1e4) / 1e4 || undefined;
504
+ } catch {
505
+ return undefined;
506
+ }
507
+ }
508
+
488
509
  export interface SubmitOptions {
489
510
  wait?: number;
490
511
  idempotencyKey?: string;
@@ -500,6 +521,14 @@ export interface SubmitOptions {
500
521
  * becomes a model parameter, is checked against the model's schema, and would be refused.
501
522
  */
502
523
  customId?: string;
524
+ /**
525
+ * The size of the file you uploaded, in megapixels (width × height ÷ 1,000,000). Used
526
+ * only to estimate `eta_seconds` for your image rather than for a typical one — it does
527
+ * not affect the price, the queue or whether the job is accepted, all of which read the
528
+ * file itself. Left unset, the size `upload()` read from the file is sent for a key this
529
+ * client uploaded; set it to override or to supply one for a key uploaded elsewhere.
530
+ */
531
+ inputMegapixels?: number;
503
532
  model?: string;
504
533
  [param: string]: unknown;
505
534
  }
@@ -507,6 +536,10 @@ export interface SubmitOptions {
507
536
  export class CraterView {
508
537
  private readonly baseUrl: string;
509
538
  private readonly headers: Record<string, string>;
539
+ // What `upload()` read from each file's header, by the key it came back with, so a later
540
+ // `submit()` of that key declares the size without the caller carrying it. Bounded: a
541
+ // long-lived client uploading thousands of files keeps the most recent few hundred.
542
+ private readonly sizes = new Map<string, number>();
510
543
 
511
544
  constructor(options: CraterViewOptions = {}) {
512
545
  this.baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/$/, "");
@@ -564,6 +597,11 @@ export class CraterView {
564
597
  /**
565
598
  * Put an image in storage and return its key. Bytes go straight to object storage on a
566
599
  * presigned URL, never through the API.
600
+ *
601
+ * The image's size is read from its header on the way past and remembered against the
602
+ * key, so `submit()` can tell the API what you sent and `eta_seconds` is estimated for
603
+ * your image rather than a typical one. A file the reader does not know is uploaded just
604
+ * the same; only the estimate is less specific.
567
605
  */
568
606
  async upload(image: Blob | ArrayBuffer | Uint8Array, contentType?: string): Promise<string> {
569
607
  const blob = image instanceof Blob
@@ -581,6 +619,11 @@ export class CraterView {
581
619
  method: "PUT", body: blob, headers: { "Content-Type": type },
582
620
  });
583
621
  if (!put.ok) throw new CraterViewError(`upload failed: ${put.status}`);
622
+ const megapixels = await megapixelsOf(blob);
623
+ if (megapixels) {
624
+ if (this.sizes.size >= 512) this.sizes.delete(this.sizes.keys().next().value!);
625
+ this.sizes.set(slot.input_key, megapixels);
626
+ }
584
627
  return slot.input_key;
585
628
  }
586
629
 
@@ -592,8 +635,9 @@ export class CraterView {
592
635
  * job and is billed for both.
593
636
  */
594
637
  async submit(inputKey: string, options: SubmitOptions = {}): Promise<Job> {
595
- const { wait = 0, idempotencyKey, webhookUrl, customId, model = "cv-enhance-v3",
596
- ...params } = options;
638
+ const { wait = 0, idempotencyKey, webhookUrl, customId, inputMegapixels,
639
+ model = "cv-enhance-v3", ...params } = options;
640
+ const declared = inputMegapixels ?? this.sizes.get(inputKey);
597
641
  // Annotated, not inferred: without this the two branches unify to a type whose key
598
642
  // may be `undefined`, which is not assignable to Record<string, string>.
599
643
  const headers: Record<string, string> = idempotencyKey
@@ -603,6 +647,7 @@ export class CraterView {
603
647
  model, input_key: inputKey, params,
604
648
  ...(webhookUrl ? { webhook_url: webhookUrl } : {}),
605
649
  ...(customId ? { custom_id: customId } : {}),
650
+ ...(declared && declared > 0 ? { input_megapixels: declared } : {}),
606
651
  }, headers);
607
652
  return new Job(data);
608
653
  }
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.13",
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",