pixelkiln 0.75.0 → 0.77.0

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/dist/cli.js CHANGED
@@ -6139,6 +6139,25 @@ function cleanupResults(images, sent, operation) {
6139
6139
  }
6140
6140
  return images.map((image) => Buffer.from(image.base64, "base64"));
6141
6141
  }
6142
+ function parseProFlashQuote(raw) {
6143
+ const body = raw && typeof raw === "object" ? raw : {};
6144
+ const nested = [body, body.usage, body.cost, body.estimate].filter((v) => Boolean(v) && typeof v === "object");
6145
+ const pick = (...names) => {
6146
+ for (const obj of nested) {
6147
+ for (const name of names) {
6148
+ const value = obj[name];
6149
+ if (typeof value === "number" && Number.isFinite(value) && value >= 0) return value;
6150
+ }
6151
+ }
6152
+ return null;
6153
+ };
6154
+ const image = pick("image", "image_generations", "first_image", "first_image_generations");
6155
+ const rotations = pick("rotations", "rotation", "rotations_generations", "rotation_generations");
6156
+ const total = pick("total", "total_generations", "generations");
6157
+ if (total !== null) return { image, rotations, total };
6158
+ if (image !== null || rotations !== null) return { image, rotations, total: (image ?? 0) + (rotations ?? 0) };
6159
+ throw new Error(`GET /pro-flash/cost returned no price: ${JSON.stringify(raw).slice(0, 200)}`);
6160
+ }
6142
6161
  var PixelLabError = class extends ProviderError {
6143
6162
  constructor(message6, status, body) {
6144
6163
  super("pixellab", message6, { status });
@@ -6197,6 +6216,19 @@ var PixelLabClient = class {
6197
6216
  plan: raw.subscription?.plan ?? "unknown"
6198
6217
  };
6199
6218
  }
6219
+ /**
6220
+ * `GET /pro-flash/cost`: PixelLab's own quote for one Pro Flash job, split
6221
+ * into the first image and the v3 rotations. Free, and provisional by
6222
+ * PixelLab's own description; the billed amount on the finished job is
6223
+ * the real one. The response schema is undocumented (`{}` in the spec),
6224
+ * so this reads it leniently: a total if one is given, else the sum of
6225
+ * the parts, and refuses an answer with no number in it.
6226
+ */
6227
+ async proFlashCost(args) {
6228
+ const query = new URLSearchParams({ operation: args.operation, width: String(args.width), height: String(args.height) });
6229
+ if (args.nDirections !== void 0) query.set("n_directions", String(args.nDirections));
6230
+ return parseProFlashQuote(await this.request(`/pro-flash/cost?${query}`));
6231
+ }
6200
6232
  /**
6201
6233
  * Square objects that persist indefinitely.
6202
6234
  *
@@ -7333,6 +7365,65 @@ function proFlashImageCost(width, height) {
7333
7365
  const side = Math.max(width, height);
7334
7366
  return side <= 96 ? 5 : side <= 208 ? 6 : 9;
7335
7367
  }
7368
+ function proFlashQuoteQuery(spec) {
7369
+ if (spec.provider !== "pixellab" || spec.mirror) return null;
7370
+ if (spec.revision) {
7371
+ if (spec.revision.engine !== "pro-flash") return null;
7372
+ return {
7373
+ operation: spec.revision.mode === "inpaint" ? "inpaint" : "edit",
7374
+ width: spec.revision.sourceWidth ?? spec.width,
7375
+ height: spec.revision.sourceHeight ?? spec.height,
7376
+ part: "total"
7377
+ };
7378
+ }
7379
+ if (spec.generator === "imageProFlash") return { operation: "create", width: spec.width, height: spec.height, part: "total" };
7380
+ const character = spec.character;
7381
+ if (spec.generator === "character" && character?.kind === "base" && character.mode === "pro-flash") {
7382
+ const south = character.reference?.south;
7383
+ return south ? { operation: "character", width: south.width, height: south.height, nDirections: 8, part: "rotations" } : { operation: "character", width: spec.width, height: spec.height, nDirections: 8, part: "total" };
7384
+ }
7385
+ const object = spec.objectPro;
7386
+ if (spec.generator === "objectPro" && object?.kind === "base") {
7387
+ const south = object.reference;
7388
+ return south ? { operation: "object", width: south.width, height: south.height, nDirections: object.directions, part: "rotations" } : { operation: "object", width: spec.width, height: spec.height, nDirections: object.directions, part: "total" };
7389
+ }
7390
+ return null;
7391
+ }
7392
+ function createProFlashQuoter(client2, opts = {}) {
7393
+ const ttl = opts.ttlMs ?? 10 * 6e4;
7394
+ const timeoutMs = opts.timeoutMs ?? 5e3;
7395
+ const now = opts.now ?? Date.now;
7396
+ const cache = /* @__PURE__ */ new Map();
7397
+ return async (spec) => {
7398
+ const query = proFlashQuoteQuery(spec);
7399
+ if (!query) return null;
7400
+ const key = `${query.operation}:${query.width}x${query.height}:${query.nDirections ?? ""}`;
7401
+ let hit = cache.get(key);
7402
+ if (!hit || now() - hit.at > ttl) {
7403
+ let timer;
7404
+ const answer = Promise.race([
7405
+ client2.proFlashCost({
7406
+ operation: query.operation,
7407
+ width: query.width,
7408
+ height: query.height,
7409
+ ...query.nDirections !== void 0 ? { nDirections: query.nDirections } : {}
7410
+ }),
7411
+ new Promise((_, reject) => {
7412
+ timer = setTimeout(() => reject(new Error(`PixelLab's Pro Flash quote did not answer within ${timeoutMs / 1e3}s`)), timeoutMs);
7413
+ })
7414
+ ]).finally(() => clearTimeout(timer));
7415
+ hit = { at: now(), answer };
7416
+ cache.set(key, hit);
7417
+ answer.catch(() => {
7418
+ if (cache.get(key) === hit) cache.delete(key);
7419
+ });
7420
+ }
7421
+ const quote = await hit.answer;
7422
+ if (query.part === "total") return quote.total;
7423
+ if (quote.rotations === null) throw new Error("PixelLab's Pro Flash quote gave no rotations figure for a base drawn from a sprite");
7424
+ return quote.rotations;
7425
+ };
7426
+ }
7336
7427
  function characterAnimationName(spec) {
7337
7428
  return `pixelkiln:${spec.styleId}/${spec.assetId}`;
7338
7429
  }
