craterview 0.3.13 → 0.3.18

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 +25 -13
  2. package/index.ts +96 -27
  3. package/package.json +3 -1
package/README.md CHANGED
@@ -8,10 +8,19 @@ npm install craterview
8
8
  ```
9
9
 
10
10
  **The package ships TypeScript source.** `index.ts` is what npm installs — there is no build
11
- step and no compiled JavaScript in the tarball — so whatever compiles your own TypeScript
12
- compiles this too: any bundler, `tsx` or `ts-node`, Bun, Deno, or Node 22.6 and newer with
13
- type stripping (`--experimental-strip-types`, which recent releases enable by default). A
14
- plain JavaScript project invoking `node` directly cannot import it.
11
+ step and no compiled JavaScript in the tarball — so it needs a toolchain that compiles
12
+ TypeScript inside `node_modules`. Two that do, and are tested against the published package:
13
+
14
+ ```bash
15
+ npx tsx main.ts # run a script directly
16
+
17
+ npx esbuild main.js --bundle --platform=node --format=esm --outfile=main.bundle.mjs
18
+ node main.bundle.mjs # or bundle first, for plain JavaScript projects
19
+ ```
20
+
21
+ A front-end bundler works the same way, provided its TypeScript step is not configured to
22
+ skip `node_modules`. **Node on its own cannot import it**, type stripping included: Node
23
+ strips types only from your own files and refuses any `.ts` file under `node_modules`.
15
24
 
16
25
  The code itself needs Node 18 or newer, or any modern browser. **One runtime dependency**,
17
26
  [`image-size`](https://www.npmjs.com/package/image-size), pure JavaScript that runs in
@@ -132,7 +141,7 @@ Everything thrown by this client extends `CraterViewError`, so one `catch` handl
132
141
 
133
142
  | Class | Meaning |
134
143
  |---|---|
135
- | `RateLimited` | 429. `.retryAfter` is seconds to wait: until the window rolls over for the request rate, or a short fixed interval to poll on for the in-flight cap. |
144
+ | `RateLimited` | 429 — one of four limits, and the message says which: the key's request rate, the account's upload URLs a minute, the account's jobs in flight, or a full queue. `.retryAfter` is seconds to wait: until the window rolls over for the two per-minute limits, or an interval to poll on for the other two. |
136
145
  | `JobFailed` | The job ran and did not succeed. `.message` says what you can do about it; `.errorCode` is the half to branch on. |
137
146
  | `CraterViewError` | Everything else, including 4xx and 5xx from the API. |
138
147
 
@@ -156,7 +165,8 @@ Every field the API publishes on a job is exposed here.
156
165
  | `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 |
157
166
  | `outputUrl`, `downloadUrl` | The result, presigned. One to display, one to save |
158
167
  | `thumbUrl` | A small JPEG of the result, for listings. Null when none was drawn |
159
- | `inputUrl` | The file you sent. Null once it has expired — inputs go after a day |
168
+ | `inputUrl` | The picture the model worked from — the region, where you named one |
169
+ | `alphaUrl` | Only for a JPEG result of a picture with transparency, which JPEG cannot hold: the transparency as a grayscale JPEG, white where opaque. The result is then the colour alone |
160
170
  | `contentType` | The result's media type |
161
171
  | `outputBytes` | The result's size in bytes |
162
172
  | `blob()`, `arrayBuffer()` | Download the result |
@@ -164,7 +174,7 @@ Every field the API publishes on a job is exposed here.
164
174
  `result` is the whole of what the job produced, and the five rows above it that describe the
165
175
  file are getters onto `result.output` rather than separate fields — the API states those links
166
176
  once. A model with no file to hand back returns its answer in `result` and leaves every one of
167
- them null.
177
+ them null; the fields it answers with are its `result_schema` in `cv.models()`.
168
178
 
169
179
  `credits` is the only figure about cost the API states, and the price is fixed and published
