scenri 0.3.5 → 0.4.0
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/dist/serve.js
CHANGED
|
@@ -10,7 +10,7 @@ import fs, { realpathSync, existsSync, readFileSync, mkdirSync, chmodSync, write
|
|
|
10
10
|
import { fileURLToPath } from 'url';
|
|
11
11
|
import { readFile, copyFile, readdir, mkdtemp, rm, writeFile } from 'fs/promises';
|
|
12
12
|
import { spawn } from 'child_process';
|
|
13
|
-
import
|
|
13
|
+
import sharp6 from 'sharp';
|
|
14
14
|
import Fastify from 'fastify';
|
|
15
15
|
import fastifyStatic from '@fastify/static';
|
|
16
16
|
import fastifyMultipart from '@fastify/multipart';
|
|
@@ -702,8 +702,20 @@ function createStore(db) {
|
|
|
702
702
|
setKept(id, kept) {
|
|
703
703
|
db.prepare("UPDATE nodes SET kept=? WHERE id=?").run(kept ? 1 : 0, id);
|
|
704
704
|
},
|
|
705
|
+
/**
|
|
706
|
+
* Archiving also clears the keeper mark.
|
|
707
|
+
*
|
|
708
|
+
* The two flags were independent, and the Keepers lens reads the live list,
|
|
709
|
+
* so archiving a keeper removed it from Keepers and from the Keepers count
|
|
710
|
+
* without saying anything: the star stayed lit on a shot that was no longer
|
|
711
|
+
* in the shortlist it claimed to be in. Keepers is a live shortlist and
|
|
712
|
+
* archive means put away, so one clears the other and the two can never
|
|
713
|
+
* disagree. Restoring does not re-star: the judgement was made once and
|
|
714
|
+
* putting the shot back is not the same as making it again.
|
|
715
|
+
*/
|
|
705
716
|
setArchived(id, archived) {
|
|
706
|
-
db.prepare("UPDATE nodes SET archived
|
|
717
|
+
if (archived) db.prepare("UPDATE nodes SET archived=1, kept=0 WHERE id=?").run(id);
|
|
718
|
+
else db.prepare("UPDATE nodes SET archived=0 WHERE id=?").run(id);
|
|
707
719
|
},
|
|
708
720
|
/** Permanent. Orphans any children rather than blocking or cascading —
|
|
709
721
|
* same technique collapseProjects already uses for a surplus root. */
|
|
@@ -2252,15 +2264,29 @@ function createCodexEngine(opts) {
|
|
|
2252
2264
|
})
|
|
2253
2265
|
);
|
|
2254
2266
|
const results = new Array(count);
|
|
2267
|
+
const failures = [];
|
|
2255
2268
|
let next = 0;
|
|
2256
2269
|
const workers = Array.from({ length: Math.min(3, count) }, async () => {
|
|
2257
2270
|
while (next < count) {
|
|
2258
2271
|
const i = next++;
|
|
2259
|
-
|
|
2272
|
+
try {
|
|
2273
|
+
results[i] = await jobs[i]();
|
|
2274
|
+
} catch (err) {
|
|
2275
|
+
if (signal?.aborted) throw err;
|
|
2276
|
+
results[i] = [];
|
|
2277
|
+
failures.push(err);
|
|
2278
|
+
}
|
|
2260
2279
|
}
|
|
2261
2280
|
});
|
|
2262
2281
|
await Promise.all(workers);
|
|
2263
|
-
|
|
2282
|
+
const images = results.flat();
|
|
2283
|
+
if (!images.length && failures.length) throw failures[0];
|
|
2284
|
+
if (failures.length) {
|
|
2285
|
+
console.warn(
|
|
2286
|
+
`codex: ${failures.length} of ${count} variants failed, keeping ${images.length}: ${String(failures[0]?.message ?? failures[0])}`
|
|
2287
|
+
);
|
|
2288
|
+
}
|
|
2289
|
+
return { images, costUsd: 0 };
|
|
2264
2290
|
},
|
|
2265
2291
|
async edit(req, signal) {
|
|
2266
2292
|
return withWorkDir(async (dir) => {
|
|
@@ -2269,13 +2295,17 @@ function createCodexEngine(opts) {
|
|
|
2269
2295
|
const editRoles = req.referenceRoles ?? [];
|
|
2270
2296
|
const refLines = [];
|
|
2271
2297
|
for (let i = 0; i < editRefs.length; i++) {
|
|
2272
|
-
const role = editRoles[i] ?? "
|
|
2298
|
+
const role = editRoles[i] ?? "reference";
|
|
2273
2299
|
const name = `${role}-${i + 1}.png`;
|
|
2274
2300
|
await copyFile(editRefs[i], join(dir, name));
|
|
2275
2301
|
refLines.push(`${name} shows ${EDIT_REFERENCE_ROLE_DIRECTIVE[role]}`);
|
|
2276
2302
|
}
|
|
2277
2303
|
const promptText = `Edit input.png using your image generation/editing tool: ${req.instruction}.` + (refLines.length ? ` ${refLines.join(". ")}.` : "") + ` Do not browse the web or explore files. Save the result in the current directory as out-1.png (you may run the commands needed to save and resize it). Nothing else.`;
|
|
2278
|
-
|
|
2304
|
+
const args = execArgs(dir, promptText);
|
|
2305
|
+
for (const name of ["input.png", ...refLines.map((_, i) => `${editRoles[i] ?? "reference"}-${i + 1}.png`)]) {
|
|
2306
|
+
args.splice(args.length - 1, 0, `--image=${join(dir, name)}`);
|
|
2307
|
+
}
|
|
2308
|
+
await runCodex(args, signal);
|
|
2279
2309
|
const images = await collectImages(dir);
|
|
2280
2310
|
return { images, costUsd: 0 };
|
|
2281
2311
|
});
|
|
@@ -2330,7 +2360,7 @@ function createDemoEngine(saveImage) {
|
|
|
2330
2360
|
<text x="24" y="${h - 48}" font-family="Helvetica, Arial" font-size="${Math.max(14, Math.round(w / 42))}" fill="#ffffff" opacity="0.92">${esc(label)}</text>
|
|
2331
2361
|
<text x="24" y="${h - 22}" font-family="Helvetica, Arial" font-size="12" fill="#ffffff" opacity="0.6">Scenri demo engine</text>
|
|
2332
2362
|
</svg>`;
|
|
2333
|
-
return
|
|
2363
|
+
return sharp6(Buffer.from(svg)).png().toBuffer();
|
|
2334
2364
|
}
|
|
2335
2365
|
return {
|
|
2336
2366
|
capabilities() {
|
|
@@ -2493,7 +2523,7 @@ async function resolvePresenterImages(core, templatesRoot, presenter) {
|
|
|
2493
2523
|
for (const [slot, angle] of PRESENTER_ANGLES) {
|
|
2494
2524
|
const path = presenterRefPath(templatesRoot, presenter.id, slot);
|
|
2495
2525
|
if (!existsSync(path)) continue;
|
|
2496
|
-
const png = await
|
|
2526
|
+
const png = await sharp6(readFileSync(path)).png().toBuffer();
|
|
2497
2527
|
const hash = core.images.save(png);
|
|
2498
2528
|
shots.push({ file: `asset:${hash}`, angle, locked: true });
|
|
2499
2529
|
}
|
|
@@ -2587,7 +2617,7 @@ async function resolveDemoProductImages(core, templatesRoot, product) {
|
|
|
2587
2617
|
for (const angle of angles) {
|
|
2588
2618
|
const path = demoProductRefPath(templatesRoot, product.id, angle);
|
|
2589
2619
|
if (!existsSync(path)) continue;
|
|
2590
|
-
const png = await
|
|
2620
|
+
const png = await sharp6(readFileSync(path)).png().toBuffer();
|
|
2591
2621
|
const hash = core.images.save(png);
|
|
2592
2622
|
shots.push({ file: `asset:${hash}`, angle, locked: true });
|
|
2593
2623
|
}
|
|
@@ -2643,6 +2673,15 @@ function productFidelityDirective(attached) {
|
|
|
2643
2673
|
}
|
|
2644
2674
|
return "The attached product images all show the exact same product from different angles: preserve its label, shape and colors faithfully, do not redesign it, and do not treat the extra angles as additional products. Any face not visible in them is unknown \u2014 keep it plain and consistent with the visible materials, and do not invent detail on it.";
|
|
2645
2675
|
}
|
|
2676
|
+
function editPreservationDirective(scope) {
|
|
2677
|
+
if (scope === "local") {
|
|
2678
|
+
return "This is a change to a photograph that already exists, not a new photograph. Return the same image with one change made. Everything the instruction does not name comes back exactly as it is now: the same framing, the same crop, the same camera position, the same subject placement and pose, the same lighting, the same colours, the same background and the same dimensions. Do not re-render, re-stage, re-light or re-compose the picture. Change only what was asked for, together with the shadows, reflections and contact points that move with it.";
|
|
2679
|
+
}
|
|
2680
|
+
return "This is a change to a photograph that already exists, not a new photograph. Apply the instruction to the image you were given and keep what it does not name: the same subject and the same face, the same product with the same label, geometry and colour, and the same dimensions. Do not replace the subject and do not redesign the product.";
|
|
2681
|
+
}
|
|
2682
|
+
function inheritedIdentityDirective() {
|
|
2683
|
+
return "The extra attached references are the same product and the same person that are already in this picture. Use them to hold that identity exact while you make the change, not as a reason to re-stage the shot.";
|
|
2684
|
+
}
|
|
2646
2685
|
function shotSpecifiesCamera(text) {
|
|
2647
2686
|
return /\b\d{2,3}\s?mm\b|\bf\/\d|\blens\b|\bcamera\b|\bshot from\b|\beye[- ]level\b|\blow angle\b|\bhigh angle\b|\boverhead\b|\btop[- ]down\b|\bbird'?s[- ]eye\b|\bclose[- ]up\b|\bmacro\b|\bwide shot\b|\bcrop(?:ped)?\b|\bframing\b|\bdepth of field\b|\bbokeh\b|\bshallow (?:focus|depth)\b|\bdeep focus\b/i.test(
|
|
2648
2687
|
text
|
|
@@ -2810,7 +2849,7 @@ function compileBrief(brief, ctx) {
|
|
|
2810
2849
|
attachments.push({ role: "character", id: c.id, label: c.name, hash: chash, essential: i === 0 });
|
|
2811
2850
|
});
|
|
2812
2851
|
personDirectives.push(
|
|
2813
|
-
"The attached person reference is the same person every time: match their face, facial structure, skin, hair and build exactly. Their outfit, pose, background and lighting are neutral studio capture conditions, not styling direction: dress and style them for this shot, to a commercial standard, following any wardrobe the direction itself specifies."
|
|
2852
|
+
"The attached person reference is the same person every time: match their face, facial structure, skin, hair and build exactly. Their outfit, pose, background and lighting are neutral studio capture conditions, not styling direction: dress and style them for this shot, to a commercial standard, following any wardrobe the direction itself specifies. Where the direction specifies none, dress them for the place and the occasion the frame shows, and never return them to the plain base layers they were photographed in."
|
|
2814
2853
|
);
|
|
2815
2854
|
if (c.identityNotes) personDirectives.push(String(c.identityNotes));
|
|
2816
2855
|
if (c.negativeConstraints?.length)
|
|
@@ -2912,6 +2951,10 @@ function compileBrief(brief, ctx) {
|
|
|
2912
2951
|
"If the attached product is something a person wears, the presenter wears that exact product, with the rest of the outfit styled around it; otherwise the presenter presents or uses the product naturally."
|
|
2913
2952
|
] : [];
|
|
2914
2953
|
const brandLines = brandRuleDirectives(ctx.brand);
|
|
2954
|
+
const preservation = ctx.mode === "edit" ? [
|
|
2955
|
+
editPreservationDirective(ctx.editScope ?? "global"),
|
|
2956
|
+
...ctx.inheritedIdentity ? [inheritedIdentityDirective()] : []
|
|
2957
|
+
] : [];
|
|
2915
2958
|
const allDirectives = [
|
|
2916
2959
|
...productDirectives,
|
|
2917
2960
|
...personDirectives,
|
|
@@ -2919,7 +2962,8 @@ function compileBrief(brief, ctx) {
|
|
|
2919
2962
|
...otherDirectives,
|
|
2920
2963
|
...cameraDirectives,
|
|
2921
2964
|
...brandLines,
|
|
2922
|
-
...guard
|
|
2965
|
+
...guard,
|
|
2966
|
+
...preservation
|
|
2923
2967
|
];
|
|
2924
2968
|
if (allDirectives.length) prompt = `${prompt}${prompt.endsWith(".") ? "" : "."} ${dedupe(allDirectives).join(" ")}`;
|
|
2925
2969
|
const ROLE_PRIORITY = {
|
|
@@ -4912,8 +4956,8 @@ async function runJob(deps, jobId, brandId, url, signal) {
|
|
|
4912
4956
|
errors.push({ code: "image_empty", message: "Empty image", url: img.sourceUrl });
|
|
4913
4957
|
return;
|
|
4914
4958
|
}
|
|
4915
|
-
const png = await
|
|
4916
|
-
const meta = await
|
|
4959
|
+
const png = await sharp6(buf).rotate().png().toBuffer();
|
|
4960
|
+
const meta = await sharp6(png).metadata();
|
|
4917
4961
|
const hash = core.images.save(png);
|
|
4918
4962
|
core.catalog.setImageAsset(img.productId, img.sourceUrl, `asset:${hash}`, {
|
|
4919
4963
|
width: meta.width,
|
|
@@ -5323,7 +5367,7 @@ async function generateStudioSet(deps, job, who, sourcePaths, signal) {
|
|
|
5323
5367
|
return STUDIO_FRAMES.map((f) => byAngle.get(f.angle)).filter((h) => !!h);
|
|
5324
5368
|
}
|
|
5325
5369
|
async function edgeBarGeometry(buf) {
|
|
5326
|
-
const { data, info } = await
|
|
5370
|
+
const { data, info } = await sharp6(buf).greyscale().raw().toBuffer({ resolveWithObject: true });
|
|
5327
5371
|
const W = info.width;
|
|
5328
5372
|
const H = info.height;
|
|
5329
5373
|
const scan = (len, cross, at) => {
|
|
@@ -5377,7 +5421,7 @@ async function trimEdgeBars(core, hash) {
|
|
|
5377
5421
|
const width = g.right - g.left + 1;
|
|
5378
5422
|
const height = g.bottom - g.top + 1;
|
|
5379
5423
|
if (width < g.W * 0.6 || height < g.H * 0.6) return hash;
|
|
5380
|
-
const png = await
|
|
5424
|
+
const png = await sharp6(buf).extract({ left: g.left, top: g.top, width, height }).png().toBuffer();
|
|
5381
5425
|
return core.images.save(png);
|
|
5382
5426
|
} catch {
|
|
5383
5427
|
return hash;
|
|
@@ -5399,11 +5443,11 @@ async function avatarCrop(core, hash) {
|
|
|
5399
5443
|
async function crop(core, hash, region) {
|
|
5400
5444
|
if (!hash || !core.images.has(hash)) return void 0;
|
|
5401
5445
|
try {
|
|
5402
|
-
const meta = await
|
|
5446
|
+
const meta = await sharp6(core.images.read(hash)).metadata();
|
|
5403
5447
|
const w = meta.width ?? 0;
|
|
5404
5448
|
const h = meta.height ?? 0;
|
|
5405
5449
|
if (!w || !h) return void 0;
|
|
5406
|
-
const png = await
|
|
5450
|
+
const png = await sharp6(core.images.read(hash)).extract(region(w, h)).png().toBuffer();
|
|
5407
5451
|
return core.images.save(png);
|
|
5408
5452
|
} catch {
|
|
5409
5453
|
return void 0;
|
|
@@ -5557,6 +5601,215 @@ function registerAccessGuard(app, opts = {}) {
|
|
|
5557
5601
|
}
|
|
5558
5602
|
});
|
|
5559
5603
|
}
|
|
5604
|
+
|
|
5605
|
+
// src/editIdentity.ts
|
|
5606
|
+
var MAX_HOPS = 8;
|
|
5607
|
+
var tokensOf = (node) => {
|
|
5608
|
+
const t = node?.brief?.tokens;
|
|
5609
|
+
return Array.isArray(t) ? t : [];
|
|
5610
|
+
};
|
|
5611
|
+
function inheritedIdentityTokens(parentId, getNode) {
|
|
5612
|
+
let id = parentId;
|
|
5613
|
+
for (let hop = 0; hop < MAX_HOPS && id; hop++) {
|
|
5614
|
+
const node = getNode(id);
|
|
5615
|
+
if (!node || node.kind === "root") return [];
|
|
5616
|
+
const identity = tokensOf(node).filter((t) => t.t === "product" || t.t === "character" || t.t === "mark");
|
|
5617
|
+
if (identity.length) return identity;
|
|
5618
|
+
id = node.parentId;
|
|
5619
|
+
}
|
|
5620
|
+
return [];
|
|
5621
|
+
}
|
|
5622
|
+
|
|
5623
|
+
// src/editScopeRules.ts
|
|
5624
|
+
var GLOBAL_CUES = [
|
|
5625
|
+
["light", /\b(light|lighting|lit|relight|exposure|white ?balance|backlit|shadows everywhere)\b/i],
|
|
5626
|
+
[
|
|
5627
|
+
"grade",
|
|
5628
|
+
/\b(grade|grading|colou?r ?grade|tone|tint|saturation|contrast|filmic|film stock|grain|black and white|monochrome|sepia)\b/i
|
|
5629
|
+
],
|
|
5630
|
+
["time", /\b(night|nighttime|daytime|dusk|dawn|sunset|sunrise|golden hour|midday|morning|evening)\b/i],
|
|
5631
|
+
["weather", /\b(rain|rainy|snow|snowy|fog|foggy|misty|storm|overcast|sunny)\b/i],
|
|
5632
|
+
["scene", /\b(scene|background|backdrop|environment|location|setting|studio|indoors|outdoors)\b/i],
|
|
5633
|
+
[
|
|
5634
|
+
"camera",
|
|
5635
|
+
/\b(angle|zoom|closer|wider|crop|reframe|recompose|framing|perspective|lens|\d{2,3} ?mm|shot from|low angle|high angle|overhead|top ?down|eye ?level)\b/i
|
|
5636
|
+
],
|
|
5637
|
+
["mood", /\b(mood|vibe|feel|editorial|cinematic|dramatic|moody|minimal|luxurious|playful|clinical)\b/i],
|
|
5638
|
+
[
|
|
5639
|
+
"comparative",
|
|
5640
|
+
/\b(warmer|cooler|brighter|darker|softer|harder|punchier|richer|flatter|sharper|moodier|more|less)\b/i
|
|
5641
|
+
],
|
|
5642
|
+
[
|
|
5643
|
+
"restage",
|
|
5644
|
+
/\b(regenerate|redo|re-?do|start over|another take|different (take|composition|version)|try again|new (version|take))\b/i
|
|
5645
|
+
],
|
|
5646
|
+
["whole", /\b(overall|whole (image|frame|shot|thing)|entire|everything|all of it|throughout)\b/i],
|
|
5647
|
+
["wardrobe", /\b(outfit|wardrobe|clothes|clothing|dress(ed)?|styling)\b/i],
|
|
5648
|
+
["pose", /\b(pose|posture|expression|smile|smiling|looking)\b/i]
|
|
5649
|
+
];
|
|
5650
|
+
var LOCAL_VERB = /\b(add|remove|delete|erase|take out|get rid of|replace|swap|clean up|fix|repair|straighten|hide|cover)\b/i;
|
|
5651
|
+
var DEFINITE_OBJECT = /\b(the|that|this|his|her|their|its|a|an|one)\b/i;
|
|
5652
|
+
var REGION_CUE = /\b(in the (top|bottom|upper|lower|left|right)|on the (left|right|label|cap|lid|sleeve|table|floor|wall|shelf)|behind|next to|beside|in front of|to the (left|right)|corner|foreground|background object)\b/i;
|
|
5653
|
+
var COORDINATION = /\b(and|then|also|plus)\b|[;]/i;
|
|
5654
|
+
var MAX_LOCAL_WORDS = 16;
|
|
5655
|
+
function scopeOfInstruction(text) {
|
|
5656
|
+
const s = String(text ?? "").trim();
|
|
5657
|
+
if (!s) return { scope: "global", matched: ["empty"] };
|
|
5658
|
+
const globals = GLOBAL_CUES.filter(([, re]) => re.test(s)).map(([name]) => name);
|
|
5659
|
+
if (globals.length) return { scope: "global", matched: globals };
|
|
5660
|
+
const words = s.split(/\s+/).filter(Boolean);
|
|
5661
|
+
if (words.length > MAX_LOCAL_WORDS) return { scope: "global", matched: ["long"] };
|
|
5662
|
+
const clauses = s.split(COORDINATION).filter((c) => c.trim().length > 0);
|
|
5663
|
+
if (clauses.length > 1 && clauses.filter((c) => LOCAL_VERB.test(c)).length > 1) {
|
|
5664
|
+
return { scope: "global", matched: ["multiple"] };
|
|
5665
|
+
}
|
|
5666
|
+
const matched = [];
|
|
5667
|
+
if (REGION_CUE.test(s)) matched.push("region");
|
|
5668
|
+
if (LOCAL_VERB.test(s) && DEFINITE_OBJECT.test(s)) matched.push("verb+object");
|
|
5669
|
+
if (!matched.length) return { scope: "global", matched: ["no local cue"] };
|
|
5670
|
+
return { scope: "local", matched };
|
|
5671
|
+
}
|
|
5672
|
+
|
|
5673
|
+
// src/expandRules.ts
|
|
5674
|
+
var round8 = (n) => Math.max(8, Math.round(n / 8) * 8);
|
|
5675
|
+
function planExpand(source, targetRatio) {
|
|
5676
|
+
if (!(source.width > 0 && source.height > 0 && targetRatio > 0)) return null;
|
|
5677
|
+
const current = source.width / source.height;
|
|
5678
|
+
if (Math.abs(current - targetRatio) / targetRatio < 0.01) return null;
|
|
5679
|
+
if (targetRatio > current) {
|
|
5680
|
+
const width = round8(source.height * targetRatio);
|
|
5681
|
+
if (width <= source.width) return null;
|
|
5682
|
+
return {
|
|
5683
|
+
width,
|
|
5684
|
+
height: source.height,
|
|
5685
|
+
left: Math.round((width - source.width) / 2),
|
|
5686
|
+
top: 0,
|
|
5687
|
+
axis: "width"
|
|
5688
|
+
};
|
|
5689
|
+
}
|
|
5690
|
+
const height = round8(source.width / targetRatio);
|
|
5691
|
+
if (height <= source.height) return null;
|
|
5692
|
+
return {
|
|
5693
|
+
width: source.width,
|
|
5694
|
+
height,
|
|
5695
|
+
left: 0,
|
|
5696
|
+
top: Math.round((height - source.height) / 2),
|
|
5697
|
+
axis: "height"
|
|
5698
|
+
};
|
|
5699
|
+
}
|
|
5700
|
+
function expandInstruction(plan, direction) {
|
|
5701
|
+
const where = plan.axis === "width" ? "to the left and right" : "above and below";
|
|
5702
|
+
return `Extend this photograph ${where} to fill the empty margin, continuing the same scene, the same surface, the same lighting and the same perspective straight out to the new edges. Do not change, move, rescale or reinterpret anything already in the picture, and do not add a subject, a product or a person that is not already there.${direction.trim() ? ` ${direction.trim()}` : ""}`;
|
|
5703
|
+
}
|
|
5704
|
+
async function expandCanvas(source, plan) {
|
|
5705
|
+
const bed = await sharp6(source).resize(plan.width, plan.height, { fit: "cover", position: "centre" }).blur(Math.max(8, Math.round(Math.max(plan.width, plan.height) / 40))).toBuffer();
|
|
5706
|
+
return sharp6(bed).composite([{ input: source, left: plan.left, top: plan.top }]).png().toBuffer();
|
|
5707
|
+
}
|
|
5708
|
+
async function compositeExpand(engineImage, source, plan) {
|
|
5709
|
+
const meta = await sharp6(engineImage).metadata();
|
|
5710
|
+
const want = plan.width / plan.height;
|
|
5711
|
+
const got = meta.width && meta.height ? meta.width / meta.height : 0;
|
|
5712
|
+
const sameOrientation = got > 0 && got >= 1 === want >= 1;
|
|
5713
|
+
const aligned = sameOrientation;
|
|
5714
|
+
const surround = aligned ? await sharp6(engineImage).resize(plan.width, plan.height, { fit: "cover", position: "centre" }).toBuffer() : await expandCanvasBedOnly(source, plan);
|
|
5715
|
+
const image = await sharp6(surround).composite([{ input: source, left: plan.left, top: plan.top }]).png().toBuffer();
|
|
5716
|
+
return { image, aligned };
|
|
5717
|
+
}
|
|
5718
|
+
async function expandCanvasBedOnly(source, plan) {
|
|
5719
|
+
return sharp6(source).resize(plan.width, plan.height, { fit: "cover", position: "centre" }).blur(Math.max(8, Math.round(Math.max(plan.width, plan.height) / 40))).toBuffer();
|
|
5720
|
+
}
|
|
5721
|
+
async function driftDiff(a, b) {
|
|
5722
|
+
const metaA = await sharp6(a).metadata();
|
|
5723
|
+
const metaB = await sharp6(b).metadata();
|
|
5724
|
+
const width = Math.min(metaA.width ?? 1, metaB.width ?? 1, 1024);
|
|
5725
|
+
const height = Math.min(metaA.height ?? 1, metaB.height ?? 1, 1024);
|
|
5726
|
+
const [rawA, rawB] = await Promise.all(
|
|
5727
|
+
[a, b].map((buf) => sharp6(buf).resize(width, height, { fit: "cover" }).ensureAlpha().raw().toBuffer())
|
|
5728
|
+
);
|
|
5729
|
+
const out = new PNG({ width, height });
|
|
5730
|
+
const changed = pixelmatch(rawA, rawB, out.data, width, height, { threshold: 0.1, diffColor: [255, 64, 64] });
|
|
5731
|
+
return {
|
|
5732
|
+
score: changed / (width * height),
|
|
5733
|
+
heatmap: PNG.sync.write(out),
|
|
5734
|
+
width,
|
|
5735
|
+
height
|
|
5736
|
+
};
|
|
5737
|
+
}
|
|
5738
|
+
async function changeMask(a, b, cap2 = 1024) {
|
|
5739
|
+
const metaA = await sharp6(a).metadata();
|
|
5740
|
+
const width = Math.min(metaA.width ?? 1, cap2);
|
|
5741
|
+
const height = Math.min(metaA.height ?? 1, cap2);
|
|
5742
|
+
const [rawA, rawB] = await Promise.all(
|
|
5743
|
+
[a, b].map((buf) => sharp6(buf).resize(width, height, { fit: "fill" }).ensureAlpha().raw().toBuffer())
|
|
5744
|
+
);
|
|
5745
|
+
const out = new PNG({ width, height });
|
|
5746
|
+
pixelmatch(rawA, rawB, out.data, width, height, { threshold: 0.1, includeAA: true, diffMask: true });
|
|
5747
|
+
const mask = Buffer.alloc(width * height);
|
|
5748
|
+
let changed = 0;
|
|
5749
|
+
let minX = width;
|
|
5750
|
+
let minY = height;
|
|
5751
|
+
let maxX = -1;
|
|
5752
|
+
let maxY = -1;
|
|
5753
|
+
for (let i = 0; i < width * height; i++) {
|
|
5754
|
+
if (out.data[i * 4 + 3] > 0) {
|
|
5755
|
+
mask[i] = 255;
|
|
5756
|
+
changed++;
|
|
5757
|
+
const x = i % width;
|
|
5758
|
+
const y = i / width | 0;
|
|
5759
|
+
if (x < minX) minX = x;
|
|
5760
|
+
if (x > maxX) maxX = x;
|
|
5761
|
+
if (y < minY) minY = y;
|
|
5762
|
+
if (y > maxY) maxY = y;
|
|
5763
|
+
}
|
|
5764
|
+
}
|
|
5765
|
+
const boxArea = maxX < 0 ? 0 : (maxX - minX + 1) * (maxY - minY + 1);
|
|
5766
|
+
return {
|
|
5767
|
+
mask,
|
|
5768
|
+
width,
|
|
5769
|
+
height,
|
|
5770
|
+
changed: changed / (width * height),
|
|
5771
|
+
spread: boxArea / (width * height)
|
|
5772
|
+
};
|
|
5773
|
+
}
|
|
5774
|
+
|
|
5775
|
+
// src/localEditRules.ts
|
|
5776
|
+
var MIN_CHANGED = 5e-4;
|
|
5777
|
+
var MAX_CHANGED = 0.25;
|
|
5778
|
+
var MAX_SPREAD = 0.85;
|
|
5779
|
+
function judgeChange(shape) {
|
|
5780
|
+
if (!(shape.changed > 0) || shape.changed < MIN_CHANGED) return "no-change";
|
|
5781
|
+
if (shape.changed > MAX_CHANGED) return "too-much-changed";
|
|
5782
|
+
if (shape.spread > MAX_SPREAD && shape.changed < 0.2) return "scattered";
|
|
5783
|
+
return "composited";
|
|
5784
|
+
}
|
|
5785
|
+
function dilationFor(longEdge) {
|
|
5786
|
+
return Math.max(6, Math.round(longEdge * 0.02));
|
|
5787
|
+
}
|
|
5788
|
+
|
|
5789
|
+
// src/localEdit.ts
|
|
5790
|
+
async function preserveOutsideChange(source, edited) {
|
|
5791
|
+
try {
|
|
5792
|
+
const srcMeta = await sharp6(source).metadata();
|
|
5793
|
+
const outMeta = await sharp6(edited).metadata();
|
|
5794
|
+
if (!srcMeta.width || !srcMeta.height || !outMeta.width || !outMeta.height)
|
|
5795
|
+
return { image: edited, outcome: "error", changed: 0 };
|
|
5796
|
+
const sameShape = Math.abs(outMeta.width / outMeta.height - srcMeta.width / srcMeta.height) / (srcMeta.width / srcMeta.height) <= 0.01;
|
|
5797
|
+
if (!sameShape) return { image: edited, outcome: "shape-changed", changed: 0 };
|
|
5798
|
+
const shape = await changeMask(source, edited);
|
|
5799
|
+
const outcome = judgeChange(shape);
|
|
5800
|
+
if (outcome !== "composited") return { image: edited, outcome, changed: shape.changed };
|
|
5801
|
+
const r = dilationFor(Math.max(shape.width, shape.height));
|
|
5802
|
+
const grown = await sharp6(shape.mask, { raw: { width: shape.width, height: shape.height, channels: 1 } }).blur(Math.max(1, r / 3)).threshold(1).blur(Math.max(2, r / 3)).resize(srcMeta.width, srcMeta.height, { fit: "fill", kernel: "cubic" }).toColourspace("b-w").raw().toBuffer();
|
|
5803
|
+
const editedRgb = await sharp6(edited).resize(srcMeta.width, srcMeta.height, { fit: "fill" }).removeAlpha().raw().toBuffer();
|
|
5804
|
+
const masked = await sharp6(editedRgb, {
|
|
5805
|
+
raw: { width: srcMeta.width, height: srcMeta.height, channels: 3 }
|
|
5806
|
+
}).joinChannel(grown, { raw: { width: srcMeta.width, height: srcMeta.height, channels: 1 } }).png().toBuffer();
|
|
5807
|
+
const image = await sharp6(source).removeAlpha().composite([{ input: masked }]).png().toBuffer();
|
|
5808
|
+
return { image, outcome: "composited", changed: shape.changed };
|
|
5809
|
+
} catch {
|
|
5810
|
+
return { image: edited, outcome: "error", changed: 0 };
|
|
5811
|
+
}
|
|
5812
|
+
}
|
|
5560
5813
|
function joinNames(labels) {
|
|
5561
5814
|
const uniq = [...new Set(labels)];
|
|
5562
5815
|
if (uniq.length <= 1) return uniq[0] ?? "";
|
|
@@ -5580,7 +5833,7 @@ var assetHash2 = (ref) => {
|
|
|
5580
5833
|
};
|
|
5581
5834
|
var LOGO_ROLES = ["primary", "mark", "wordmark", "monochrome", "alternate"];
|
|
5582
5835
|
var LOGO_BACKGROUNDS = ["light", "dark", "any"];
|
|
5583
|
-
var toPng = (buf) =>
|
|
5836
|
+
var toPng = (buf) => sharp6(buf).rotate().png().toBuffer();
|
|
5584
5837
|
var COST_PROBE = {
|
|
5585
5838
|
prompt: "",
|
|
5586
5839
|
brand: { brand: {}, assetPaths: {} },
|
|
@@ -5589,7 +5842,7 @@ var COST_PROBE = {
|
|
|
5589
5842
|
count: 1
|
|
5590
5843
|
};
|
|
5591
5844
|
var MARK_MAX_EDGE = 2048;
|
|
5592
|
-
var toMarkPng = (buf) =>
|
|
5845
|
+
var toMarkPng = (buf) => sharp6(buf, { density: 384 }).rotate().resize({ width: MARK_MAX_EDGE, height: MARK_MAX_EDGE, fit: "inside", withoutEnlargement: true }).png().toBuffer();
|
|
5593
5846
|
var readImagePart = async (core, req, normalize2) => {
|
|
5594
5847
|
const part = await req.file();
|
|
5595
5848
|
if (!part) return { error: "multipart file field required" };
|
|
@@ -5766,7 +6019,7 @@ async function vibrantColor(input) {
|
|
|
5766
6019
|
let data;
|
|
5767
6020
|
let channels;
|
|
5768
6021
|
try {
|
|
5769
|
-
const out = await
|
|
6022
|
+
const out = await sharp6(input).resize(48, 48, { fit: "inside" }).raw().toBuffer({ resolveWithObject: true });
|
|
5770
6023
|
data = out.data;
|
|
5771
6024
|
channels = out.info.channels;
|
|
5772
6025
|
} catch {
|
|
@@ -5789,7 +6042,7 @@ async function vibrantColor(input) {
|
|
|
5789
6042
|
const best = buckets.reduce((a, b) => b.score > a.score ? b : a, buckets[0]);
|
|
5790
6043
|
if (best.score <= 0) {
|
|
5791
6044
|
try {
|
|
5792
|
-
const { dominant } = await
|
|
6045
|
+
const { dominant } = await sharp6(input).stats();
|
|
5793
6046
|
return toHex(dominant.r, dominant.g, dominant.b);
|
|
5794
6047
|
} catch {
|
|
5795
6048
|
return null;
|
|
@@ -6321,23 +6574,6 @@ function registerCodexSetupRoutes(app, deps) {
|
|
|
6321
6574
|
}
|
|
6322
6575
|
});
|
|
6323
6576
|
}
|
|
6324
|
-
async function driftDiff(a, b) {
|
|
6325
|
-
const metaA = await sharp5(a).metadata();
|
|
6326
|
-
const metaB = await sharp5(b).metadata();
|
|
6327
|
-
const width = Math.min(metaA.width ?? 1, metaB.width ?? 1, 1024);
|
|
6328
|
-
const height = Math.min(metaA.height ?? 1, metaB.height ?? 1, 1024);
|
|
6329
|
-
const [rawA, rawB] = await Promise.all(
|
|
6330
|
-
[a, b].map((buf) => sharp5(buf).resize(width, height, { fit: "cover" }).ensureAlpha().raw().toBuffer())
|
|
6331
|
-
);
|
|
6332
|
-
const out = new PNG({ width, height });
|
|
6333
|
-
const changed = pixelmatch(rawA, rawB, out.data, width, height, { threshold: 0.1, diffColor: [255, 64, 64] });
|
|
6334
|
-
return {
|
|
6335
|
-
score: changed / (width * height),
|
|
6336
|
-
heatmap: PNG.sync.write(out),
|
|
6337
|
-
width,
|
|
6338
|
-
height
|
|
6339
|
-
};
|
|
6340
|
-
}
|
|
6341
6577
|
var EXPORT_PRESETS = [
|
|
6342
6578
|
{ id: "original", label: "Original", width: null, height: null },
|
|
6343
6579
|
{ id: "ig-post", label: "Instagram post 1080\xD71080", width: 1080, height: 1080 },
|
|
@@ -6349,7 +6585,7 @@ async function buildExportZip(image, baseName, presetIds) {
|
|
|
6349
6585
|
const chosen = EXPORT_PRESETS.filter((p) => presetIds.includes(p.id));
|
|
6350
6586
|
if (chosen.length === 0) throw new Error("No valid export presets selected");
|
|
6351
6587
|
for (const p of chosen) {
|
|
6352
|
-
const buf = p.width && p.height ? await
|
|
6588
|
+
const buf = p.width && p.height ? await sharp6(image).resize(p.width, p.height, { fit: "cover", position: "attention" }).png().toBuffer() : image;
|
|
6353
6589
|
zip.file(`${baseName}-${p.id}.png`, buf);
|
|
6354
6590
|
}
|
|
6355
6591
|
return zip.generateAsync({ type: "nodebuffer" });
|
|
@@ -6510,7 +6746,7 @@ function registerImageRoutes(app, deps) {
|
|
|
6510
6746
|
if (!part) return reply.status(400).send({ error: "multipart file field required" });
|
|
6511
6747
|
const buf = await part.toBuffer();
|
|
6512
6748
|
if (buf.length === 0) return reply.status(400).send({ error: "empty file" });
|
|
6513
|
-
const png = await
|
|
6749
|
+
const png = await sharp6(buf).rotate().png().toBuffer();
|
|
6514
6750
|
return { hash: core.images.save(png) };
|
|
6515
6751
|
});
|
|
6516
6752
|
app.post("/api/diff", async (req, reply) => {
|
|
@@ -6545,6 +6781,29 @@ function registerImageRoutes(app, deps) {
|
|
|
6545
6781
|
|
|
6546
6782
|
// src/release/notes.data.ts
|
|
6547
6783
|
var RELEASES = [
|
|
6784
|
+
{
|
|
6785
|
+
version: "0.4.0",
|
|
6786
|
+
date: "2026-08-23",
|
|
6787
|
+
title: "Refining a shot keeps the shot.",
|
|
6788
|
+
sections: [
|
|
6789
|
+
{
|
|
6790
|
+
heading: "Refining",
|
|
6791
|
+
body: "Asking for one change now makes one change. Adding a prop or removing an object keeps the rest of the photograph exactly as it was, down to the pixel, instead of returning a fresh interpretation of the same idea. A refinement also carries the product and the presenter it started from, so identity holds through a thread of edits, and a request that genuinely affects the whole frame, like new lighting or a different time of day, is still free to change it."
|
|
6792
|
+
},
|
|
6793
|
+
{
|
|
6794
|
+
heading: "Expand",
|
|
6795
|
+
body: "A finished shot can be grown into another shape. Choosing a new aspect ratio while refining extends the picture you have and generates only the new margin, so the original is kept at its own resolution rather than being replaced by a different take. Nothing is ever cropped to fit."
|
|
6796
|
+
},
|
|
6797
|
+
{
|
|
6798
|
+
heading: "Presenters",
|
|
6799
|
+
body: "The plain studio layers a presenter is photographed in no longer turn up as the outfit in a finished shot. Where the direction names no wardrobe, they are dressed for the place and the occasion in the frame."
|
|
6800
|
+
},
|
|
6801
|
+
{
|
|
6802
|
+
heading: "Fixes",
|
|
6803
|
+
body: "The row of takes under a shot no longer stretches portrait and landscape images into squares. A shot card states what it is in one row, so set names no longer print over the version count and the Refine button, and the keeper star can now be used to keep a shot rather than only to un-keep one. Photos uploaded from a phone are stored the right way up. A run that loses one variant keeps the others instead of throwing all of them away, and the resolution setting no longer promises pixel counts on an engine that renders at its own size."
|
|
6804
|
+
}
|
|
6805
|
+
]
|
|
6806
|
+
},
|
|
6548
6807
|
{
|
|
6549
6808
|
version: "0.3.5",
|
|
6550
6809
|
date: "2026-08-21",
|
|
@@ -7232,14 +7491,14 @@ function buildServer(opts) {
|
|
|
7232
7491
|
const out = [];
|
|
7233
7492
|
for (const h of images) {
|
|
7234
7493
|
const buf = core.images.read(h);
|
|
7235
|
-
out.push(buf.subarray(0, 8).equals(PNG_SIG) ? h : core.images.save(await
|
|
7494
|
+
out.push(buf.subarray(0, 8).equals(PNG_SIG) ? h : core.images.save(await sharp6(buf).png().toBuffer()));
|
|
7236
7495
|
}
|
|
7237
7496
|
return out;
|
|
7238
7497
|
}
|
|
7239
7498
|
async function assertAspect(images, expect) {
|
|
7240
7499
|
const want = expect.width / expect.height;
|
|
7241
7500
|
for (const h of images) {
|
|
7242
|
-
const meta2 = await
|
|
7501
|
+
const meta2 = await sharp6(core.images.read(h)).metadata();
|
|
7243
7502
|
if (!meta2.width || !meta2.height) continue;
|
|
7244
7503
|
const got = meta2.width / meta2.height;
|
|
7245
7504
|
if (Math.abs(got - want) / want > ASPECT_TOLERANCE)
|
|
@@ -7248,7 +7507,7 @@ function buildServer(opts) {
|
|
|
7248
7507
|
);
|
|
7249
7508
|
}
|
|
7250
7509
|
}
|
|
7251
|
-
async function runNode(nodeId, engine, estimate, work, expect) {
|
|
7510
|
+
async function runNode(nodeId, engine, estimate, work, expect, post) {
|
|
7252
7511
|
const engineId = engine.capabilities().id;
|
|
7253
7512
|
reserved.set(engineId, (reserved.get(engineId) ?? 0) + estimate);
|
|
7254
7513
|
const ctrl = new AbortController();
|
|
@@ -7256,6 +7515,7 @@ function buildServer(opts) {
|
|
|
7256
7515
|
try {
|
|
7257
7516
|
const result = await work(ctrl.signal);
|
|
7258
7517
|
result.images = await normalizePngs(result.images);
|
|
7518
|
+
if (post) result.images = await post(result.images);
|
|
7259
7519
|
if (expect) await assertAspect(result.images, expect);
|
|
7260
7520
|
core.store.completeNode(nodeId, result);
|
|
7261
7521
|
core.ledger.recordCost(engineId, nodeId, result.costUsd);
|
|
@@ -7296,6 +7556,12 @@ function buildServer(opts) {
|
|
|
7296
7556
|
const resolvedParentId = parentId ? String(parentId) : rootNode.id;
|
|
7297
7557
|
const ctx = brandContext(core, project.brandId);
|
|
7298
7558
|
let compiled2 = null;
|
|
7559
|
+
let inheritedTokens = [];
|
|
7560
|
+
let inheritedAttachments = [];
|
|
7561
|
+
let editScope = "global";
|
|
7562
|
+
const extraWarnings = [];
|
|
7563
|
+
let expandPlan = null;
|
|
7564
|
+
let expandSourceHash = null;
|
|
7299
7565
|
if (brief && Array.isArray(brief.tokens)) {
|
|
7300
7566
|
const briefErrors = validateBrief(brief);
|
|
7301
7567
|
if (briefErrors.length) return reply.status(400).send({ error: `invalid brief: ${briefErrors.join("; ")}` });
|
|
@@ -7313,14 +7579,34 @@ function buildServer(opts) {
|
|
|
7313
7579
|
brief.tokens
|
|
7314
7580
|
);
|
|
7315
7581
|
const sceneById = sceneFor(brandJson);
|
|
7582
|
+
if (kind === "edit") {
|
|
7583
|
+
const borrowed = inheritedIdentityTokens(resolvedParentId, (id) => core.store.getNode(id));
|
|
7584
|
+
if (borrowed.length) {
|
|
7585
|
+
const already = new Set(
|
|
7586
|
+
brief.tokens.filter((t) => t.t === "product" || t.t === "character" || t.t === "mark").map((t) => JSON.stringify(t))
|
|
7587
|
+
);
|
|
7588
|
+
inheritedTokens = borrowed.filter((t) => !already.has(JSON.stringify(t)));
|
|
7589
|
+
}
|
|
7590
|
+
editScope = scopeOfInstruction(
|
|
7591
|
+
brief.tokens.filter((t) => t.t === "text").map((t) => t.v).join(" ")
|
|
7592
|
+
).scope;
|
|
7593
|
+
}
|
|
7316
7594
|
compiled2 = compileBrief(brief, {
|
|
7317
7595
|
brand: brandJson,
|
|
7318
7596
|
images: core.images,
|
|
7319
7597
|
engineCaps: engine.capabilities(),
|
|
7320
7598
|
template: brief.templateId ? sceneById(String(brief.templateId)) : void 0,
|
|
7321
|
-
templateById: sceneById
|
|
7599
|
+
templateById: sceneById,
|
|
7600
|
+
...kind === "edit" ? { mode: "edit", editScope, inheritedIdentity: inheritedTokens.length > 0 } : {}
|
|
7322
7601
|
});
|
|
7323
7602
|
if (!compiled2.prompt.trim()) return reply.status(400).send({ error: "the brief is empty" });
|
|
7603
|
+
if (inheritedTokens.length) {
|
|
7604
|
+
const identity = compileBrief(
|
|
7605
|
+
{ tokens: inheritedTokens },
|
|
7606
|
+
{ brand: brandJson, images: core.images, engineCaps: engine.capabilities(), templateById: sceneById }
|
|
7607
|
+
);
|
|
7608
|
+
inheritedAttachments = identity.attachments.filter((a) => a.essential);
|
|
7609
|
+
}
|
|
7324
7610
|
}
|
|
7325
7611
|
let finalPrompt = String(prompt ?? "");
|
|
7326
7612
|
let referenceImages;
|
|
@@ -7400,13 +7686,31 @@ function buildServer(opts) {
|
|
|
7400
7686
|
return reply.status(400).send({ error: "edit needs a parent node with an image (sourceImage)" });
|
|
7401
7687
|
if (!engine.capabilities().supportsEdit)
|
|
7402
7688
|
return reply.status(400).send({ error: "engine does not support edits" });
|
|
7403
|
-
const cap2 = engine.capabilities().maxReferenceImages;
|
|
7689
|
+
const cap2 = Math.max(0, engine.capabilities().maxReferenceImages - 1);
|
|
7690
|
+
const own = (referenceImages ?? []).map((path, i) => ({ path, role: referenceRoles?.[i] }));
|
|
7691
|
+
const borrowedRefs = inheritedAttachments.map((a) => ({ path: core.images.pathFor(a.hash), role: a.role })).filter((r) => !own.some((o) => o.path === r.path));
|
|
7692
|
+
const editRefs = [...own, ...borrowedRefs].slice(0, cap2);
|
|
7693
|
+
if (cap2 === 0 && borrowedRefs.length)
|
|
7694
|
+
extraWarnings.push(
|
|
7695
|
+
`${engine.capabilities().displayName} cannot carry reference images, so the identity rides on the source frame alone.`
|
|
7696
|
+
);
|
|
7697
|
+
const srcBuf = core.images.read(String(srcHash));
|
|
7698
|
+
const srcMeta = await sharp6(srcBuf).metadata();
|
|
7699
|
+
if (srcMeta.width && srcMeta.height) expectShape = { width: srcMeta.width, height: srcMeta.height };
|
|
7700
|
+
if (srcMeta.width && srcMeta.height && compiled2?.width && compiled2?.height) {
|
|
7701
|
+
expandPlan = planExpand({ width: srcMeta.width, height: srcMeta.height }, compiled2.width / compiled2.height);
|
|
7702
|
+
}
|
|
7703
|
+
if (expandPlan) {
|
|
7704
|
+
const canvas = await expandCanvas(srcBuf, expandPlan);
|
|
7705
|
+
expandSourceHash = core.images.save(canvas);
|
|
7706
|
+
expectShape = { width: expandPlan.width, height: expandPlan.height };
|
|
7707
|
+
}
|
|
7404
7708
|
const editReq = {
|
|
7405
|
-
instruction: finalPrompt,
|
|
7406
|
-
sourceImage: core.images.pathFor(String(srcHash)),
|
|
7709
|
+
instruction: expandPlan ? expandInstruction(expandPlan, finalPrompt) : finalPrompt,
|
|
7710
|
+
sourceImage: core.images.pathFor(String(expandSourceHash ?? srcHash)),
|
|
7407
7711
|
brand: ctx,
|
|
7408
|
-
...
|
|
7409
|
-
...
|
|
7712
|
+
...editRefs.length ? { referenceImages: editRefs.map((r) => r.path) } : {},
|
|
7713
|
+
...editRefs.length ? { referenceRoles: editRefs.map((r) => r.role ?? "reference") } : {}
|
|
7410
7714
|
};
|
|
7411
7715
|
estimate = await engine.costEstimate(editReq);
|
|
7412
7716
|
work = (signal) => engine.edit(editReq, signal);
|
|
@@ -7420,10 +7724,31 @@ function buildServer(opts) {
|
|
|
7420
7724
|
engineId: String(engineId)
|
|
7421
7725
|
});
|
|
7422
7726
|
if (brief) core.store.setBrief(node.id, editedFrom ? { ...brief, sourceImage: editedFrom } : brief);
|
|
7423
|
-
|
|
7727
|
+
const plan = expandPlan;
|
|
7728
|
+
const original = editedFrom ? core.images.read(editedFrom) : null;
|
|
7729
|
+
const localScope = kind === "edit" && !plan && editScope === "local" && original;
|
|
7730
|
+
const post = plan ? async (images) => {
|
|
7731
|
+
const out = [];
|
|
7732
|
+
for (const h of images) {
|
|
7733
|
+
const { image, aligned } = await compositeExpand(core.images.read(h), original, plan);
|
|
7734
|
+
if (!aligned) app.log.warn({ nodeId: node.id }, "expand: engine frame did not align, kept the bed");
|
|
7735
|
+
out.push(core.images.save(image));
|
|
7736
|
+
}
|
|
7737
|
+
return out;
|
|
7738
|
+
} : localScope ? async (images) => {
|
|
7739
|
+
const out = [];
|
|
7740
|
+
for (const h of images) {
|
|
7741
|
+
const { image, outcome, changed } = await preserveOutsideChange(original, core.images.read(h));
|
|
7742
|
+
app.log.info({ nodeId: node.id, outcome, changed }, "local edit");
|
|
7743
|
+
out.push(outcome === "composited" ? core.images.save(image) : h);
|
|
7744
|
+
}
|
|
7745
|
+
return out;
|
|
7746
|
+
} : void 0;
|
|
7747
|
+
void runNode(node.id, engine, estimate, work, expectShape, post).catch(
|
|
7424
7748
|
(err) => app.log.error({ err }, "node run failed")
|
|
7425
7749
|
);
|
|
7426
|
-
|
|
7750
|
+
const allWarnings = [...compiled2?.warnings ?? [], ...extraWarnings];
|
|
7751
|
+
return reply.status(202).send(allWarnings.length ? { ...node, warnings: allWarnings } : node);
|
|
7427
7752
|
});
|
|
7428
7753
|
app.post("/api/nodes/:id/cancel", async (req, reply) => {
|
|
7429
7754
|
const id = req.params.id;
|
|
@@ -7679,8 +8004,8 @@ async function verify() {
|
|
|
7679
8004
|
const db = new Database2(":memory:");
|
|
7680
8005
|
db.pragma("user_version");
|
|
7681
8006
|
db.close();
|
|
7682
|
-
const { default:
|
|
7683
|
-
await
|
|
8007
|
+
const { default: sharp14 } = await import('sharp');
|
|
8008
|
+
await sharp14({ create: { width: 1, height: 1, channels: 3, background: "#000" } }).png().toBuffer();
|
|
7684
8009
|
console.log(JSON.stringify({ ok: true, version: readMeta().version }));
|
|
7685
8010
|
} catch (err) {
|
|
7686
8011
|
console.log(JSON.stringify({ ok: false, error: String(err?.message ?? err) }));
|