scenri 0.7.1 → 0.7.3

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,21 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.7.3](https://github.com/tonygorb/Scenri/compare/v0.7.2...v0.7.3) (2026-08-30)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * a node is done only after its rendered record exists ([88fbfb7](https://github.com/tonygorb/Scenri/commit/88fbfb7b53001e6f4bda12cc61258dbb21e353a4))
9
+ * a node is done only after its rendered record exists ([2e31d09](https://github.com/tonygorb/Scenri/commit/2e31d098ab78a22138e99aa463cae41199e51b6d))
10
+
11
+ ## [0.7.2](https://github.com/tonygorb/Scenri/compare/v0.7.1...v0.7.2) (2026-08-30)
12
+
13
+
14
+ ### Bug Fixes
15
+
16
+ * a reshape is planned at the engine budget, never upscaled ([a580adc](https://github.com/tonygorb/Scenri/commit/a580adce27c85ae31b9071d9ff5411cb9d06bdaa))
17
+ * a reshape is planned at the engine budget, never upscaled ([2325779](https://github.com/tonygorb/Scenri/commit/2325779b32bdb83687135a21758953a1e3465eb2))
18
+
3
19
  ## [0.7.1](https://github.com/tonygorb/Scenri/compare/v0.7.0...v0.7.1) (2026-08-30)
4
20
 
5
21
 
package/dist/serve.js CHANGED
@@ -3351,6 +3351,9 @@ function productFidelityDirective(attached) {
3351
3351
  }
3352
3352
  return "The attached product images all show the exact product to feature: preserve its label, shape and proportions faithfully, do not redesign it, and never treat an extra image as an additional product. The first product image is the authority for its color, finish and material. Where another image differs in color or finish, it shows the same product in another colorway \u2014 never blend colorways, and render the one the first image shows. Any face not visible in them is unknown \u2014 keep it plain and consistent with the materials the first image shows, and do not invent detail on it. If the direction above explicitly asks for more than one colorway, that explicit request wins.";
3353
3353
  }
3354
+ function extendPreservationDirective() {
3355
+ return "This grows the frame of a photograph that already exists; it does not restage it. The photograph in hand is the shot: the same person with the same face and the same clothing, the same product with the same label, geometry and colour, each at the same size, in the same place, under the same light. New area only continues the same scene past the original edges. Do not redesign the product, replace the person, change what anyone wears, or move the camera nearer or further away.";
3356
+ }
3354
3357
  function editPreservationDirective(scope, opts) {
3355
3358
  if (scope === "local") {
3356
3359
  const removal = opts?.removal ? " What is removed leaves nothing behind: the surface and the scene continue as if it had never been there, with no outline, silhouette, residue or ghost of it." : "";
@@ -3810,7 +3813,13 @@ function compileBrief(brief, ctx) {
3810
3813
  ] : [];
3811
3814
  const brandLines = brandRuleDirectives(ctx.brand);
3812
3815
  const preservation = ctx.mode === "edit" ? [
3813
- ...ctx.editReshape === "extend" ? [] : [editPreservationDirective(ctx.editScope ?? "global", { removal: ctx.editRemoval })],
3816
+ // An extend gets its own preservation language: the global
3817
+ // directive's "same framing, same dimensions" lines contradict a
3818
+ // frame that is deliberately growing, but dropping preservation
3819
+ // altogether left the redrawn-frame arm with nothing protecting
3820
+ // the person, the product or the wardrobe. See
3821
+ // extendPreservationDirective.
3822
+ ...ctx.editReshape === "extend" ? [extendPreservationDirective()] : [editPreservationDirective(ctx.editScope ?? "global", { removal: ctx.editRemoval })],
3814
3823
  ...ctx.inheritedIdentity ? [inheritedIdentityDirective(ctx.inheritedIdentity === true ? void 0 : ctx.inheritedIdentity)] : [],
3815
3824
  ...ctx.inheritedDirectives ?? []
3816
3825
  ] : [];
@@ -7057,6 +7066,74 @@ function planCrop(source, targetRatio) {
7057
7066
  const height = Math.max(1, Math.min(source.height, Math.round(source.width / targetRatio)));
7058
7067
  return { left: 0, top: Math.floor((source.height - height) / 2), width: source.width, height, axis: "height" };
7059
7068
  }
7069
+ function defaultReshapeOp(sourceRatio, targetRatio) {
7070
+ if (!(sourceRatio > 0 && targetRatio > 0)) return "extend";
7071
+ return Math.abs(Math.log(targetRatio)) < Math.abs(Math.log(sourceRatio)) - 0.01 ? "crop" : "extend";
7072
+ }
7073
+
7074
+ // src/outpaint/growth.ts
7075
+ var SINGLE_PASS_MAX = 1.5;
7076
+ var CROP_ASSIST_ABOVE = 2;
7077
+ var CROP_ASSIST_MAX = 0.15;
7078
+ var STAGE_MAX = 1.4;
7079
+ function planGrowth(source, targetRatio) {
7080
+ if (!(source.width > 0 && source.height > 0 && targetRatio > 0)) return null;
7081
+ const current = source.width / source.height;
7082
+ if (Math.abs(current - targetRatio) / targetRatio < 0.01) return null;
7083
+ const axis = targetRatio > current ? "width" : "height";
7084
+ const growth = axis === "width" ? targetRatio / current : current / targetRatio;
7085
+ let cropAssist = 0;
7086
+ if (growth > CROP_ASSIST_ABOVE) {
7087
+ const wanted = 1 - CROP_ASSIST_ABOVE / growth;
7088
+ cropAssist = Math.min(CROP_ASSIST_MAX, wanted);
7089
+ }
7090
+ const effective = growth * (1 - cropAssist);
7091
+ const stages = effective <= SINGLE_PASS_MAX ? 1 : Math.ceil(Math.log(effective) / Math.log(STAGE_MAX));
7092
+ return { growth, axis, stages, cropAssist, effective };
7093
+ }
7094
+ function cropAssistWindow(source, plan) {
7095
+ if (plan.cropAssist <= 0) return null;
7096
+ if (plan.axis === "width") {
7097
+ const height = Math.max(1, Math.round(source.height * (1 - plan.cropAssist)));
7098
+ return { left: 0, top: Math.floor((source.height - height) / 2), width: source.width, height };
7099
+ }
7100
+ const width = Math.max(1, Math.round(source.width * (1 - plan.cropAssist)));
7101
+ return { left: Math.floor((source.width - width) / 2), top: 0, width, height: source.height };
7102
+ }
7103
+
7104
+ // src/reshapeRules.ts
7105
+ var EXTEND_MAX = CROP_ASSIST_ABOVE;
7106
+ function classifyReshape(source, targetRatio, requested) {
7107
+ if (!(source.width > 0 && source.height > 0 && targetRatio > 0)) return { op: "none" };
7108
+ const growth = planGrowth(source, targetRatio);
7109
+ if (!growth) return { op: "none" };
7110
+ const op = requested ?? defaultReshapeOp(source.width / source.height, targetRatio);
7111
+ if (op === "crop") return { op: "crop", forced: false };
7112
+ if (growth.effective > EXTEND_MAX + 1e-9) return { op: "crop", forced: true, growth };
7113
+ return { op: "extend", growth, assist: cropAssistWindow(source, growth) };
7114
+ }
7115
+ function fitExpandToBudget(plan, source, pixelBudget) {
7116
+ if (!pixelBudget || plan.width * plan.height <= pixelBudget) return { plan, source, scale: 1 };
7117
+ const frame = budgetSize(plan.width, plan.height, pixelBudget);
7118
+ if (plan.axis === "width") {
7119
+ const scale2 = frame.height / plan.height;
7120
+ const width = Math.min(frame.width, Math.max(1, Math.round(source.width * scale2)));
7121
+ const left = Math.min(Math.max(0, Math.round(plan.left * scale2)), frame.width - width);
7122
+ return {
7123
+ plan: { width: frame.width, height: frame.height, left, top: 0, axis: "width" },
7124
+ source: { width, height: frame.height },
7125
+ scale: scale2
7126
+ };
7127
+ }
7128
+ const scale = frame.width / plan.width;
7129
+ const height = Math.min(frame.height, Math.max(1, Math.round(source.height * scale)));
7130
+ const top = Math.min(Math.max(0, Math.round(plan.top * scale)), frame.height - height);
7131
+ return {
7132
+ plan: { width: frame.width, height: frame.height, left: 0, top, axis: "height" },
7133
+ source: { width: frame.width, height },
7134
+ scale
7135
+ };
7136
+ }
7060
7137
  async function attentionCropOrigin(srcBuf, source, plan) {
7061
7138
  try {
7062
7139
  const { info } = await sharp20(srcBuf).resize(plan.width, plan.height, { fit: "cover", position: "attention" }).toBuffer({ resolveWithObject: true });
@@ -8649,6 +8726,31 @@ function registerImageRoutes(app, deps) {
8649
8726
 
8650
8727
  // src/release/notes.data.ts
8651
8728
  var RELEASES = [
8729
+ {
8730
+ version: "0.7.3",
8731
+ date: "2026-08-31",
8732
+ sections: [
8733
+ {
8734
+ heading: "Fixes",
8735
+ body: "A finished shot now reports done only after its delivered size is on record, so a tile can no longer guess its shape for a moment while the record catches up."
8736
+ }
8737
+ ]
8738
+ },
8739
+ {
8740
+ version: "0.7.2",
8741
+ date: "2026-08-30",
8742
+ title: "Changing a shot to a new shape keeps the photograph.",
8743
+ sections: [
8744
+ {
8745
+ heading: "Create",
8746
+ body: "Refining a shot into a different aspect ratio no longer costs it quality. The frame is planned at the size the engine can genuinely draw, so nothing is enlarged afterwards to fill a canvas its pixels could not reach. The presenter, the product, the wardrobe, the light and the subject scale carry across the new shape, and the stored size is now the size that was really drawn."
8747
+ },
8748
+ {
8749
+ heading: "Fixes",
8750
+ body: "A target shape that is tighter than the shot now crops it, instantly and without a generation, instead of building out around it. A shape too far from the current one to reach in a single step crops as well, and says so, rather than attempting a stretch that could not work. The composer tells you which of the two will happen before you run it."
8751
+ }
8752
+ ]
8753
+ },
8652
8754
  {
8653
8755
  version: "0.7.1",
8654
8756
  date: "2026-08-30",
@@ -10013,7 +10115,6 @@ function buildServer(opts) {
10013
10115
  result.images = await normalizePngs(result.images);
10014
10116
  if (post) result.images = await post(result.images);
10015
10117
  if (expect) await assertAspect(result.images, expect);
10016
- core.store.completeNode(nodeId, { ...result, durationMs: Date.now() - startedAt });
10017
10118
  try {
10018
10119
  const sizes = [];
10019
10120
  for (const h of result.images) {
@@ -10035,6 +10136,7 @@ function buildServer(opts) {
10035
10136
  }
10036
10137
  } catch {
10037
10138
  }
10139
+ core.store.completeNode(nodeId, { ...result, durationMs: Date.now() - startedAt });
10038
10140
  core.ledger.recordCost(engineId, nodeId, result.costUsd);
10039
10141
  } catch (err) {
10040
10142
  if (watchdogFired)
@@ -10067,33 +10169,15 @@ function buildServer(opts) {
10067
10169
  if (!project) return reply.status(404).send({ error: "project not found" });
10068
10170
  const rawReshape = req.body.reshape ?? req.body.brief?.reshape;
10069
10171
  const reshape = rawReshape === "crop" ? "crop" : rawReshape === "extend" ? "extend" : void 0;
10070
- if (kind === "edit" && reshape === "crop") {
10071
- const rootForCrop = core.store.treeFor(project.id).find((n) => n.kind === "root");
10072
- if (!rootForCrop) return reply.status(500).send({ error: "project has no root node" });
10073
- const cropParentId = parentId ? String(parentId) : rootForCrop.id;
10074
- if (brief && Array.isArray(brief.tokens)) {
10075
- const briefErrors = validateBrief(brief);
10076
- if (briefErrors.length) return reply.status(400).send({ error: `invalid brief: ${briefErrors.join("; ")}` });
10077
- }
10078
- const fmt = Array.isArray(brief?.tokens) ? brief.tokens.find(
10079
- (t) => t.t === "format" && Number(t.w) > 0 && Number(t.h) > 0
10080
- ) : void 0;
10081
- if (!fmt) return reply.status(400).send({ error: "a crop needs a target format" });
10082
- const parent = core.store.getNode(cropParentId);
10083
- const srcHash = req.body.sourceImage ?? parent?.images[0];
10084
- if (!srcHash || !core.images.has(String(srcHash)))
10085
- return reply.status(400).send({ error: "edit needs a parent node with an image (sourceImage)" });
10086
- const srcBuf = core.images.read(String(srcHash));
10087
- const srcMeta = await sharp20(srcBuf).metadata();
10088
- if (!srcMeta.width || !srcMeta.height) return reply.status(400).send({ error: "source image unreadable" });
10089
- const plan2 = planCrop({ width: srcMeta.width, height: srcMeta.height }, Number(fmt.w) / Number(fmt.h));
10172
+ const runCropNode = async (args) => {
10173
+ const plan2 = planCrop(args.srcSize, Number(args.fmt.w) / Number(args.fmt.h));
10090
10174
  if (!plan2) return reply.status(400).send({ error: "the picture is already this shape" });
10091
- const origin = await attentionCropOrigin(srcBuf, { width: srcMeta.width, height: srcMeta.height }, plan2);
10175
+ const origin = await attentionCropOrigin(args.srcBuf, args.srcSize, plan2);
10092
10176
  const window = { left: origin.left, top: origin.top, width: plan2.width, height: plan2.height };
10093
- const label = FORMATS.find((f) => f.id === fmt.id)?.label ?? `${fmt.w}x${fmt.h}`;
10177
+ const label = FORMATS.find((f) => f.id === args.fmt.id)?.label ?? `${args.fmt.w}x${args.fmt.h}`;
10094
10178
  const node2 = core.store.addNode({
10095
10179
  projectId: project.id,
10096
- parentId: cropParentId,
10180
+ parentId: args.parentId,
10097
10181
  kind: "edit",
10098
10182
  prompt: `Cropped to ${label}`,
10099
10183
  // No provider was asked; recording the engine the client HAPPENED to
@@ -10102,18 +10186,45 @@ function buildServer(opts) {
10102
10186
  });
10103
10187
  core.store.setBrief(node2.id, {
10104
10188
  ...briefInputsOnly(brief ?? {}),
10105
- sourceImage: String(srcHash),
10189
+ sourceImage: args.srcHash,
10106
10190
  reshape: "crop",
10107
10191
  crop: window
10108
10192
  });
10109
10193
  const work2 = async () => ({
10110
- images: [core.images.save(await sharp20(srcBuf).extract(window).png().toBuffer())],
10194
+ images: [core.images.save(await sharp20(args.srcBuf).extract(window).png().toBuffer())],
10111
10195
  costUsd: 0
10112
10196
  });
10113
10197
  void runNode(node2.id, null, 0, work2, { width: plan2.width, height: plan2.height }).catch(
10114
10198
  (err) => app.log.error({ err }, "crop run failed")
10115
10199
  );
10116
- return reply.status(202).send(node2);
10200
+ return reply.status(202).send(args.note ? { ...node2, warnings: [args.note] } : node2);
10201
+ };
10202
+ if (kind === "edit" && reshape === "crop") {
10203
+ const rootForCrop = core.store.treeFor(project.id).find((n) => n.kind === "root");
10204
+ if (!rootForCrop) return reply.status(500).send({ error: "project has no root node" });
10205
+ const cropParentId = parentId ? String(parentId) : rootForCrop.id;
10206
+ if (brief && Array.isArray(brief.tokens)) {
10207
+ const briefErrors = validateBrief(brief);
10208
+ if (briefErrors.length) return reply.status(400).send({ error: `invalid brief: ${briefErrors.join("; ")}` });
10209
+ }
10210
+ const fmt = Array.isArray(brief?.tokens) ? brief.tokens.find(
10211
+ (t) => t.t === "format" && Number(t.w) > 0 && Number(t.h) > 0
10212
+ ) : void 0;
10213
+ if (!fmt) return reply.status(400).send({ error: "a crop needs a target format" });
10214
+ const parent = core.store.getNode(cropParentId);
10215
+ const srcHash = req.body.sourceImage ?? parent?.images[0];
10216
+ if (!srcHash || !core.images.has(String(srcHash)))
10217
+ return reply.status(400).send({ error: "edit needs a parent node with an image (sourceImage)" });
10218
+ const srcBuf = core.images.read(String(srcHash));
10219
+ const srcMeta = await sharp20(srcBuf).metadata();
10220
+ if (!srcMeta.width || !srcMeta.height) return reply.status(400).send({ error: "source image unreadable" });
10221
+ return runCropNode({
10222
+ parentId: cropParentId,
10223
+ fmt,
10224
+ srcHash: String(srcHash),
10225
+ srcBuf,
10226
+ srcSize: { width: srcMeta.width, height: srcMeta.height }
10227
+ });
10117
10228
  }
10118
10229
  const engine = engines.get(String(engineId));
10119
10230
  if (!engine) return reply.status(400).send({ error: `unknown engine ${engineId}` });
@@ -10134,6 +10245,9 @@ function buildServer(opts) {
10134
10245
  let sentSize;
10135
10246
  const extraWarnings = [];
10136
10247
  let expandPlan = null;
10248
+ let expandSent = null;
10249
+ let expandAssist = null;
10250
+ let expandWorkHash = null;
10137
10251
  let expandSourceHash = null;
10138
10252
  if (brief && Array.isArray(brief.tokens)) {
10139
10253
  const briefErrors = validateBrief(brief);
@@ -10288,14 +10402,41 @@ function buildServer(opts) {
10288
10402
  width: srcMeta.width,
10289
10403
  height: srcMeta.height
10290
10404
  });
10291
- if (reshapeIntended && srcMeta.width && srcMeta.height && compiled2?.width && compiled2?.height) {
10292
- expandPlan = planExpand({ width: srcMeta.width, height: srcMeta.height }, compiled2.width / compiled2.height);
10405
+ let workBuf = srcBuf;
10406
+ let workSize = srcMeta.width && srcMeta.height ? { width: srcMeta.width, height: srcMeta.height } : null;
10407
+ if (reshapeIntended && workSize && compiled2?.width && compiled2?.height) {
10408
+ const targetRatio = compiled2.width / compiled2.height;
10409
+ const decision = classifyReshape(workSize, targetRatio, reshape);
10410
+ if (decision.op === "crop") {
10411
+ if (reshape === "extend")
10412
+ return reply.status(400).send({
10413
+ error: `growing a ${workSize.width}x${workSize.height} frame to this shape would invent more of the photograph than it keeps; crop instead`
10414
+ });
10415
+ const fmt = (brief?.tokens ?? []).find(
10416
+ (t) => t.t === "format" && Number(t.w) > 0 && Number(t.h) > 0
10417
+ );
10418
+ if (fmt)
10419
+ return runCropNode({
10420
+ parentId: resolvedParentId,
10421
+ fmt,
10422
+ srcHash: String(srcHash),
10423
+ srcBuf,
10424
+ srcSize: workSize,
10425
+ note: decision.forced ? "That shape is further than one extend can reach, so the picture was cropped to it instead." : void 0
10426
+ });
10427
+ } else if (decision.op === "extend") {
10428
+ if (decision.assist) {
10429
+ expandAssist = { width: decision.assist.width, height: decision.assist.height };
10430
+ workBuf = await sharp20(srcBuf).extract(decision.assist).png().toBuffer();
10431
+ workSize = { width: decision.assist.width, height: decision.assist.height };
10432
+ }
10433
+ expandPlan = planExpand(workSize, targetRatio);
10434
+ }
10293
10435
  }
10294
10436
  if (reshape === "extend" && !expandPlan)
10295
10437
  return reply.status(400).send({ error: "the picture is already this shape" });
10296
- if (expandPlan && srcMeta.width && srcMeta.height) {
10297
- const size = { width: srcMeta.width, height: srcMeta.height };
10298
- expandPlan = placeExpand(expandPlan, size, await subjectFraction(srcBuf, size, expandPlan.axis));
10438
+ if (expandPlan && workSize) {
10439
+ expandPlan = placeExpand(expandPlan, workSize, await subjectFraction(workBuf, workSize, expandPlan.axis));
10299
10440
  }
10300
10441
  if (expandPlan) {
10301
10442
  const route = await resolveOutpaintRoute(engines.all(), engine);
@@ -10303,11 +10444,25 @@ function buildServer(opts) {
10303
10444
  expandMethod = route.method;
10304
10445
  }
10305
10446
  const canOutpaint2 = expandMethod === "outpaint";
10447
+ if (expandPlan && workSize) {
10448
+ const fit = fitExpandToBudget(expandPlan, workSize, runEngine.capabilities().editPixelBudget);
10449
+ if (fit.scale < 1) {
10450
+ expandPlan = fit.plan;
10451
+ workBuf = await sharp20(workBuf).resize(fit.source.width, fit.source.height, { fit: "fill", kernel: "lanczos3" }).png().toBuffer();
10452
+ workSize = fit.source;
10453
+ extraWarnings.push(
10454
+ `${runEngine.capabilities().displayName} draws about ${((runEngine.capabilities().editPixelBudget ?? 0) / 1e6).toFixed(1)} megapixels, so this shape continues as a ${fit.plan.width}x${fit.plan.height} frame with the photograph riding inside it at ${fit.source.width}x${fit.source.height}. Nothing is upscaled; the stored size is the size the engine truly drew.`
10455
+ );
10456
+ }
10457
+ expandSent = workSize;
10458
+ }
10306
10459
  let reframeSourceHash;
10307
10460
  if (expandPlan) {
10308
10461
  if (!canOutpaint2) {
10309
- expandSourceHash = core.images.save(await expandCanvas(srcBuf, expandPlan));
10310
- reframeSourceHash = core.images.save(await conditioningCanvas(srcBuf, expandPlan, "edge"));
10462
+ expandSourceHash = core.images.save(await expandCanvas(workBuf, expandPlan));
10463
+ reframeSourceHash = core.images.save(await conditioningCanvas(workBuf, expandPlan, "edge"));
10464
+ } else {
10465
+ expandWorkHash = core.images.save(workBuf);
10311
10466
  }
10312
10467
  expectShape = { width: expandPlan.width, height: expandPlan.height };
10313
10468
  }
@@ -10324,7 +10479,7 @@ function buildServer(opts) {
10324
10479
  }
10325
10480
  const editReq = {
10326
10481
  instruction: expandPlan ? expandInstruction(expandPlan, finalPrompt) : finalPrompt,
10327
- sourceImage: core.images.pathFor(String(expandSourceHash ?? budgetSourceHash ?? srcHash)),
10482
+ sourceImage: core.images.pathFor(String(expandSourceHash ?? expandWorkHash ?? budgetSourceHash ?? srcHash)),
10328
10483
  brand: ctx,
10329
10484
  ...editRefs.length ? { referenceImages: editRefs.map((r) => r.path) } : {},
10330
10485
  ...editRefs.length ? { referenceRoles: editRefs.map((r) => r.role ?? "reference") } : {},
@@ -10341,8 +10496,10 @@ function buildServer(opts) {
10341
10496
  expand: {
10342
10497
  left: expandPlan.left,
10343
10498
  top: expandPlan.top,
10344
- width: srcMeta.width ?? 0,
10345
- height: srcMeta.height ?? 0
10499
+ // The sent copy's own size: after crop assist and the budget
10500
+ // fit these are the pixels the offsets actually refer to.
10501
+ width: workSize?.width ?? srcMeta.width ?? 0,
10502
+ height: workSize?.height ?? srcMeta.height ?? 0
10346
10503
  },
10347
10504
  // Derived from the picture and the shape asked for, so the same
10348
10505
  // extend of the same shot is the same picture every time. Without
@@ -10354,8 +10511,8 @@ function buildServer(opts) {
10354
10511
  };
10355
10512
  estimate = await runEngine.costEstimate(editReq);
10356
10513
  const plan2 = expandPlan;
10357
- const srcSize = { width: srcMeta.width ?? 0, height: srcMeta.height ?? 0 };
10358
- const original2 = srcBuf;
10514
+ const srcSize = workSize ?? { width: srcMeta.width ?? 0, height: srcMeta.height ?? 0 };
10515
+ const original2 = workBuf;
10359
10516
  const reframeReq = plan2 && !canOutpaint2 && reframeSourceHash ? {
10360
10517
  ...editReq,
10361
10518
  instruction: reframeInstruction(plan2, srcSize, finalPrompt),
@@ -10425,7 +10582,13 @@ function buildServer(opts) {
10425
10582
  // Placement is no longer always centred, so a reader that
10426
10583
  // assumes it is would be looking in the wrong place.
10427
10584
  left: expandPlan.left,
10428
- top: expandPlan.top
10585
+ top: expandPlan.top,
10586
+ // The planned frame and the size the photograph was sent at,
10587
+ // so requested-versus-drawn is a readable fact — and the
10588
+ // assist window when a slice of the other axis was given up.
10589
+ frame: [expandPlan.width, expandPlan.height],
10590
+ ...expandSent ? { source: [expandSent.width, expandSent.height] } : {},
10591
+ ...expandAssist ? { assist: [expandAssist.width, expandAssist.height] } : {}
10429
10592
  }
10430
10593
  } : {},
10431
10594
  // What the refinement carried, recorded apart from what it asked for:
@@ -10498,7 +10661,8 @@ function buildServer(opts) {
10498
10661
  { nodeId: node.id, got: `${got.width}x${got.height}`, want: `${plan.width}x${plan.height}` },
10499
10662
  "expand: engine size differs from plan"
10500
10663
  );
10501
- const { image, aligned } = await compositeExpand(answer, original, plan);
10664
+ const pasted = expandWorkHash ? core.images.read(expandWorkHash) : original;
10665
+ const { image, aligned } = await compositeExpand(answer, pasted, plan);
10502
10666
  if (!aligned) app.log.warn({ nodeId: node.id }, "expand: engine frame did not align, kept the bed");
10503
10667
  out.push(core.images.save(image));
10504
10668
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "scenri",
3
- "version": "0.7.1",
3
+ "version": "0.7.3",
4
4
  "author": "Tony Gorb <hello@scenri.co>",
5
5
  "license": "AGPL-3.0-only",
6
6
  "type": "module",