170
180
  per model, so an invoice reconciles against `credits` alone. For how long a job took, subtract
@@ -176,7 +186,7 @@ accepts exactly that length and nothing else. You do not pass it — it is read
176
186
  in hand, which is what makes it impossible to get wrong.
177
187
 
178
188
  **Running out of credit does not stop you.** A job submitted against a balance of zero is
179
- accepted, charged and run — it simply waits in the community queue, which is served after
189
+ accepted and run, and charged when it succeeds — it simply waits in the community queue, which is served after
180
190
  paid work and always takes a share of it, so it never stalls behind paid work. It comes back
181
191
  with `community` set. There is no payment error to handle:
182
192
  paying — with credit, or with a subscription — buys a place at the front of the queue rather
@@ -189,10 +199,12 @@ is signed in, so the second cannot be derived from the first. Both expire, so fe
189
199
  result rather than storing the link.
190
200
 
191
201
  `thumbUrl` and `inputUrl` are for building a job listing: a few-hundred-pixel preview so a
192
- page of results costs kilobytes, and the original so a result can be shown against what made
193
- it. They keep very different company on expiry — the preview lives as long as the result,
194
- while inputs are deleted a day in — so treat a missing `inputUrl` as normal rather than as an
195
- error.
202
+ page of results costs kilobytes, and the picture the model worked from so a result can be
203
+ shown against it. Where you named a region, `inputUrl` is that region — so a before-and-after
204
+ is a true pair, and what was used is something you can look at rather than something to take
205
+ on trust. It is not the file you uploaded: yours stays yours and is removed on its own
206
+ schedule. Both expire with the result. A model that produces no file has no `thumbUrl`, but still
207
+ has the picture it answered about.
196
208
 
197
209
  ## Webhooks
198
210
 
@@ -268,7 +280,7 @@ old one. You cannot revoke the key you are calling with.
268
280
  ## Everything else
269
281
 
