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 +8 -0
- package/dist/serve.js +309 -25
- package/package.json +1 -1
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
|
|
1242
|
-
const shots =
|
|
1243
|
-
const hiddenShots =
|
|
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
|
|
6145
|
-
|
|
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
|
|
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] +
|
|
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
|
-
|
|
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 (!
|
|
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 &&
|
|
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
|
|
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 && !
|
|
8972
|
+
work = plan2 && !canOutpaint2 ? async (signal) => {
|
|
8708
8973
|
const draws = await Promise.all([
|
|
8709
|
-
|
|
8710
|
-
|
|
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
|
-
|
|
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.
|
|
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) =>
|
|
8991
|
+
} : (signal) => runEngine.edit(editReq, signal);
|
|
8723
8992
|
}
|
|
8724
|
-
|
|
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:
|
|
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,
|
|
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:
|
|
9030
|
-
await
|
|
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) }));
|