@@ -14208,31 +14299,43 @@ async function applyManifestEdit(manifestPath, edit) {
14208
14299
  (absolute, original) => ({ manifestPath: absolute, sha256: sha256(original), changed: false })
14209
14300
  );
14210
14301
  }
14211
- async function priceManifestEdit(manifestPath, edit) {
14302
+ async function priceManifestEdit(manifestPath, edit, opts = {}) {
14212
14303
  return withCandidate(
14213
14304
  manifestPath,
14214
14305
  edit,
14215
14306
  async ({ absolute, specs }) => {
14216
14307
  const before = new Map((await resolveSpecs(await loadManifest(absolute))).map((spec) => [`${spec.styleId}/${spec.assetId}`, spec.specHash]));
14308
+ const priced = specs.filter((spec) => before.get(`${spec.styleId}/${spec.assetId}`) !== spec.specHash);
14309
+ let quoteError;
14310
+ const live = await Promise.all(priced.map(async (spec) => {
14311
+ if (!opts.quote) return null;
14312
+ try {
14313
+ return await opts.quote(spec);
14314
+ } catch (error) {
14315
+ quoteError ??= error instanceof Error ? error.message : String(error);
14316
+ return null;
14317
+ }
14318
+ }));
14217
14319
  const items = [];
14218
14320
  const totals = {};
14219
- for (const spec of specs) {
14321
+ priced.forEach((spec, index) => {
14220
14322
  const key = `${spec.styleId}/${spec.assetId}`;
14221
- const was = before.get(key);
14222
- if (was === spec.specHash) continue;
14323
+ const quoted = live[index];
14324
+ const cost = quoted ?? spec.cost;
14223
14325
  items.push({
14224
14326
  key,
14225
14327
  styleId: spec.styleId,
14226
14328
  assetId: spec.assetId,
14227
- change: was === void 0 ? "new" : "changed",
14329
+ change: before.has(key) ? "changed" : "new",
14228
14330
  provider: spec.provider,
14229
- cost: spec.cost,
14331
+ cost,
14230
14332
  costUnit: spec.costUnit,
14231
- candidates: spec.candidates
14333
+ candidates: spec.candidates,
14334
+ ...quoted !== null && quoted !== void 0 ? { quote: "live", estimate: spec.cost } : {}
14232
14335
  });
14233
- totals[spec.costUnit] = (totals[spec.costUnit] ?? 0) + spec.cost;
14234
- }
14235
- return { items, totals };
14336
+ totals[spec.costUnit] = (totals[spec.costUnit] ?? 0) + cost;
14337
+ });
14338
+ return quoteError ? { items, totals, quoteError } : { items, totals };
14236
14339
  },
14237
14340
  () => ({ items: [], totals: {} })
14238
14341
  );
@@ -14895,7 +14998,15 @@ function createGalleryPriceHandler(opts) {
14895
14998
  } catch (error) {
14896
14999
  throw new ManifestEditError(error instanceof Error ? error.message : String(error));
14897
15000
  }
14898
- return priceManifestEdit(manifestPath, parsed.data);
15001
+ let quote = null;
15002
+ let quoteError;
15003
+ try {
15004
+ quote = opts.quoteFor ? await opts.quoteFor(parsed.data.project) : null;
15005
+ } catch (error) {
15006
+ quoteError = error instanceof Error ? error.message : String(error);
15007
+ }
15008
+ const price = await priceManifestEdit(manifestPath, parsed.data, quote ? { quote } : {});
15009
+ return quoteError && !price.quoteError ? { ...price, quoteError } : price;
14899
15010
  };
14900
15011
  }
