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 +16 -0
- package/dist/serve.js +206 -42
- package/package.json +1 -1
- package/studio-dist/assets/{index-pvL9RRJ6.js → index-C-khfwCq.js} +21 -21
- package/studio-dist/index.html +1 -1
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
|
-
|
|
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
|
-
|
|
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));
|
|
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,
|
|
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:
|
|
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:
|
|
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
|
-
|
|
10292
|
-
|
|
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 &&
|
|
10297
|
-
|
|
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(
|
|
10310
|
-
reframeSourceHash = core.images.save(await conditioningCanvas(
|
|
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
|
-
|
|
10345
|
-
|
|
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 =
|
|
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
|
|
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
|
}
|