scenri 0.5.0 → 0.5.1

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/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.5.1](https://github.com/tonygorb/Scenri/compare/v0.5.0...v0.5.1) (2026-08-26)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * a grown frame keeps its subject's place and solves its own seam ([9d2108b](https://github.com/tonygorb/Scenri/commit/9d2108b2e5af29e1a6efdd531b82b647ec1c58ad))
9
+ * a grown frame keeps its subject's place and solves its own seam ([d45bf73](https://github.com/tonygorb/Scenri/commit/d45bf73d4760573af863b2f4533983382b16795c))
10
+
3
11
  ## [0.5.0](https://github.com/tonygorb/Scenri/compare/v0.4.7...v0.5.0) (2026-08-25)
4
12
 
5
13
 
package/dist/serve.js CHANGED
@@ -1238,9 +1238,9 @@ function libraryMethods(db) {
1238
1238
  local: String(i.sourceUrl).startsWith("local:")
1239
1239
  });
1240
1240
  const seen = /* @__PURE__ */ new Set();
1241
- const usable = images.filter((i) => i.assetRef && !seen.has(i.assetRef) && seen.add(i.assetRef));
1242
- const shots = usable.filter((i) => !i.excluded).map(shot);
1243
- const hiddenShots = usable.filter((i) => i.excluded).map(shot);
1241
+ const usable2 = images.filter((i) => i.assetRef && !seen.has(i.assetRef) && seen.add(i.assetRef));
1242
+ const shots = usable2.filter((i) => !i.excluded).map(shot);
1243
+ const hiddenShots = usable2.filter((i) => i.excluded).map(shot);
1244
1244
  return {
1245
1245
  id: `cat-${p.id}`,
1246
1246
  name: p.title,
@@ -6141,8 +6141,9 @@ function planExpand(source, targetRatio) {
6141
6141
  function expandInstruction(plan, direction) {
6142
6142
  const where = plan.axis === "width" ? "left and right" : "top and bottom";
6143
6143
  const nearEdge = plan.axis === "height" ? "\nDepth: the bottom edge of the frame is the part of the surface nearest the camera; the top edge is the furthest away." : "";
6144
- const own = direction.trim() ? `
6145
- Also: ${direction.trim()}` : "";
6144
+ const extra = direction.trim().replace(/^[.\s]+/, "");
6145
+ const own = extra ? `
6146
+ Also: ${extra}` : "";
6146
6147
  return `Fill only the soft blurred margin at the ${where} of this frame so the photograph continues into it.
6147
6148
  Continue: the same surface, the same light direction, the same colour temperature and the same depth of field that are already in the picture.` + nearEdge + `
6148
6149
  Constraints: change only the blurred margin; keep the sharp photograph unchanged in position, scale and content.
@@ -6173,6 +6174,148 @@ async function attentionCropOrigin(srcBuf, source, plan) {
6173
6174
  return { left: plan.left, top: plan.top };
6174
6175
  }
6175
6176
  }
6177
+
6178
+ // src/outpaint/membrane.ts
6179
+ var SOLVE_MAX_EDGE = 192;
6180
+ var SOLVE_MIN_EDGE = 8;
6181
+ var SWEEPS_PER_EDGE = 2;
6182
+ var MIN_SWEEPS = 40;
6183
+ var POLISH_SWEEPS = 32;
6184
+ var POLISH_MAX_CELLS = 4e6;
6185
+ function solveMembrane(req) {
6186
+ const { width, height, axis, seamAt } = req;
6187
+ const along = axis === "width" ? height : width;
6188
+ const depth = axis === "width" ? width : height;
6189
+ const out = new Float32Array(width * height * 3);
6190
+ if (width < 1 || height < 1 || along < 1 || depth < 1) return out;
6191
+ if (depth === 1) {
6192
+ for (let a = 0; a < along; a++) {
6193
+ const off = marginOffset(0, a, width, axis, seamAt, depth) * 3;
6194
+ for (let c = 0; c < 3; c++) out[off + c] = req.seam[a * 3 + c];
6195
+ }
6196
+ return out;
6197
+ }
6198
+ const coarse = cascade(req.seam, along, depth);
6199
+ if (depth * along <= POLISH_MAX_CELLS) {
6200
+ const full = { along, depth, data: new Float32Array(along * depth * 3) };
6201
+ prolong(coarse, full);
6202
+ relax(full, req.seam, POLISH_SWEEPS);
6203
+ for (let d = 0; d < depth; d++) {
6204
+ for (let a = 0; a < along; a++) {
6205
+ const off = marginOffset(d, a, width, axis, seamAt, depth) * 3;
6206
+ const src = (d * along + a) * 3;
6207
+ out[off] = full.data[src];
6208
+ out[off + 1] = full.data[src + 1];
6209
+ out[off + 2] = full.data[src + 2];
6210
+ }
6211
+ }
6212
+ return out;
6213
+ }
6214
+ for (let d = 0; d < depth; d++) {
6215
+ for (let a = 0; a < along; a++) {
6216
+ const off = marginOffset(d, a, width, axis, seamAt, depth) * 3;
6217
+ sampleGrid(coarse, d, a, depth, along, out, off);
6218
+ }
6219
+ }
6220
+ return out;
6221
+ }
6222
+ function marginOffset(d, a, width, axis, seamAt, depth) {
6223
+ if (axis === "width") {
6224
+ const x = seamAt === "far" ? depth - 1 - d : d;
6225
+ return a * width + x;
6226
+ }
6227
+ const y = seamAt === "far" ? depth - 1 - d : d;
6228
+ return y * width + a;
6229
+ }
6230
+ function cascade(seam, along, depth) {
6231
+ const scale = Math.max(1, Math.ceil(Math.max(along, depth) / SOLVE_MAX_EDGE));
6232
+ const targetAlong = Math.max(2, Math.round(along / scale));
6233
+ const targetDepth = Math.max(2, Math.round(depth / scale));
6234
+ const edges = [];
6235
+ for (let e = SOLVE_MIN_EDGE; e < Math.max(targetAlong, targetDepth); e *= 2) edges.push(e);
6236
+ edges.push(Math.max(targetAlong, targetDepth));
6237
+ let grid = null;
6238
+ for (const edge of edges) {
6239
+ const ratio = edge / Math.max(targetAlong, targetDepth);
6240
+ const la = Math.max(2, Math.round(targetAlong * ratio));
6241
+ const ld = Math.max(2, Math.round(targetDepth * ratio));
6242
+ const next = { along: la, depth: ld, data: new Float32Array(la * ld * 3) };
6243
+ if (grid) prolong(grid, next);
6244
+ relax(next, resampleSeam(seam, along, la));
6245
+ grid = next;
6246
+ }
6247
+ return grid;
6248
+ }
6249
+ function resampleSeam(seam, along, to) {
6250
+ const out = new Float32Array(to * 3);
6251
+ for (let i = 0; i < to; i++) {
6252
+ const src = to === 1 ? 0 : i * (along - 1) / (to - 1);
6253
+ const lo = Math.floor(src);
6254
+ const hi = Math.min(along - 1, lo + 1);
6255
+ const t = src - lo;
6256
+ for (let c = 0; c < 3; c++) out[i * 3 + c] = seam[lo * 3 + c] * (1 - t) + seam[hi * 3 + c] * t;
6257
+ }
6258
+ return out;
6259
+ }
6260
+ function prolong(from, to) {
6261
+ for (let d = 0; d < to.depth; d++) {
6262
+ const sd = to.depth === 1 ? 0 : d * (from.depth - 1) / (to.depth - 1);
6263
+ for (let a = 0; a < to.along; a++) {
6264
+ const sa = to.along === 1 ? 0 : a * (from.along - 1) / (to.along - 1);
6265
+ sampleGrid(from, sd, sa, from.depth, from.along, to.data, (d * to.along + a) * 3);
6266
+ }
6267
+ }
6268
+ }
6269
+ function sampleGrid(grid, d, a, fromDepth, fromAlong, into, off) {
6270
+ const sd = fromDepth === grid.depth ? d : grid.depth === 1 ? 0 : d * (grid.depth - 1) / (fromDepth - 1);
6271
+ const sa = fromAlong === grid.along ? a : grid.along === 1 ? 0 : a * (grid.along - 1) / (fromAlong - 1);
6272
+ const d0 = Math.floor(sd);
6273
+ const a0 = Math.floor(sa);
6274
+ const d1 = Math.min(grid.depth - 1, d0 + 1);
6275
+ const a1 = Math.min(grid.along - 1, a0 + 1);
6276
+ const td = sd - d0;
6277
+ const ta = sa - a0;
6278
+ const i00 = (d0 * grid.along + a0) * 3;
6279
+ const i01 = (d0 * grid.along + a1) * 3;
6280
+ const i10 = (d1 * grid.along + a0) * 3;
6281
+ const i11 = (d1 * grid.along + a1) * 3;
6282
+ for (let c = 0; c < 3; c++) {
6283
+ const top = grid.data[i00 + c] * (1 - ta) + grid.data[i01 + c] * ta;
6284
+ const bot = grid.data[i10 + c] * (1 - ta) + grid.data[i11 + c] * ta;
6285
+ into[off + c] = top * (1 - td) + bot * td;
6286
+ }
6287
+ }
6288
+ function relax(grid, seam, fixedSweeps) {
6289
+ const { depth, along, data } = grid;
6290
+ for (let a = 0; a < along; a++) {
6291
+ const off = a * 3;
6292
+ for (let c = 0; c < 3; c++) data[off + c] = seam[off + c];
6293
+ }
6294
+ if (depth < 2) return;
6295
+ const n = Math.max(depth, along);
6296
+ const omega = Math.min(1.99, 2 / (1 + Math.sin(Math.PI / Math.max(2, n))));
6297
+ const sweeps = fixedSweeps ?? Math.max(MIN_SWEEPS, SWEEPS_PER_EDGE * n);
6298
+ for (let sweep = 0; sweep < sweeps; sweep++) {
6299
+ for (let parity = 0; parity < 2; parity++) {
6300
+ for (let d = 1; d < depth; d++) {
6301
+ const row = d * along;
6302
+ const rowUp = (d - 1) * along;
6303
+ const rowDn = (d < depth - 1 ? d + 1 : d - 1) * along;
6304
+ for (let a = (d + parity) % 2; a < along; a += 2) {
6305
+ const iL = row + (a > 0 ? a - 1 : Math.min(1, along - 1));
6306
+ const iR = row + (a < along - 1 ? a + 1 : Math.max(0, along - 2));
6307
+ const i = row + a;
6308
+ for (let c = 0; c < 3; c++) {
6309
+ const avg = (data[(rowUp + a) * 3 + c] + data[(rowDn + a) * 3 + c] + data[iL * 3 + c] + data[iR * 3 + c]) * 0.25;
6310
+ data[i * 3 + c] += omega * (avg - data[i * 3 + c]);
6311
+ }
6312
+ }
6313
+ }
6314
+ }
6315
+ }
6316
+ }
6317
+
6318
+ // src/expand.ts
6176
6319
  async function expandCanvas(source, plan) {
6177
6320
  const bed = await sharp7(source).resize(plan.width, plan.height, { fit: "cover", position: "centre" }).blur(Math.max(8, Math.round(Math.max(plan.width, plan.height) / 40))).toBuffer();
6178
6321
  return sharp7(bed).composite([{ input: source, left: plan.left, top: plan.top }]).png().toBuffer();
@@ -6267,17 +6410,27 @@ async function reconcile(surround, source, side, axis) {
6267
6410
  smooth[i * 3 + c] = sum / n;
6268
6411
  }
6269
6412
  }
6270
- const depth = axis === "width" ? W : H;
6413
+ const half = Math.max(1, Math.floor(along / 2));
6414
+ const centreLo = (half - 1) / 2;
6415
+ const centreHi = half + (along - half - 1) / 2;
6416
+ const span = Math.max(1, centreHi - centreLo);
6417
+ const base = [0, 0, 0];
6418
+ const slope = [0, 0, 0];
6419
+ for (let c = 0; c < 3; c++) {
6420
+ const lo = medianOf(smooth, c, 0, half);
6421
+ const hi = medianOf(smooth, c, half, along);
6422
+ slope[c] = (hi - lo) / span;
6423
+ base[c] = lo - slope[c] * centreLo;
6424
+ for (let i = 0; i < along; i++) smooth[i * 3 + c] -= base[c] + slope[c] * i;
6425
+ }
6426
+ const field = solveMembrane({ width: W, height: H, axis, seamAt: side.seamAt, seam: smooth });
6271
6427
  const corrected = Buffer.from(marginRaw);
6272
6428
  for (let y = 0; y < H; y++) {
6273
6429
  for (let x = 0; x < W; x++) {
6274
- const d = axis === "width" ? side.seamAt === "far" ? W - 1 - x : x : side.seamAt === "far" ? H - 1 - y : y;
6275
- const fall = 1 - d / Math.max(1, depth - 1);
6276
- if (fall <= 0) continue;
6277
6430
  const i = axis === "width" ? y : x;
6278
6431
  const off = (y * W + x) * 3;
6279
6432
  for (let c = 0; c < 3; c++) {
6280
- const v = corrected[off + c] + smooth[i * 3 + c] * fall;
6433
+ const v = corrected[off + c] + base[c] + slope[c] * i + field[off + c];
6281
6434
  corrected[off + c] = v < 0 ? 0 : v > 255 ? 255 : Math.round(v);
6282
6435
  }
6283
6436
  }
@@ -6288,6 +6441,14 @@ async function reconcile(surround, source, side, axis) {
6288
6441
  async function expandCanvasBedOnly(source, plan) {
6289
6442
  return sharp7(source).resize(plan.width, plan.height, { fit: "cover", position: "centre" }).blur(Math.max(8, Math.round(Math.max(plan.width, plan.height) / 40))).toBuffer();
6290
6443
  }
6444
+ function medianOf(rgb, channel, from, to) {
6445
+ const n = to - from;
6446
+ if (n < 1) return 0;
6447
+ const values = new Float64Array(n);
6448
+ for (let i = 0; i < n; i++) values[i] = rgb[(from + i) * 3 + channel];
6449
+ values.sort();
6450
+ return n % 2 ? values[(n - 1) / 2] : (values[n / 2 - 1] + values[n / 2]) / 2;
6451
+ }
6291
6452
  async function seamScore(image, plan, source) {
6292
6453
  const { data, info } = await sharp7(image).removeAlpha().greyscale().raw().toBuffer({ resolveWithObject: true });
6293
6454
  const W = info.width;
@@ -6318,6 +6479,89 @@ async function seamScore(image, plan, source) {
6318
6479
  const second = first + (horizontal ? source.width : source.height);
6319
6480
  return Math.max(at(first), at(second));
6320
6481
  }
6482
+ var SEAM_VISIBLE = 2.2;
6483
+ var OFFSET = 4;
6484
+ var RESIDUAL_VISIBLE = 15;
6485
+ async function seamResidual(image, plan, source) {
6486
+ const { data, info } = await sharp7(image).removeAlpha().raw().toBuffer({ resolveWithObject: true });
6487
+ const W = info.width;
6488
+ const H = info.height;
6489
+ const ch = info.channels;
6490
+ const horizontal = plan.axis === "width";
6491
+ const between = (a, b) => {
6492
+ const limit = horizontal ? W : H;
6493
+ if (a < 0 || b < 0 || a >= limit || b >= limit) return null;
6494
+ const run2 = horizontal ? H : W;
6495
+ let sum = 0;
6496
+ for (let i = 0; i < run2; i++) {
6497
+ const ia = (horizontal ? i * W + a : a * W + i) * ch;
6498
+ const ib = (horizontal ? i * W + b : b * W + i) * ch;
6499
+ sum += Math.abs(data[ia] - data[ib]) + Math.abs(data[ia + 1] - data[ib + 1]) + Math.abs(data[ia + 2] - data[ib + 2]);
6500
+ }
6501
+ return sum / (run2 * 3);
6502
+ };
6503
+ const near = horizontal ? plan.left : plan.top;
6504
+ const far = near + (horizontal ? source.width : source.height);
6505
+ const both = [
6506
+ // The margin before the picture starts, against the picture just inside it.
6507
+ between(near - OFFSET, near + OFFSET - 1),
6508
+ // The picture just before it ends, against the margin just after.
6509
+ between(far - OFFSET, far + OFFSET - 1)
6510
+ ].filter((v) => v !== null);
6511
+ return both.length ? Math.max(...both) : 0;
6512
+ }
6513
+ function seamPenalty(score, residual) {
6514
+ return Math.max(score / SEAM_VISIBLE, residual / RESIDUAL_VISIBLE);
6515
+ }
6516
+ var MIN_SHARE = 0.2;
6517
+ var MAX_SHARE = 0.8;
6518
+ async function subjectFraction(src, source, axis) {
6519
+ try {
6520
+ const window = axis === "width" ? { width: Math.max(8, Math.round(source.width * 0.5)), height: source.height } : { width: source.width, height: Math.max(8, Math.round(source.height * 0.5)) };
6521
+ const { info } = await sharp7(src).resize(window.width, window.height, { fit: "cover", position: "attention" }).toBuffer({ resolveWithObject: true });
6522
+ const offset = axis === "width" ? Math.abs(typeof info.cropOffsetLeft === "number" ? info.cropOffsetLeft : 0) : Math.abs(typeof info.cropOffsetTop === "number" ? info.cropOffsetTop : 0);
6523
+ const span = axis === "width" ? source.width : source.height;
6524
+ const extent = axis === "width" ? window.width : window.height;
6525
+ const centre = (offset + extent / 2) / span;
6526
+ return Math.min(1, Math.max(0, (centre + 0.5) / 2));
6527
+ } catch {
6528
+ return 0.5;
6529
+ }
6530
+ }
6531
+ function placeExpand(plan, source, fraction) {
6532
+ const share = Math.min(MAX_SHARE, Math.max(MIN_SHARE, fraction));
6533
+ if (plan.axis === "width") {
6534
+ const room2 = plan.width - source.width;
6535
+ if (room2 <= 0) return plan;
6536
+ return { ...plan, left: Math.min(room2, Math.max(0, Math.round(room2 * share))) };
6537
+ }
6538
+ const room = plan.height - source.height;
6539
+ if (room <= 0) return plan;
6540
+ return { ...plan, top: Math.min(room, Math.max(0, Math.round(room * share))) };
6541
+ }
6542
+
6543
+ // src/outpaint/route.ts
6544
+ var canOutpaint = (e) => {
6545
+ const caps = e.capabilities();
6546
+ return caps.supportsOutpaint === true;
6547
+ };
6548
+ var usable = async (e) => {
6549
+ try {
6550
+ return (await e.isAvailable()).ok;
6551
+ } catch {
6552
+ return false;
6553
+ }
6554
+ };
6555
+ async function resolveOutpaintRoute(all, shot) {
6556
+ if (canOutpaint(shot)) return { engine: shot, method: "outpaint", crossed: false };
6557
+ const shotId = shot.capabilities().id;
6558
+ for (const candidate of all) {
6559
+ const caps = candidate.capabilities();
6560
+ if (caps.id === shotId || caps.placeholder || !canOutpaint(candidate)) continue;
6561
+ if (await usable(candidate)) return { engine: candidate, method: "outpaint", crossed: true };
6562
+ }
6563
+ return { engine: shot, method: "reframe", crossed: false };
6564
+ }
6321
6565
  async function driftDiff(a, b) {
6322
6566
  const metaA = await sharp7(a).metadata();
6323
6567
  const metaB = await sharp7(b).metadata();
@@ -7430,6 +7674,16 @@ function registerImageRoutes(app, deps) {
7430
7674
 
7431
7675
  // src/release/notes.data.ts
7432
7676
  var RELEASES = [
7677
+ {
7678
+ version: "0.5.1",
7679
+ date: "2026-08-26",
7680
+ sections: [
7681
+ {
7682
+ heading: "Shots",
7683
+ body: "An extended shot keeps its subject where it was composed, so a product standing near one edge stays near that edge in the wider frame rather than drifting to the middle. The new margin now meets the picture across the whole join instead of only at the seam, so a difference in tone or light no longer survives out at the frame edge. Where a connected engine can paint a margin directly, an extension is handed to it."
7684
+ }
7685
+ ]
7686
+ },
7433
7687
  {
7434
7688
  version: "0.5.0",
7435
7689
  date: "2026-08-26",
@@ -8620,6 +8874,8 @@ function buildServer(opts) {
8620
8874
  let work;
8621
8875
  let expectShape;
8622
8876
  let editedFrom = null;
8877
+ let runEngine = engine;
8878
+ let expandMethod = null;
8623
8879
  if (compiled2) {
8624
8880
  finalPrompt = compiled2.prompt;
8625
8881
  referenceImages = compiled2.referenceImages;
@@ -8666,9 +8922,18 @@ function buildServer(opts) {
8666
8922
  }
8667
8923
  if (reshape === "extend" && !expandPlan)
8668
8924
  return reply.status(400).send({ error: "the picture is already this shape" });
8669
- const canOutpaint = expandPlan ? engine.capabilities().supportsOutpaint === true : false;
8925
+ if (expandPlan && srcMeta.width && srcMeta.height) {
8926
+ const size = { width: srcMeta.width, height: srcMeta.height };
8927
+ expandPlan = placeExpand(expandPlan, size, await subjectFraction(srcBuf, size, expandPlan.axis));
8928
+ }
8929
+ if (expandPlan) {
8930
+ const route = await resolveOutpaintRoute(engines.all(), engine);
8931
+ runEngine = route.engine;
8932
+ expandMethod = route.method;
8933
+ }
8934
+ const canOutpaint2 = expandMethod === "outpaint";
8670
8935
  if (expandPlan) {
8671
- if (!canOutpaint) {
8936
+ if (!canOutpaint2) {
8672
8937
  const canvas = await expandCanvas(srcBuf, expandPlan);
8673
8938
  expandSourceHash = core.images.save(canvas);
8674
8939
  }
@@ -8685,7 +8950,7 @@ function buildServer(opts) {
8685
8950
  ...expandPlan ? { width: expandPlan.width, height: expandPlan.height } : {},
8686
8951
  // Only an engine that can genuinely paint a margin is told where the
8687
8952
  // picture sits; the rest would ignore it anyway.
8688
- ...expandPlan && canOutpaint ? {
8953
+ ...expandPlan && canOutpaint2 ? {
8689
8954
  expand: {
8690
8955
  left: expandPlan.left,
8691
8956
  top: expandPlan.top,
@@ -8700,40 +8965,59 @@ function buildServer(opts) {
8700
8965
  seed: seedFor(String(editedFrom ?? srcHash), expandPlan.width, expandPlan.height)
8701
8966
  } : {}
8702
8967
  };
8703
- estimate = await engine.costEstimate(editReq);
8968
+ estimate = await runEngine.costEstimate(editReq);
8704
8969
  const plan2 = expandPlan;
8705
8970
  const srcSize = { width: srcMeta.width ?? 0, height: srcMeta.height ?? 0 };
8706
8971
  const original2 = srcBuf;
8707
- work = plan2 && !canOutpaint ? async (signal) => {
8972
+ work = plan2 && !canOutpaint2 ? async (signal) => {
8708
8973
  const draws = await Promise.all([
8709
- engine.edit(editReq, signal),
8710
- engine.edit(editReq, signal).catch(() => null)
8974
+ runEngine.edit(editReq, signal),
8975
+ runEngine.edit(editReq, signal).catch(() => null)
8711
8976
  ]);
8712
8977
  const scored = await Promise.all(
8713
8978
  draws.map(async (got) => {
8714
8979
  const first = got?.images[0];
8715
8980
  if (!got || !first) return null;
8716
8981
  const { image } = await compositeExpand(core.images.read(first), original2, plan2);
8717
- return { got, score: await seamScore(image, plan2, srcSize) };
8982
+ const [score, residual] = await Promise.all([
8983
+ seamScore(image, plan2, srcSize),
8984
+ seamResidual(image, plan2, srcSize)
8985
+ ]);
8986
+ return { got, penalty: seamPenalty(score, residual) };
8718
8987
  })
8719
8988
  );
8720
- const best = scored.filter((x) => x !== null).sort((a, b) => a.score - b.score)[0];
8989
+ const best = scored.filter((x) => x !== null).sort((a, b) => a.penalty - b.penalty)[0];
8721
8990
  return best?.got ?? draws[0];
8722
- } : (signal) => engine.edit(editReq, signal);
8991
+ } : (signal) => runEngine.edit(editReq, signal);
8723
8992
  }
8724
- core.ledger.assertUnderCap(engine.capabilities().id, estimate + (reserved.get(engine.capabilities().id) ?? 0));
8993
+ const billedId = runEngine.capabilities().id;
8994
+ core.ledger.assertUnderCap(billedId, estimate + (reserved.get(billedId) ?? 0));
8725
8995
  const node = core.store.addNode({
8726
8996
  projectId: project.id,
8727
8997
  parentId: resolvedParentId,
8728
8998
  kind,
8729
8999
  prompt: finalPrompt,
8730
- engineId: String(engineId)
9000
+ engineId: billedId
8731
9001
  });
8732
9002
  if (brief)
8733
9003
  core.store.setBrief(node.id, {
8734
9004
  ...brief,
8735
9005
  ...editedFrom ? { sourceImage: editedFrom } : {},
8736
9006
  ...kind === "edit" && reshape ? { reshape } : {},
9007
+ // How the margin was actually made, and by whom. An extend may be
9008
+ // handed to a different engine than the shot used, and a record that
9009
+ // does not say so cannot be read back later.
9010
+ ...expandMethod && expandPlan ? {
9011
+ expand: {
9012
+ method: expandMethod,
9013
+ engineId: billedId,
9014
+ // Where the protected picture sits in the frame it grew into.
9015
+ // Placement is no longer always centred, so a reader that
9016
+ // assumes it is would be looking in the wrong place.
9017
+ left: expandPlan.left,
9018
+ top: expandPlan.top
9019
+ }
9020
+ } : {},
8737
9021
  // What the refinement carried, recorded apart from what it asked for:
8738
9022
  // the detail view shows both, and remix reads tokens alone.
8739
9023
  ...kind === "edit" && inheritedTokens.length ? { inherited: inheritedTokens } : {}
@@ -8765,7 +9049,7 @@ function buildServer(opts) {
8765
9049
  }
8766
9050
  return out;
8767
9051
  } : void 0;
8768
- void runNode(node.id, engine, estimate, work, expectShape, post).catch(
9052
+ void runNode(node.id, runEngine, estimate, work, expectShape, post).catch(
8769
9053
  (err) => app.log.error({ err }, "node run failed")
8770
9054
  );
8771
9055
  const allWarnings = [...compiled2?.warnings ?? [], ...extraWarnings];
@@ -9026,8 +9310,8 @@ async function verify() {
9026
9310
  const db = new Database2(":memory:");
9027
9311
  db.pragma("user_version");
9028
9312
  db.close();
9029
- const { default: sharp16 } = await import('sharp');
9030
- await sharp16({ create: { width: 1, height: 1, channels: 3, background: "#000" } }).png().toBuffer();
9313
+ const { default: sharp18 } = await import('sharp');
9314
+ await sharp18({ create: { width: 1, height: 1, channels: 3, background: "#000" } }).png().toBuffer();
9031
9315
  console.log(JSON.stringify({ ok: true, version: readMeta().version }));
9032
9316
  } catch (err) {
9033
9317
  console.log(JSON.stringify({ ok: false, error: String(err?.message ?? err) }));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "scenri",
3
- "version": "0.5.0",
3
+ "version": "0.5.1",
4
4
  "author": "Tony Gorb <hello@scenri.co>",
5
5
  "license": "AGPL-3.0-only",
6
6
  "type": "module",