scenri 0.7.1 → 0.7.2
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.7.2](https://github.com/tonygorb/Scenri/compare/v0.7.1...v0.7.2) (2026-08-30)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
* a reshape is planned at the engine budget, never upscaled ([a580adc](https://github.com/tonygorb/Scenri/commit/a580adce27c85ae31b9071d9ff5411cb9d06bdaa))
|
|
9
|
+
* a reshape is planned at the engine budget, never upscaled ([2325779](https://github.com/tonygorb/Scenri/commit/2325779b32bdb83687135a21758953a1e3465eb2))
|
|
10
|
+
|
|
3
11
|
## [0.7.1](https://github.com/tonygorb/Scenri/compare/v0.7.0...v0.7.1) (2026-08-30)
|
|
4
12
|
|
|
5
13
|
|
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
|
-
|
|
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,21 @@ function registerImageRoutes(app, deps) {
|
|
|
8649
8726
|
|
|
8650
8727
|
// src/release/notes.data.ts
|
|
8651
8728
|
var RELEASES = [
|
|
8729
|
+
{
|
|
8730
|
+
version: "0.7.2",
|
|
8731
|
+
date: "2026-08-30",
|
|
8732
|
+
title: "Changing a shot to a new shape keeps the photograph.",
|
|
8733
|
+
sections: [
|
|
8734
|
+
{
|
|
8735
|
+
heading: "Create",
|
|
8736
|
+
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."
|
|
8737
|
+
},
|
|
8738
|
+
{
|
|
8739
|
+
heading: "Fixes",
|
|
8740
|
+
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."
|
|
8741
|
+
}
|
|
8742
|
+
]
|
|
8743
|
+
},
|
|
8652
8744
|
{
|
|
8653
8745
|
version: "0.7.1",
|
|
8654
8746
|
date: "2026-08-30",
|
|
@@ -10067,33 +10159,15 @@ function buildServer(opts) {
|
|
|
10067
10159
|
if (!project) return reply.status(404).send({ error: "project not found" });
|
|
10068
10160
|
const rawReshape = req.body.reshape ?? req.body.brief?.reshape;
|
|
10069
10161
|
const reshape = rawReshape === "crop" ? "crop" : rawReshape === "extend" ? "extend" : void 0;
|
|
10070
|
-
|
|
10071
|
-
const
|
|
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));
|
|
10162
|
+
const runCropNode = async (args) => {
|
|
10163
|
+
const plan2 = planCrop(args.srcSize, Number(args.fmt.w) / Number(args.fmt.h));
|
|
10090
10164
|
if (!plan2) return reply.status(400).send({ error: "the picture is already this shape" });
|
|
10091
|
-
const origin = await attentionCropOrigin(srcBuf,
|
|
10165
|
+
const origin = await attentionCropOrigin(args.srcBuf, args.srcSize, plan2);
|
|
10092
10166
|
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}`;
|
|
10167
|
+
const label = FORMATS.find((f) => f.id === args.fmt.id)?.label ?? `${args.fmt.w}x${args.fmt.h}`;
|
|
10094
10168
|
const node2 = core.store.addNode({
|
|
10095
10169
|
projectId: project.id,
|
|
10096
|
-
parentId:
|
|
10170
|
+
parentId: args.parentId,
|
|
10097
10171
|
kind: "edit",
|
|
10098
10172
|
prompt: `Cropped to ${label}`,
|
|
10099
10173
|
// No provider was asked; recording the engine the client HAPPENED to
|
|
@@ -10102,18 +10176,45 @@ function buildServer(opts) {
|
|
|
10102
10176
|
});
|
|
10103
10177
|
core.store.setBrief(node2.id, {
|
|
10104
10178
|
...briefInputsOnly(brief ?? {}),
|
|
10105
|
-
sourceImage:
|
|
10179
|
+
sourceImage: args.srcHash,
|
|
10106
10180
|
reshape: "crop",
|
|
10107
10181
|
crop: window
|
|
10108
10182
|
});
|
|
10109
10183
|
const work2 = async () => ({
|
|
10110
|
-
images: [core.images.save(await sharp20(srcBuf).extract(window).png().toBuffer())],
|
|
10184
|
+
images: [core.images.save(await sharp20(args.srcBuf).extract(window).png().toBuffer())],
|
|
10111
10185
|
costUsd: 0
|
|
10112
10186
|
});
|
|
10113
10187
|
void runNode(node2.id, null, 0, work2, { width: plan2.width, height: plan2.height }).catch(
|
|
10114
10188
|
(err) => app.log.error({ err }, "crop run failed")
|
|
10115
10189
|
);
|
|
10116
|
-
return reply.status(202).send(node2);
|
|
10190
|
+
return reply.status(202).send(args.note ? { ...node2, warnings: [args.note] } : node2);
|
|
10191
|
+
};
|
|
10192
|
+
if (kind === "edit" && reshape === "crop") {
|
|
10193
|
+
const rootForCrop = core.store.treeFor(project.id).find((n) => n.kind === "root");
|
|
10194
|
+
if (!rootForCrop) return reply.status(500).send({ error: "project has no root node" });
|
|
10195
|
+
const cropParentId = parentId ? String(parentId) : rootForCrop.id;
|
|
10196
|
+
if (brief && Array.isArray(brief.tokens)) {
|
|
10197
|
+
const briefErrors = validateBrief(brief);
|
|
10198
|
+
if (briefErrors.length) return reply.status(400).send({ error: `invalid brief: ${briefErrors.join("; ")}` });
|
|
10199
|
+
}
|
|
10200
|
+
const fmt = Array.isArray(brief?.tokens) ? brief.tokens.find(
|
|
10201
|
+
(t) => t.t === "format" && Number(t.w) > 0 && Number(t.h) > 0
|
|
10202
|
+
) : void 0;
|
|
10203
|
+
if (!fmt) return reply.status(400).send({ error: "a crop needs a target format" });
|
|
10204
|
+
const parent = core.store.getNode(cropParentId);
|
|
10205
|
+
const srcHash = req.body.sourceImage ?? parent?.images[0];
|
|
10206
|
+
if (!srcHash || !core.images.has(String(srcHash)))
|
|
10207
|
+
return reply.status(400).send({ error: "edit needs a parent node with an image (sourceImage)" });
|
|
10208
|
+
const srcBuf = core.images.read(String(srcHash));
|
|
10209
|
+
const srcMeta = await sharp20(srcBuf).metadata();
|
|
10210
|
+
if (!srcMeta.width || !srcMeta.height) return reply.status(400).send({ error: "source image unreadable" });
|
|
10211
|
+
return runCropNode({
|
|
10212
|
+
parentId: cropParentId,
|
|
10213
|
+
fmt,
|
|
10214
|
+
srcHash: String(srcHash),
|
|
10215
|
+
srcBuf,
|
|
10216
|
+
srcSize: { width: srcMeta.width, height: srcMeta.height }
|
|
10217
|
+
});
|
|
10117
10218
|
}
|
|
10118
10219
|
const engine = engines.get(String(engineId));
|
|
10119
10220
|
if (!engine) return reply.status(400).send({ error: `unknown engine ${engineId}` });
|
|
@@ -10134,6 +10235,9 @@ function buildServer(opts) {
|
|
|
10134
10235
|
let sentSize;
|
|
10135
10236
|
const extraWarnings = [];
|
|
10136
10237
|
let expandPlan = null;
|
|
10238
|
+
let expandSent = null;
|
|
10239
|
+
let expandAssist = null;
|
|
10240
|
+
let expandWorkHash = null;
|
|
10137
10241
|
let expandSourceHash = null;
|
|
10138
10242
|
if (brief && Array.isArray(brief.tokens)) {
|
|
10139
10243
|
const briefErrors = validateBrief(brief);
|
|
@@ -10288,14 +10392,41 @@ function buildServer(opts) {
|
|
|
10288
10392
|
width: srcMeta.width,
|
|
10289
10393
|
height: srcMeta.height
|
|
10290
10394
|
});
|
|
10291
|
-
|
|
10292
|
-
|
|
10395
|
+
let workBuf = srcBuf;
|
|
10396
|
+
let workSize = srcMeta.width && srcMeta.height ? { width: srcMeta.width, height: srcMeta.height } : null;
|
|
10397
|
+
if (reshapeIntended && workSize && compiled2?.width && compiled2?.height) {
|
|
10398
|
+
const targetRatio = compiled2.width / compiled2.height;
|
|
10399
|
+
const decision = classifyReshape(workSize, targetRatio, reshape);
|
|
10400
|
+
if (decision.op === "crop") {
|
|
10401
|
+
if (reshape === "extend")
|
|
10402
|
+
return reply.status(400).send({
|
|
10403
|
+
error: `growing a ${workSize.width}x${workSize.height} frame to this shape would invent more of the photograph than it keeps; crop instead`
|
|
10404
|
+
});
|
|
10405
|
+
const fmt = (brief?.tokens ?? []).find(
|
|
10406
|
+
(t) => t.t === "format" && Number(t.w) > 0 && Number(t.h) > 0
|
|
10407
|
+
);
|
|
10408
|
+
if (fmt)
|
|
10409
|
+
return runCropNode({
|
|
10410
|
+
parentId: resolvedParentId,
|
|
10411
|
+
fmt,
|
|
10412
|
+
srcHash: String(srcHash),
|
|
10413
|
+
srcBuf,
|
|
10414
|
+
srcSize: workSize,
|
|
10415
|
+
note: decision.forced ? "That shape is further than one extend can reach, so the picture was cropped to it instead." : void 0
|
|
10416
|
+
});
|
|
10417
|
+
} else if (decision.op === "extend") {
|
|
10418
|
+
if (decision.assist) {
|
|
10419
|
+
expandAssist = { width: decision.assist.width, height: decision.assist.height };
|
|
10420
|
+
workBuf = await sharp20(srcBuf).extract(decision.assist).png().toBuffer();
|
|
10421
|
+
workSize = { width: decision.assist.width, height: decision.assist.height };
|
|
10422
|
+
}
|
|
10423
|
+
expandPlan = planExpand(workSize, targetRatio);
|
|
10424
|
+
}
|
|
10293
10425
|
}
|
|
10294
10426
|
if (reshape === "extend" && !expandPlan)
|
|
10295
10427
|
return reply.status(400).send({ error: "the picture is already this shape" });
|
|
10296
|
-
if (expandPlan &&
|
|
10297
|
-
|
|
10298
|
-
expandPlan = placeExpand(expandPlan, size, await subjectFraction(srcBuf, size, expandPlan.axis));
|
|
10428
|
+
if (expandPlan && workSize) {
|
|
10429
|
+
expandPlan = placeExpand(expandPlan, workSize, await subjectFraction(workBuf, workSize, expandPlan.axis));
|
|
10299
10430
|
}
|
|
10300
10431
|
if (expandPlan) {
|
|
10301
10432
|
const route = await resolveOutpaintRoute(engines.all(), engine);
|
|
@@ -10303,11 +10434,25 @@ function buildServer(opts) {
|
|
|
10303
10434
|
expandMethod = route.method;
|
|
10304
10435
|
}
|
|
10305
10436
|
const canOutpaint2 = expandMethod === "outpaint";
|
|
10437
|
+
if (expandPlan && workSize) {
|
|
10438
|
+
const fit = fitExpandToBudget(expandPlan, workSize, runEngine.capabilities().editPixelBudget);
|
|
10439
|
+
if (fit.scale < 1) {
|
|
10440
|
+
expandPlan = fit.plan;
|
|
10441
|
+
workBuf = await sharp20(workBuf).resize(fit.source.width, fit.source.height, { fit: "fill", kernel: "lanczos3" }).png().toBuffer();
|
|
10442
|
+
workSize = fit.source;
|
|
10443
|
+
extraWarnings.push(
|
|
10444
|
+
`${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.`
|
|
10445
|
+
);
|
|
10446
|
+
}
|
|
10447
|
+
expandSent = workSize;
|
|
10448
|
+
}
|
|
10306
10449
|
let reframeSourceHash;
|
|
10307
10450
|
if (expandPlan) {
|
|
10308
10451
|
if (!canOutpaint2) {
|
|
10309
|
-
expandSourceHash = core.images.save(await expandCanvas(
|
|
10310
|
-
reframeSourceHash = core.images.save(await conditioningCanvas(
|
|
10452
|
+
expandSourceHash = core.images.save(await expandCanvas(workBuf, expandPlan));
|
|
10453
|
+
reframeSourceHash = core.images.save(await conditioningCanvas(workBuf, expandPlan, "edge"));
|
|
10454
|
+
} else {
|
|
10455
|
+
expandWorkHash = core.images.save(workBuf);
|
|
10311
10456
|
}
|
|
10312
10457
|
expectShape = { width: expandPlan.width, height: expandPlan.height };
|
|
10313
10458
|
}
|
|
@@ -10324,7 +10469,7 @@ function buildServer(opts) {
|
|
|
10324
10469
|
}
|
|
10325
10470
|
const editReq = {
|
|
10326
10471
|
instruction: expandPlan ? expandInstruction(expandPlan, finalPrompt) : finalPrompt,
|
|
10327
|
-
sourceImage: core.images.pathFor(String(expandSourceHash ?? budgetSourceHash ?? srcHash)),
|
|
10472
|
+
sourceImage: core.images.pathFor(String(expandSourceHash ?? expandWorkHash ?? budgetSourceHash ?? srcHash)),
|
|
10328
10473
|
brand: ctx,
|
|
10329
10474
|
...editRefs.length ? { referenceImages: editRefs.map((r) => r.path) } : {},
|
|
10330
10475
|
...editRefs.length ? { referenceRoles: editRefs.map((r) => r.role ?? "reference") } : {},
|
|
@@ -10341,8 +10486,10 @@ function buildServer(opts) {
|
|
|
10341
10486
|
expand: {
|
|
10342
10487
|
left: expandPlan.left,
|
|
10343
10488
|
top: expandPlan.top,
|
|
10344
|
-
|
|
10345
|
-
|
|
10489
|
+
// The sent copy's own size: after crop assist and the budget
|
|
10490
|
+
// fit these are the pixels the offsets actually refer to.
|
|
10491
|
+
width: workSize?.width ?? srcMeta.width ?? 0,
|
|
10492
|
+
height: workSize?.height ?? srcMeta.height ?? 0
|
|
10346
10493
|
},
|
|
10347
10494
|
// Derived from the picture and the shape asked for, so the same
|
|
10348
10495
|
// extend of the same shot is the same picture every time. Without
|
|
@@ -10354,8 +10501,8 @@ function buildServer(opts) {
|
|
|
10354
10501
|
};
|
|
10355
10502
|
estimate = await runEngine.costEstimate(editReq);
|
|
10356
10503
|
const plan2 = expandPlan;
|
|
10357
|
-
const srcSize = { width: srcMeta.width ?? 0, height: srcMeta.height ?? 0 };
|
|
10358
|
-
const original2 =
|
|
10504
|
+
const srcSize = workSize ?? { width: srcMeta.width ?? 0, height: srcMeta.height ?? 0 };
|
|
10505
|
+
const original2 = workBuf;
|
|
10359
10506
|
const reframeReq = plan2 && !canOutpaint2 && reframeSourceHash ? {
|
|
10360
10507
|
...editReq,
|
|
10361
10508
|
instruction: reframeInstruction(plan2, srcSize, finalPrompt),
|
|
@@ -10425,7 +10572,13 @@ function buildServer(opts) {
|
|
|
10425
10572
|
// Placement is no longer always centred, so a reader that
|
|
10426
10573
|
// assumes it is would be looking in the wrong place.
|
|
10427
10574
|
left: expandPlan.left,
|
|
10428
|
-
top: expandPlan.top
|
|
10575
|
+
top: expandPlan.top,
|
|
10576
|
+
// The planned frame and the size the photograph was sent at,
|
|
10577
|
+
// so requested-versus-drawn is a readable fact — and the
|
|
10578
|
+
// assist window when a slice of the other axis was given up.
|
|
10579
|
+
frame: [expandPlan.width, expandPlan.height],
|
|
10580
|
+
...expandSent ? { source: [expandSent.width, expandSent.height] } : {},
|
|
10581
|
+
...expandAssist ? { assist: [expandAssist.width, expandAssist.height] } : {}
|
|
10429
10582
|
}
|
|
10430
10583
|
} : {},
|
|
10431
10584
|
// What the refinement carried, recorded apart from what it asked for:
|
|
@@ -10498,7 +10651,8 @@ function buildServer(opts) {
|
|
|
10498
10651
|
{ nodeId: node.id, got: `${got.width}x${got.height}`, want: `${plan.width}x${plan.height}` },
|
|
10499
10652
|
"expand: engine size differs from plan"
|
|
10500
10653
|
);
|
|
10501
|
-
const
|
|
10654
|
+
const pasted = expandWorkHash ? core.images.read(expandWorkHash) : original;
|
|
10655
|
+
const { image, aligned } = await compositeExpand(answer, pasted, plan);
|
|
10502
10656
|
if (!aligned) app.log.warn({ nodeId: node.id }, "expand: engine frame did not align, kept the bed");
|
|
10503
10657
|
out.push(core.images.save(image));
|
|
10504
10658
|
}
|