270
282
  ```ts
271
- await cv.models(); // models, parameter schemas, which queue you are on
283
+ await cv.models(); // models, what each takes and answers with, which queue you are on
272
284
  await cv.job("job_..."); // one job by id
273
285
  await cv.usage(); // credit balance, spend and job counts
274
286
 
package/index.ts CHANGED
@@ -4,7 +4,7 @@
4
4
  * import { CraterView } from "craterview";
5
5
  *
6
6
  * const cv = new CraterView({ apiKey: "cv_..." });
7
- * const job = await cv.run(file, { style: "photo" });
7
+ * const job = await cv.run(file, { scale: 4 });
8
8
  * const blob = await job.blob();
9
9
  *
10
10
  * One dependency: `image-size`, which reads an image's dimensions from its header — no
@@ -18,7 +18,7 @@ import { imageSize } from "image-size";
18
18
  // Mirrored from package.json, which is the number a release bumps. It cannot be imported
19
19
  // from there — this ships as TypeScript, so the import would have to resolve in the
20
20
  // consumer's toolchain — so test/version.test.ts asserts the two agree.
21
- export const VERSION = "0.3.13";
21
+ export const VERSION = "0.3.18";
22
22
  const DEFAULT_BASE_URL = "https://api.craterview.ai";
23
23
  // The server rejects a longer wait outright, so asking for one costs a 422 rather than the
24
24
  // wait you asked for. `run()` clamps to this rather than letting that happen.
@@ -35,7 +35,7 @@ const MAX_PAGE = 200;
35
35
  * const key = newIdempotencyKey(); // once, before the first attempt
36
36
  * for (let attempt = 0; attempt < 3; attempt++) {
37
37
  * try {
38
- * job = await cv.submit(inputKey, { idempotencyKey: key, style: "photo" });
38
+ * job = await cv.submit(inputKey, { idempotencyKey: key, scale: 4 });
39
39
  * break;
40
40
  * } catch (e) {
41
41
  * if (!(e instanceof CraterViewError)) continue; // same key, at most one job
@@ -70,15 +70,20 @@ export class CraterViewError extends Error {
70
70
  }
71
71
 
72
72
  /**
73
- * 429 — too fast, or too much at once. Two different limits answer with this: the key's
74
- * request rate, and the account's cap on jobs queued or running at the same time, which
75
- * exists so one caller cannot occupy the whole fleet.
73
+ * 429 — too fast, or too much at once. Four different limits answer with this, and the
74
+ * message says which:
75
+ *
76
+ * - the key's request rate, per minute;
77
+ * - the account's upload URLs, per minute — counted across all its keys;
78
+ * - the account's cap on jobs queued or running at the same time, which exists so one
79
+ * caller cannot occupy the whole fleet. `usage()` reports the cap and what you currently
80
+ * hold against it;
81
+ * - the queue itself, when it is full and taking no more work. Nothing you sent is wrong;
82
+ * an account with credit or a subscription submits on a queue that fills separately.
76
83
  *
77
84
  * `retryAfter` is seconds to wait, and what it means depends on which limit you hit: for
78
- * the rate limit it is when the window rolls over; for the in-flight cap it is a fixed
79
- * short interval to poll on, since a slot frees when one of your own jobs finishes and
80
- * nothing here predicts that. `usage()` reports the cap and what you currently hold
81
- * against it.
85
+ * the two per-minute limits it is when the window rolls over; for the in-flight cap and the
86
+ * full queue it is an interval to poll on, since nothing here predicts when a slot frees.
82
87
  */
83
88
  export class RateLimited extends CraterViewError {
84
89
  constructor(message: string, readonly retryAfter?: number) {
@@ -92,7 +97,7 @@ export class RateLimited extends CraterViewError {
92
97
  *
93
98
  * `message` says what you can do about it and `errorCode` is the half to branch on, because
94
99
  * the prose is written for a person and gets reworded. `inference_failed` means the fault was
95
- * ours, the credits were refunded, and the same call is worth making again.
100
+ * ours, nothing was charged, and the same call is worth making again.
96
101
  *
97
102
  * An unfamiliar code is a failure with no special handling, not an error in itself: new ones
98
103
  * appear as new things become worth telling apart.
@@ -261,10 +266,9 @@ export interface ModelInfo {
261
266
  * more tightly than paid work, so these move when `community` below does — check them
262
267
  * before uploading rather than learning them from a rejected job.
263
268
  *
264
- * Two axes, and they are the two a job is made of. `max_frame_megapixels` bounds one
265
- * frame, which is what a GPU actually holds, and applies to images too since a still is one
266
- * frame. `max_frames` bounds how many frames one job may carry. Zero means that axis is
267
- * unbounded for you.
269
+ * `max_frame_megapixels` bounds one frame you send, and applies to images too since a
270
+ * still is one frame. `max_frames` bounds how many frames one job may carry. Zero means
271
+ * that axis is unbounded for you.
268
272
  *
269
273
  * For a duration, divide: `max_frames / reference_fps` is the longest clip you may send, in
270
274
  * seconds. It is not published as its own field on purpose — one limit with two spellings
@@ -273,12 +277,36 @@ export interface ModelInfo {
273
277
  */
274
278
  max_frame_megapixels: number;
275
279
  max_frames: number;
280
+ /**
281
+ * The largest frame you may get **back**, in megapixels, and for a model that enlarges it
282
+ * is the ceiling that decides whether a job runs: what returns is the frame you send
283
+ * multiplied by the enlargement in each direction, so a modest photograph at a large
284
+ * enlargement is refused where the same photograph unenlarged is not.
285
+ *
286
+ * Check it before uploading:
287
+ *
288
+ * ```ts
289
+ * const result = megapixels * scale ** 2;
290
+ * if (model.max_output_megapixels && result > model.max_output_megapixels) {
291
+ * // send a smaller region of the image, or ask for less enlargement
292
+ * }
293
+ * ```
294
+ *
295
+ * Sending a smaller region keeps every pixel of what it covers; asking for less
296
+ * enlargement keeps the whole picture. Zero means this axis is unbounded for you.
297
+ */
298
+ max_output_megapixels: number;
276
299
  /** Whole credits. The price is flat and knowable before you send anything. */
277
300
  credits_per_image: number;
278
301
  /** Whole credits. Null means this model takes stills only. */
279
302
  credits_per_video_second?: number | null;
280
303
  reference_fps: number;
281
304
  params_schema: Record<string, unknown>;
305
+ /**
306
+ * JSON Schema for the fields a finished job of this model states under `result`, beside
307
+ * any files it links. Empty for a model whose whole answer is its file.
308
+ */
309
+ result_schema: Record<string, unknown>;
282
310
  /**
283
311
  * Whether your key's work goes to the community queue, which is served after priority
284
312
  * work and always takes a share of it, so it never stalls behind paid work. False while
@@ -304,8 +332,8 @@ export interface KeyInfo {
304
332
  rate_limit_per_minute: number;
305
333
  /**
306
334
  * Whether this is the key you are calling with. It cannot be revoked by id while it is;
307
- * `endCurrentKey()` (`DELETE /v1/keys/current`) ends it on request, unless it is the
308
- * account's only way back in.
335
+ * `DELETE /v1/keys/current` ends it on request, unless it is the account's only way
336
+ * back in. This client has no method for that call.
309
337
  */
310
338
  current: boolean;
311
339
  }
@@ -331,6 +359,13 @@ export interface WebhookEvent extends Required<JobData> {}
331
359
 
332
360
  export type JobStatus = "queued" | "running" | "succeeded" | "failed";
333
361
 
362
+ /** A job's standing with the public gallery: `pending` under review, `approved` once shown. */
363
+ export interface GalleryState {
364
+ status: "pending" | "approved";
365
+ /** The submission's id, which is what withdrawing it takes. */
366
+ post_id: string;
367
+ }
368
+
334
369
  /** The API's job representation, exactly as it arrives. Snake case is the wire's. */
335
370
  export interface JobData {
336
371
  id: string;
@@ -360,12 +395,17 @@ export interface JobData {
360
395
  * read them out of `result.output`.
361
396
  */
362
397
  result?: Record<string, unknown> | null;
363
- input_url?: string | null;
364
398
  /** Whole credits. TypeScript cannot say integer, but the API only ever sends one. */
365
399
  credits?: number | null;
366
400
  eta_seconds?: number | null;
367
401
  /** Retained past the ordinary expiry because its owner asked, links and all. */
368
402
  kept?: boolean;
403
+ /**
404
+ * Where this job stands with the public gallery, when you have offered it: present while
405
+ * it is being reviewed or shown, absent or null when it is not offered — including after
406
+ * you withdraw it.
407
+ */
408
+ gallery?: GalleryState | null;
369
409
  community?: boolean;
370
410
  /**
371
411
  * Whether an automated check thought this image may fall outside what the service
@@ -390,11 +430,10 @@ export interface JobData {
390
430
  * credential. Both are presigned and expire — fetch the result rather than storing the link.
391
431
  *
392
432
  * `thumbUrl` is a small JPEG of the result, for showing a page of jobs without downloading
393
- * a page of full-size outputs. `inputUrl` is the file you sent, so a result can be shown
394
- * against what it was made from. The two expire on very different clocks: the preview goes
395
- * with the result, while inputs are deleted after a day — much sooner than the output — so
396
- * `inputUrl` is null for most of a job's life and code that reads it should expect nothing
397
- * there.
433
+ * a page of full-size outputs. `inputUrl` is the picture the model worked from, so a result
434
+ * can be shown against what it was made from. Both expire with the result. `alphaUrl` is
435
+ * there only when you asked for a JPEG of a picture with transparency: the transparency, as
436
+ * a file of its own.
398
437
  *
399
438
  * `etaSeconds` is the whole of what is reported about waiting: how long until the result,
400
439
  * counting time spent waiting for a GPU as well as time spent on one. An estimate and never
@@ -427,6 +466,10 @@ export class Job {
427
466
  // checked, and `false` would say it was checked and cleared. The two are different
428
467
  // answers and only one of them is true.
429
468
  get flagged() { return this.data.flagged ?? null; }
469
+ /** Retained past the ordinary expiry because you asked. */
470
+ get kept() { return this.data.kept ?? false; }
471
+ /** Where this job stands with the public gallery, or null when it is not offered. */
472
+ get gallery() { return this.data.gallery ?? null; }
430
473
  /** The whole answer, including where the file is when there is one. */
431
474
  get result() { return this.data.result ?? null; }
432
475
  /**
@@ -449,7 +492,33 @@ export class Job {
449
492
  */
450
493
  get downloadUrl() { return (this.output["download_url"] as string) ?? null; }
451
494
  get thumbUrl() { return (this.output["thumbnail_url"] as string) ?? null; }
452
- get inputUrl() { return this.data.input_url ?? null; }
495
+ /**
496
+ * A link to the picture the model worked from. Where you named a region, this is that
497
+ * region — so what was used is something you can look at rather than something to take on
498
+ * trust. It is not the file you uploaded: yours stays yours and is removed on its own
499
+ * schedule.
500
+ *
501
+ * Null for a job that has not finished. A model that answers about a picture rather than
502
+ * producing one has it too: it is the picture that answer is about.
503
+ */
504
+ get inputUrl() {
505
+ const given = (this.data.result ?? {})["input"];
506
+ return (given && typeof given === "object")
507
+ ? ((given as Record<string, unknown>)["url"] as string) ?? null
508
+ : null;
509
+ }
510
+ /**
511
+ * The transparency of a result you asked for as JPEG, which cannot hold it: a grayscale
512
+ * JPEG the size of the result, white where it is opaque and black where it is transparent.
513
+ * The result is then the colour alone, not placed on any background, so the two together
514
+ * are the picture. Null for every other result.
515
+ */
516
+ get alphaUrl() {
517
+ const mask = (this.data.result ?? {})["alpha"];
518
+ return (mask && typeof mask === "object")
519
+ ? ((mask as Record<string, unknown>)["url"] as string) ?? null
520
+ : null;
521
+ }
453
522
  get contentType() { return (this.output["content_type"] as string) ?? null; }
454
523
  get outputBytes() { return (this.output["bytes"] as number) ?? null; }
455
524
 
@@ -689,9 +758,9 @@ export class CraterView {
689
758
  /**
690
759
  * The account's credit balance, what it has spent, and what is in flight.
691
760
  *
692
- * All-time, not monthly: credits are granted and deplete rather than renewing. Counts
693
- * committed work rather than completed, so a queued job is already in the figures —
694
- * otherwise this and the balance a submission is checked against would disagree.
761
+ * All-time, not monthly: credits are granted and deplete rather than renewing. A job is
762
+ * charged when it succeeds, so queued and running work is not in the figures yet, and a
763
+ * failed job costs nothing.
695
764
  *
696
765
  * `credits_remaining` is always a number, floored at zero. `jobs_in_flight` and
697
766
  * `max_jobs_in_flight` are the state of the queue and are what a 429 on submit is about.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "craterview",
3
- "version": "0.3.13",
3
+ "version": "0.3.18",
4
4
  "dependencies": {
5
5
  "image-size": "^2.0.4"
6
6
  },
@@ -26,6 +26,8 @@
26
26
  },
27
27
  "devDependencies": {
28
28
  "@types/node": "^26.2.0",
29
+ "esbuild": "^0.25.0",
30
+ "tsx": "^4.23.15",
29
31
  "typescript": "^7.0.2",
30
32
  "vitest": "^4.1.11"
31
33
  }