14901
15012
 
@@ -15753,6 +15864,9 @@ ${css}</style>
15753
15864
  <button id="studio" type="button" hidden title="Draft a new character: its style, base, loops, and portrait, priced as you go">+ New character</button>
15754
15865
  <button id="refresh" type="button" title="Re-read the manifest, lockfile, and disk">Refresh</button>
15755
15866
  <label class="chip" title="Refresh every 5 seconds while this tab is visible"><input id="auto" type="checkbox"> auto</label>
15867
+ <label class="chip" title="Play every loop on the grid; without this, a loop plays while the pointer is over it"><input id="playloops" type="checkbox"> play loops</label>
15868
+ <span id="backdrop-slot"></span>
15869
+ <label class="chip" id="notify-chip" hidden title="A system notification when a job finishes while this tab is in the background"><input id="notify" type="checkbox"> notify</label>
15756
15870
  </div>
15757
15871
  <div class="totals" id="totals"></div>
15758
15872
  <div class="chips" id="chips"></div>
@@ -15762,7 +15876,7 @@ ${css}</style>
15762
15876
  <main id="root"></main>
15763
15877
  <footer>
15764
15878
  Click a sprite for its full record; <kbd>shift</kbd>-click adds it to a side-by-side comparison. <kbd>\u2190</kbd>/<kbd>\u2192</kbd> step through the visible set while a record
15765
- is open, <kbd>Esc</kbd> closes it, and <kbd>/</kbd> jumps to search. This page reads the manifest, lockfile,
15879
+ is open, <kbd>,</kbd>/<kbd>.</kbd> step a loop's frames and <kbd>space</kbd> plays it, <kbd>Esc</kbd> closes it, and <kbd>/</kbd> jumps to search. This page reads the manifest, lockfile,
15766
15880
  and disk only. It never contacts a provider<span id="foot-edit"> and never writes anything</span><span id="foot-editing" hidden>.
15767
15881
  Editing is on: saving rewrites the manifest and nothing else</span><span id="foot-gen" hidden>.
15768
15882
  Generation is on under the session budget shown above; every run is the same submit, poll, and fetch as <code>pixelkiln gen</code></span>.
@@ -17879,7 +17993,10 @@ async function serveUntilStopped(initial, reload, args, access2) {
17879
17993
  };
17880
17994
  const loadProject = async (project) => {
17881
17995
  const ctx = await access2.loadProject(project);
17882
- const dir = path34.dirname(ctx.loaded.path);
17996
+ loadProjectEnv(path34.dirname(ctx.loaded.path));
17997
+ return ctx;
17998
+ };
17999
+ const loadProjectEnv = (dir) => {
17883
18000
  const declared = readEnvFiles(dir);
17884
18001
  const credentialNames = new Set(availableProviders().flatMap((id) => providerCredentialEnvs(providerFactory(id))));
17885
18002
  for (const [name, value] of Object.entries(declared)) {
@@ -17890,7 +18007,18 @@ async function serveUntilStopped(initial, reload, args, access2) {
17890
18007
  }
17891
18008
  }
17892
18009
  loadEnvFiles(dir);
17893
- return ctx;
18010
+ };
18011
+ const quoters = /* @__PURE__ */ new Map();
18012
+ const quoteFor = async (project) => {
18013
+ loadProjectEnv(path34.dirname(path34.resolve(await access2.manifestFor(project))));
18014
+ const key = process.env.PIXELLAB_API_KEY;
18015
+ if (!key) return null;
18016
+ let quoter = quoters.get(key);
18017
+ if (!quoter) {
18018
+ quoter = createProFlashQuoter(new PixelLabClient(key));
18019
+ quoters.set(key, quoter);
18020
+ }
18021
+ return quoter;
17894
18022
  };
17895
18023
  const server = await serveGallery({
17896
18024
  load: () => {
@@ -17913,7 +18041,7 @@ async function serveUntilStopped(initial, reload, args, access2) {
17913
18041
  ),
17914
18042
  ...args.edit ? { edit: createGalleryEditHandler({ manifestFor: access2.manifestFor, loadProject: access2.loadProject, reload, onProgress: log }) } : {},
17915
18043
  ...budget ? { generate: createGenerateHandlers({ loadProject, providerFor, budget, reload, onProgress: log }) } : {},
17916
- ...args.edit ? { price: createGalleryPriceHandler({ manifestFor: access2.manifestFor }) } : {},
18044
+ ...args.edit ? { price: createGalleryPriceHandler({ manifestFor: access2.manifestFor, quoteFor }) } : {},
17917
18045
  // Estimating acts on the author's behalf and feeds a manifest edit, so it follows the write gate.
17918
18046
  ...args.edit ? { skeleton: createGallerySkeletonHandlers({ loadProject, limit: args.estimateLimit, onProgress: log }) } : {},
17919
18047
  // The editor exists to write hand edits back, so it follows the write gate.