scenri 0.6.8 → 0.6.10
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 +34 -0
- package/dist/serve.js +385 -78
- package/package.json +1 -1
- package/studio-dist/assets/{index-DDkS-0PM.js → index-CGjGxrsJ.js} +27 -27
- package/studio-dist/index.html +1 -1
- package/templates/contour-key-portrait.json +1 -1
- package/templates/rembrandt-triangle-portrait.json +1 -1
package/dist/serve.js
CHANGED
|
@@ -1310,6 +1310,28 @@ var EDIT_REFERENCE_ROLE_DIRECTIVE = {
|
|
|
1310
1310
|
reference: "a reference for composition, lighting and treatment only"
|
|
1311
1311
|
};
|
|
1312
1312
|
var ASPECT_TOLERANCE = 0.15;
|
|
1313
|
+
var NAMED_RATIOS = [
|
|
1314
|
+
["1:1", 1],
|
|
1315
|
+
["4:5", 4 / 5],
|
|
1316
|
+
["5:4", 5 / 4],
|
|
1317
|
+
["2:3", 2 / 3],
|
|
1318
|
+
["3:2", 3 / 2],
|
|
1319
|
+
["3:4", 3 / 4],
|
|
1320
|
+
["4:3", 4 / 3],
|
|
1321
|
+
["9:16", 9 / 16],
|
|
1322
|
+
["16:9", 16 / 9],
|
|
1323
|
+
["2:1", 2],
|
|
1324
|
+
["1:2", 0.5]
|
|
1325
|
+
];
|
|
1326
|
+
function ratioLabel(width, height) {
|
|
1327
|
+
const ratio = width / height;
|
|
1328
|
+
for (const [label, value] of NAMED_RATIOS) {
|
|
1329
|
+
if (Math.abs(ratio - value) / value < 0.02) return label;
|
|
1330
|
+
}
|
|
1331
|
+
const gcd = (a, b) => b ? gcd(b, a % b) : a;
|
|
1332
|
+
const d = gcd(width, height) || 1;
|
|
1333
|
+
return `${Math.round(width / d)}:${Math.round(height / d)}`;
|
|
1334
|
+
}
|
|
1313
1335
|
|
|
1314
1336
|
// ../core/src/index.ts
|
|
1315
1337
|
function defaultHome() {
|
|
@@ -1483,7 +1505,10 @@ function createOpenRouterEngine(opts) {
|
|
|
1483
1505
|
...editRefs.map((p) => ({ type: "image_url", image_url: { url: dataUrl(p) } }))
|
|
1484
1506
|
]
|
|
1485
1507
|
}
|
|
1486
|
-
]
|
|
1508
|
+
],
|
|
1509
|
+
// Mirrors generate: an edit given no shape answered at whatever ratio
|
|
1510
|
+
// the model felt like, and the drift compounded down refine chains.
|
|
1511
|
+
...req.width && req.height ? { image_config: { aspect_ratio: aspectRatioFor(req.width, req.height) } } : {}
|
|
1487
1512
|
};
|
|
1488
1513
|
const json = await post(key, body, signal);
|
|
1489
1514
|
const [buf] = extractImages(json);
|
|
@@ -2476,6 +2501,27 @@ var CODEX_POOL = 2;
|
|
|
2476
2501
|
function codexNodeBudgetMs(count) {
|
|
2477
2502
|
return Math.ceil(Math.max(1, count) / CODEX_POOL) * DEFAULT_TIMEOUT_MS2 + 6e4;
|
|
2478
2503
|
}
|
|
2504
|
+
function orientationOf(width, height) {
|
|
2505
|
+
return width === height ? "square" : width > height ? "landscape" : "portrait";
|
|
2506
|
+
}
|
|
2507
|
+
var CODEX_PIXEL_BUDGET = 1572864;
|
|
2508
|
+
function codexNativeSize(width, height) {
|
|
2509
|
+
const ratio = width / height;
|
|
2510
|
+
if (!(ratio > 0) || !Number.isFinite(ratio)) return { width, height };
|
|
2511
|
+
return {
|
|
2512
|
+
width: Math.round(Math.sqrt(CODEX_PIXEL_BUDGET * ratio)),
|
|
2513
|
+
height: Math.round(Math.sqrt(CODEX_PIXEL_BUDGET / ratio))
|
|
2514
|
+
};
|
|
2515
|
+
}
|
|
2516
|
+
function refFileNames(roles, count) {
|
|
2517
|
+
const perRole = /* @__PURE__ */ new Map();
|
|
2518
|
+
return Array.from({ length: count }, (_, i) => {
|
|
2519
|
+
const role = roles[i] ?? "reference";
|
|
2520
|
+
const n = (perRole.get(role) ?? 0) + 1;
|
|
2521
|
+
perRole.set(role, n);
|
|
2522
|
+
return `${role}-${n}.png`;
|
|
2523
|
+
});
|
|
2524
|
+
}
|
|
2479
2525
|
function createCodexEngine(opts) {
|
|
2480
2526
|
const { saveImage } = opts;
|
|
2481
2527
|
const platform = opts.platform ?? process.platform;
|
|
@@ -2508,7 +2554,9 @@ function createCodexEngine(opts) {
|
|
|
2508
2554
|
}
|
|
2509
2555
|
const hashes = [];
|
|
2510
2556
|
for (const name of outFiles) {
|
|
2511
|
-
|
|
2557
|
+
const buf = await readFile(join(dir, name));
|
|
2558
|
+
if (buf.length === 0) throw new Error(`codex: ${name} is empty`);
|
|
2559
|
+
hashes.push(saveImage(buf));
|
|
2512
2560
|
}
|
|
2513
2561
|
return hashes;
|
|
2514
2562
|
}
|
|
@@ -2555,6 +2603,11 @@ function createCodexEngine(opts) {
|
|
|
2555
2603
|
* attached — and on an edit the first one attached is `input.png`, the
|
|
2556
2604
|
* shot being edited. A refine carrying a full identity payload was
|
|
2557
2605
|
* silently editing nothing at all.
|
|
2606
|
+
*
|
|
2607
|
+
* That same backwards walk is why every reference is bound to its role
|
|
2608
|
+
* by FILENAME (refFileNames) on both paths: the tool decides which
|
|
2609
|
+
* pictures it surfaces and in what order, so an ordinal "Attached
|
|
2610
|
+
* image N" binding pointed identity claims at the wrong picture.
|
|
2558
2611
|
*/
|
|
2559
2612
|
maxReferenceImages: 5,
|
|
2560
2613
|
/*
|
|
@@ -2586,8 +2639,9 @@ function createCodexEngine(opts) {
|
|
|
2586
2639
|
(_, i) => async () => withWorkDir(async (dir) => {
|
|
2587
2640
|
const args = execArgs(dir);
|
|
2588
2641
|
let refBytes = 0;
|
|
2642
|
+
const names = refFileNames(roles, refs.length);
|
|
2589
2643
|
for (const [idx, ref] of refs.entries()) {
|
|
2590
|
-
const dest = join(dir,
|
|
2644
|
+
const dest = join(dir, names[idx]);
|
|
2591
2645
|
await copyFile(ref, dest);
|
|
2592
2646
|
refBytes += (await stat(dest)).size;
|
|
2593
2647
|
args.splice(args.length - 1, 0, `--image=${dest}`);
|
|
@@ -2663,9 +2717,15 @@ function createCodexEngine(opts) {
|
|
|
2663
2717
|
await copyFile(editRefs[i], join(dir, name));
|
|
2664
2718
|
refLines.push(`${name} shows ${EDIT_REFERENCE_ROLE_DIRECTIVE[role]}`);
|
|
2665
2719
|
}
|
|
2666
|
-
const promptText = `Edit input.png using your image generation/editing tool: ${req.instruction}.` + (refLines.length ? ` ${refLines.join(". ")}.` : "") +
|
|
2667
|
-
//
|
|
2668
|
-
|
|
2720
|
+
const promptText = `Edit input.png using your image generation/editing tool: ${req.instruction}.` + (refLines.length ? ` ${refLines.join(". ")}.` : "") + // The old tail licensed "the commands needed to save and resize it"
|
|
2721
|
+
// and then asked for exactly WxH pixels - which the model honoured
|
|
2722
|
+
// with sips -z, a force-fit of BOTH axes. Every refine hop was a
|
|
2723
|
+
// cheap-kernel shell resample of freshly generated pixels, and when
|
|
2724
|
+
// the tool had drifted the shape it was a shear: the reported
|
|
2725
|
+
// crushed faces and the deep-chain mush. The server's own canvas
|
|
2726
|
+
// pass (enforceEditCanvas) owns size now, with one uniform lanczos
|
|
2727
|
+
// only when actually needed.
|
|
2728
|
+
(req.width && req.height ? ` Keep the edited frame at input.png's own ${ratioLabel(req.width, req.height)} shape, ${codexNativeSize(req.width, req.height).width}x${codexNativeSize(req.width, req.height).height}.` : "") + ` Do not browse the web or explore files. Save the tool's output in the current directory as out-1.png, byte-for-byte unchanged: you may run the commands needed to copy or move the file, but never resize, scale, stretch, pad, crop or re-encode it \u2014 deliver the tool's own pixels at the tool's own size. Nothing else.`;
|
|
2669
2729
|
const args = execArgs(dir);
|
|
2670
2730
|
for (const name of ["input.png", ...refLines.map((_, i) => `${editRoles[i] ?? "reference"}-${i + 1}.png`)]) {
|
|
2671
2731
|
args.splice(args.length - 1, 0, `--image=${join(dir, name)}`);
|
|
@@ -2689,10 +2749,30 @@ function createCodexEngine(opts) {
|
|
|
2689
2749
|
}
|
|
2690
2750
|
function buildPrompt2(req, index, roles) {
|
|
2691
2751
|
const roleDirective = REFERENCE_ROLE_DIRECTIVE;
|
|
2692
|
-
const
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
|
|
2752
|
+
const names = refFileNames(roles, roles.length);
|
|
2753
|
+
const refDirectives = roles.map((role, i) => `${names[i]} shows ${roleDirective[role]}.`).join(" ");
|
|
2754
|
+
const count = Math.max(1, req.count);
|
|
2755
|
+
const native = codexNativeSize(req.width, req.height);
|
|
2756
|
+
return (
|
|
2757
|
+
// "professional-grade", not "flawless": the audit of the waxy-presenter
|
|
2758
|
+
// report traced part of the plastic, over-perfected rendering to that
|
|
2759
|
+
// one unconditional word. Still "image", never "photograph" - this
|
|
2760
|
+
// wrapper also generates graphic assets. Independently revertible on
|
|
2761
|
+
// render evidence.
|
|
2762
|
+
`Generate one professional-grade image immediately using your image generation tool, composed as a ${native.width}x${native.height} frame (${ratioLabel(req.width, req.height)} ${orientationOf(req.width, req.height)}): ${req.prompt}.` + (refDirectives ? ` ${refDirectives}` : "") + // The save instruction bans what the old one licensed. "you may run the
|
|
2763
|
+
// commands needed to save and resize it" invited sips -z, which
|
|
2764
|
+
// force-fits BOTH axes: the model drew at one shape, sheared the pixels
|
|
2765
|
+
// to the requested one, and the aspect check passed BECAUSE of the shear
|
|
2766
|
+
// - the reported crushed faces. Copy/move stays licensed because the
|
|
2767
|
+
// win32 recovery path moves files out of generated_images.
|
|
2768
|
+
` Do not browse the web or explore files. Save the tool's output in the current directory as out-1.png, byte-for-byte unchanged: you may run the commands needed to copy or move the file, but never resize, scale, stretch, pad, crop or re-encode it \u2014 deliver the tool's own pixels at the tool's own size. Nothing else.` + // Every take in a batch gets the SAME-shaped clause. Take 1 used to get
|
|
2769
|
+
// nothing - so the first output was literally asked for the most
|
|
2770
|
+
// reference-faithful decode - and later takes were licensed to a
|
|
2771
|
+
// "different composition", which read as permission to drift from the
|
|
2772
|
+
// directives. Reported as: output #1 copies the scene reference, output
|
|
2773
|
+
// #2 mixes identities. A single generation stays byte-stable.
|
|
2774
|
+
(count > 1 ? ` (take ${index + 1} of ${count} \u2014 same brief, same identities and constraints, a naturally different moment and framing of the same shoot)` : "")
|
|
2775
|
+
);
|
|
2696
2776
|
}
|
|
2697
2777
|
}
|
|
2698
2778
|
|
|
@@ -2773,7 +2853,13 @@ function createDemoEngine(saveImage) {
|
|
|
2773
2853
|
},
|
|
2774
2854
|
async edit(req) {
|
|
2775
2855
|
const colors = paletteOf(req);
|
|
2776
|
-
const buf = await render(
|
|
2856
|
+
const buf = await render(
|
|
2857
|
+
colors,
|
|
2858
|
+
`edit: ${req.instruction}`,
|
|
2859
|
+
req.width ?? 1024,
|
|
2860
|
+
req.height ?? 1024,
|
|
2861
|
+
req.instruction.length
|
|
2862
|
+
);
|
|
2777
2863
|
return { images: [saveImage(buf)], costUsd: 0 };
|
|
2778
2864
|
}
|
|
2779
2865
|
};
|
|
@@ -2916,6 +3002,7 @@ async function resolvePresenterImages(core, templatesRoot, presenter) {
|
|
|
2916
3002
|
name: presenter.name,
|
|
2917
3003
|
...presenter.identityNotes ? { identityNotes: presenter.identityNotes } : {},
|
|
2918
3004
|
...presenter.negativeConstraints?.length ? { negativeConstraints: presenter.negativeConstraints } : {},
|
|
3005
|
+
...presenter.skin ? { skin: presenter.skin } : {},
|
|
2919
3006
|
shots
|
|
2920
3007
|
};
|
|
2921
3008
|
}
|
|
@@ -3019,7 +3106,14 @@ async function resolveDemoProductImages(core, templatesRoot, product) {
|
|
|
3019
3106
|
// brushed steel case read as steel rather than plastic. They were being
|
|
3020
3107
|
// dropped here, so the compiler never saw them.
|
|
3021
3108
|
...product.materials ? { materials: product.materials } : {},
|
|
3022
|
-
...product.primaryColors ? { primaryColors: product.primaryColors } : {}
|
|
3109
|
+
...product.primaryColors ? { primaryColors: product.primaryColors } : {},
|
|
3110
|
+
// The description is the only text a demo product has that carries scale
|
|
3111
|
+
// ("sized for a facial serum or treatment oil") - dropping it here is why
|
|
3112
|
+
// a cream jar could render the size of a basketball. And category is what
|
|
3113
|
+
// arms the compiler's apparel guard; without it no demo garment ever
|
|
3114
|
+
// earned the "present it as a product" line.
|
|
3115
|
+
...product.description ? { description: product.description } : {},
|
|
3116
|
+
...product.category ? { category: product.category } : {}
|
|
3023
3117
|
};
|
|
3024
3118
|
}
|
|
3025
3119
|
async function brandJsonWithResolvedDemoProducts(core, templatesRoot, demoProducts, brandJson, tokens) {
|
|
@@ -3127,6 +3221,38 @@ function editPreservationDirective(scope, opts) {
|
|
|
3127
3221
|
function inheritedIdentityDirective() {
|
|
3128
3222
|
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.";
|
|
3129
3223
|
}
|
|
3224
|
+
function productFactDirectives(p) {
|
|
3225
|
+
const out = [];
|
|
3226
|
+
if (p.preservationNotes) out.push(String(p.preservationNotes));
|
|
3227
|
+
if (p.negativeConstraints) out.push(`Avoid: ${p.negativeConstraints}`);
|
|
3228
|
+
const materials = p.materials ?? p.material;
|
|
3229
|
+
if (materials) out.push(`Its materials and finish: ${materials}.`);
|
|
3230
|
+
if (p.primaryColors) out.push(`Its actual colors: ${p.primaryColors}.`);
|
|
3231
|
+
if (p.dimensions)
|
|
3232
|
+
out.push(`Its real-world size is ${p.dimensions} \u2014 keep it at true scale relative to everything else in frame.`);
|
|
3233
|
+
return out;
|
|
3234
|
+
}
|
|
3235
|
+
function characterFactDirectives(c) {
|
|
3236
|
+
const out = [];
|
|
3237
|
+
if (c.identityNotes) out.push(String(c.identityNotes));
|
|
3238
|
+
if (c.negativeConstraints?.length) out.push(`Avoid: ${[].concat(c.negativeConstraints).join(", ")}`);
|
|
3239
|
+
return out;
|
|
3240
|
+
}
|
|
3241
|
+
function productEditFidelityDirective(name) {
|
|
3242
|
+
return `Every attached product reference shows ${name}, the exact product already in this photograph: hold its label, shape, colours and proportions to what the references and the photograph agree on while you make the change, and never treat an extra angle as a second product. Any face of it no reference shows stays exactly as the photograph already has it.`;
|
|
3243
|
+
}
|
|
3244
|
+
function characterEditIdentityDirective(name) {
|
|
3245
|
+
return `${name} is the person in this photograph: keep them present and clearly visible. Match their face, facial structure, skin, hair and build to the attached person reference exactly. The reference's plain outfit and studio backdrop are capture conditions, not direction: keep the styling this photograph already has unless the instruction itself changes it, and never return them to the plain base layers they were photographed in.`;
|
|
3246
|
+
}
|
|
3247
|
+
function markEditDirective() {
|
|
3248
|
+
return "The attached brand mark is this brand's own mark: wherever the logo appears or the instruction asks for it, reproduce it exactly as drawn \u2014 same colours, letterforms and proportions \u2014 never redrawn or re-lettered.";
|
|
3249
|
+
}
|
|
3250
|
+
function personSkinDirective() {
|
|
3251
|
+
return "Every person in this photograph has real photographed skin: fine natural texture at pore scale, faint lines and natural asymmetry left intact, true-to-life proportions, never airbrushed, waxy, plastic or synthetic-looking. Light behaves physically on it: a hard flash or a specular key may genuinely shine on skin, but the surface underneath stays living skin in a photograph, never gloss, never a render. Any retouch, clarity or sharpness language in the direction above is about finish and focus, never a licence to smooth skin beyond what a professional photograph holds; if the direction above explicitly asks for a stylised or non-photographic treatment of the person, that explicit request wins.";
|
|
3252
|
+
}
|
|
3253
|
+
function productHandlingDirective() {
|
|
3254
|
+
return "Where the presenter touches the product, they handle it the way a real person handles an object of exactly that size and weight: a natural grip, fingers wrapping in genuine contact with its surface, hand and wrist at anatomically real angles, the product supported by that grip rather than floating. If the direction above explicitly stages the product away from their hands or in an impossible way, that explicit direction wins.";
|
|
3255
|
+
}
|
|
3130
3256
|
function garmentDisplayDirective() {
|
|
3131
3257
|
return "No person is part of this brief. Present the garment as a product, laid, hung, folded or dressed on a plain form, never on a person, a partial figure or an invisible body, unless the direction above explicitly asks for it worn.";
|
|
3132
3258
|
}
|
|
@@ -3315,14 +3441,10 @@ function compileBrief(brief, ctx) {
|
|
|
3315
3441
|
attachments.push({ role: "product", id: p.id, label: p.name, hash: h, essential: i === 0 });
|
|
3316
3442
|
});
|
|
3317
3443
|
productDirectives.push(productFidelityDirective(phashes.length));
|
|
3318
|
-
|
|
3319
|
-
if (p.
|
|
3320
|
-
const materials = p.materials ?? p.material;
|
|
3321
|
-
if (materials) productDirectives.push(`Its materials and finish: ${materials}.`);
|
|
3322
|
-
if (p.primaryColors) productDirectives.push(`Its actual colors: ${p.primaryColors}.`);
|
|
3323
|
-
if (p.dimensions)
|
|
3444
|
+
productDirectives.push(...productFactDirectives(p));
|
|
3445
|
+
if (p.description && !p.dimensions)
|
|
3324
3446
|
productDirectives.push(
|
|
3325
|
-
`
|
|
3447
|
+
`What this object physically is: ${String(p.description).replace(/\.\s*$/, "")}. Keep it at that real size relative to hands, faces, furniture and everything else in frame.`
|
|
3326
3448
|
);
|
|
3327
3449
|
} else {
|
|
3328
3450
|
warnings.push(`${p.name} has no usable photo, so it is named but not attached.`);
|
|
@@ -3348,9 +3470,11 @@ function compileBrief(brief, ctx) {
|
|
|
3348
3470
|
personDirectives.push(
|
|
3349
3471
|
"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."
|
|
3350
3472
|
);
|
|
3351
|
-
|
|
3352
|
-
if (c.
|
|
3353
|
-
personDirectives.push(
|
|
3473
|
+
personDirectives.push(...characterFactDirectives(c));
|
|
3474
|
+
if (c.skin)
|
|
3475
|
+
personDirectives.push(
|
|
3476
|
+
`${c.promptName ?? c.name}'s skin, exactly as the reference photographs show it: ${c.skin}.`
|
|
3477
|
+
);
|
|
3354
3478
|
} else {
|
|
3355
3479
|
warnings.push(`${c.name} has no usable photo, so they are named but not attached.`);
|
|
3356
3480
|
}
|
|
@@ -3466,7 +3590,8 @@ function compileBrief(brief, ctx) {
|
|
|
3466
3590
|
// the compiler: fired only when a person is attached, worded like
|
|
3467
3591
|
// the mark exception — the ban stays for invented people, the
|
|
3468
3592
|
// attached one is deliberate.
|
|
3469
|
-
"Any earlier instruction that bans props, hands, people, or a presenter from the frame is a solo-packshot rule for this product and does not apply to this shot: the attached presenter is deliberate and must appear as directed."
|
|
3593
|
+
"Any earlier instruction that bans props, hands, people, or a presenter from the frame is a solo-packshot rule for this product and does not apply to this shot: the attached presenter is deliberate and must appear as directed.",
|
|
3594
|
+
productHandlingDirective()
|
|
3470
3595
|
] : [];
|
|
3471
3596
|
const figureDirectives = scene?.figure ? sceneFigureDirectives({
|
|
3472
3597
|
figure: scene.figure,
|
|
@@ -3476,13 +3601,15 @@ function compileBrief(brief, ctx) {
|
|
|
3476
3601
|
// deliberately in play; attachments are fully collected by this point.
|
|
3477
3602
|
hasMark: attachments.some((a) => a.role === "brand")
|
|
3478
3603
|
}) : [];
|
|
3604
|
+
if (hasPerson) personDirectives.push(personSkinDirective());
|
|
3479
3605
|
const closeUpDirectives = hasPerson && /\bclose[- ]?up\b|\bmacro\b|\bzoom(?:ed)?\b|\bDOF\b|\bdepth of field\b/i.test(sentence) ? [
|
|
3480
3606
|
"The tight framing includes the presenter: keep at least their hand in genuine contact with the product, and as much more of them as the crop allows. A close-up is never a reason to leave the person out of the photograph."
|
|
3481
3607
|
] : [];
|
|
3482
3608
|
const brandLines = brandRuleDirectives(ctx.brand);
|
|
3483
3609
|
const preservation = ctx.mode === "edit" ? [
|
|
3484
3610
|
...ctx.editReshape === "extend" ? [] : [editPreservationDirective(ctx.editScope ?? "global", { removal: ctx.editRemoval })],
|
|
3485
|
-
...ctx.inheritedIdentity ? [inheritedIdentityDirective()] : []
|
|
3611
|
+
...ctx.inheritedIdentity ? [inheritedIdentityDirective()] : [],
|
|
3612
|
+
...ctx.inheritedDirectives ?? []
|
|
3486
3613
|
] : [];
|
|
3487
3614
|
const apparelUnworn = ctx.mode !== "edit" && !hasPerson && attachments.some((a) => {
|
|
3488
3615
|
if (a.role !== "product" || !a.id) return false;
|
|
@@ -6259,23 +6386,34 @@ function registerAccessGuard(app, opts = {}) {
|
|
|
6259
6386
|
}
|
|
6260
6387
|
|
|
6261
6388
|
// src/editIdentity.ts
|
|
6262
|
-
var MAX_HOPS =
|
|
6263
|
-
var
|
|
6264
|
-
|
|
6265
|
-
return Array.isArray(t) ? t : [];
|
|
6266
|
-
};
|
|
6389
|
+
var MAX_HOPS = 64;
|
|
6390
|
+
var IDENTITY_KINDS = /* @__PURE__ */ new Set(["product", "character", "mark", "ref"]);
|
|
6391
|
+
var identityOf = (list2) => Array.isArray(list2) ? list2.filter((t) => IDENTITY_KINDS.has(t?.t)) : [];
|
|
6267
6392
|
function inheritedIdentityTokens(parentId, getNode) {
|
|
6268
6393
|
let id = parentId;
|
|
6394
|
+
const visited = /* @__PURE__ */ new Set();
|
|
6269
6395
|
for (let hop = 0; hop < MAX_HOPS && id; hop++) {
|
|
6396
|
+
if (visited.has(id)) return { tokens: [], truncated: false };
|
|
6397
|
+
visited.add(id);
|
|
6270
6398
|
const node = getNode(id);
|
|
6271
|
-
if (!node || node.kind === "root") return [];
|
|
6272
|
-
const
|
|
6273
|
-
|
|
6274
|
-
);
|
|
6275
|
-
if (
|
|
6399
|
+
if (!node || node.kind === "root") return { tokens: [], truncated: false };
|
|
6400
|
+
const brief = node.brief;
|
|
6401
|
+
const own = identityOf(brief?.tokens);
|
|
6402
|
+
const carried = identityOf(brief?.inherited);
|
|
6403
|
+
if (own.length || carried.length) {
|
|
6404
|
+
const seen = /* @__PURE__ */ new Set();
|
|
6405
|
+
const tokens = [];
|
|
6406
|
+
for (const t of [...own, ...carried]) {
|
|
6407
|
+
const key = JSON.stringify(t);
|
|
6408
|
+
if (seen.has(key)) continue;
|
|
6409
|
+
seen.add(key);
|
|
6410
|
+
tokens.push(t);
|
|
6411
|
+
}
|
|
6412
|
+
return { tokens, truncated: false };
|
|
6413
|
+
}
|
|
6276
6414
|
id = node.parentId;
|
|
6277
6415
|
}
|
|
6278
|
-
return [];
|
|
6416
|
+
return { tokens: [], truncated: id !== null };
|
|
6279
6417
|
}
|
|
6280
6418
|
|
|
6281
6419
|
// src/editScopeRules.ts
|
|
@@ -6356,6 +6494,13 @@ function planExpand(source, targetRatio) {
|
|
|
6356
6494
|
axis: "height"
|
|
6357
6495
|
};
|
|
6358
6496
|
}
|
|
6497
|
+
function wantsImplicitReshape(requested, parentNominal, sourcePixels) {
|
|
6498
|
+
const base = parentNominal ?? sourcePixels;
|
|
6499
|
+
if (!(requested.width > 0 && requested.height > 0 && base.width > 0 && base.height > 0)) return false;
|
|
6500
|
+
const want = requested.width / requested.height;
|
|
6501
|
+
const have = base.width / base.height;
|
|
6502
|
+
return Math.abs(want - have) / have > 0.01;
|
|
6503
|
+
}
|
|
6359
6504
|
function expandInstruction(plan, direction) {
|
|
6360
6505
|
const where = plan.axis === "width" ? "left and right" : "top and bottom";
|
|
6361
6506
|
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." : "";
|
|
@@ -6367,28 +6512,6 @@ Continue: the same surface, the same light direction, the same colour temperatur
|
|
|
6367
6512
|
Constraints: change only the blurred margin; keep the sharp photograph unchanged in position, scale and content.
|
|
6368
6513
|
Avoid: new objects, products, people, text or watermarks.` + own;
|
|
6369
6514
|
}
|
|
6370
|
-
var NAMED_RATIOS = [
|
|
6371
|
-
["1:1", 1],
|
|
6372
|
-
["4:5", 4 / 5],
|
|
6373
|
-
["5:4", 5 / 4],
|
|
6374
|
-
["2:3", 2 / 3],
|
|
6375
|
-
["3:2", 3 / 2],
|
|
6376
|
-
["3:4", 3 / 4],
|
|
6377
|
-
["4:3", 4 / 3],
|
|
6378
|
-
["9:16", 9 / 16],
|
|
6379
|
-
["16:9", 16 / 9],
|
|
6380
|
-
["2:1", 2],
|
|
6381
|
-
["1:2", 0.5]
|
|
6382
|
-
];
|
|
6383
|
-
function ratioLabel(width, height) {
|
|
6384
|
-
const ratio = width / height;
|
|
6385
|
-
for (const [label, value] of NAMED_RATIOS) {
|
|
6386
|
-
if (Math.abs(ratio - value) / value < 0.02) return label;
|
|
6387
|
-
}
|
|
6388
|
-
const gcd = (a, b) => b ? gcd(b, a % b) : a;
|
|
6389
|
-
const d = gcd(width, height) || 1;
|
|
6390
|
-
return `${Math.round(width / d)}:${Math.round(height / d)}`;
|
|
6391
|
-
}
|
|
6392
6515
|
function reframeInstruction(plan, source, direction) {
|
|
6393
6516
|
const wider = plan.axis === "width";
|
|
6394
6517
|
const where = wider ? "to the left and to the right" : "above and below";
|
|
@@ -6406,6 +6529,20 @@ Keep: every object in the same place at the same size, the same camera position
|
|
|
6406
6529
|
Avoid: new objects, products, people, text or watermarks; do not recompose, recentre, crop or resize anything already visible.` + own;
|
|
6407
6530
|
}
|
|
6408
6531
|
|
|
6532
|
+
// src/editSizeRules.ts
|
|
6533
|
+
var SAME_SHAPE_TOL = 0.01;
|
|
6534
|
+
var SHRINK_FLOOR = 0.8;
|
|
6535
|
+
function judgeEditSize(src, got) {
|
|
6536
|
+
if (!(src.width > 0 && src.height > 0 && got.width > 0 && got.height > 0)) return { action: "keep" };
|
|
6537
|
+
if (got.width === src.width && got.height === src.height) return { action: "keep" };
|
|
6538
|
+
const want = src.width / src.height;
|
|
6539
|
+
const have = got.width / got.height;
|
|
6540
|
+
if (Math.abs(have - want) / want > SAME_SHAPE_TOL) return { action: "keep" };
|
|
6541
|
+
const scale = Math.max(got.width, got.height) / Math.max(src.width, src.height);
|
|
6542
|
+
if (scale < SHRINK_FLOOR) return { action: "reject", scale };
|
|
6543
|
+
return { action: "resize", scale };
|
|
6544
|
+
}
|
|
6545
|
+
|
|
6409
6546
|
// src/cropRules.ts
|
|
6410
6547
|
function planCrop(source, targetRatio) {
|
|
6411
6548
|
if (!(source.width > 0 && source.height > 0 && targetRatio > 0)) return null;
|
|
@@ -8075,6 +8212,40 @@ function registerImageRoutes(app, deps) {
|
|
|
8075
8212
|
|
|
8076
8213
|
// src/release/notes.data.ts
|
|
8077
8214
|
var RELEASES = [
|
|
8215
|
+
{
|
|
8216
|
+
version: "0.6.10",
|
|
8217
|
+
date: "2026-08-29",
|
|
8218
|
+
title: "Pictures keep their shape.",
|
|
8219
|
+
sections: [
|
|
8220
|
+
{
|
|
8221
|
+
heading: "Create",
|
|
8222
|
+
body: "Images can no longer come back crushed or stretched: Scenri now works with the exact frame sizes the Codex image tool actually produces, and an answer that drifts off the requested shape is trimmed to it rather than distorted or refused. The wave of failed shots saying the engine could not produce the requested aspect ratio is gone with it, and refining an image over and over keeps its full sharpness at every step."
|
|
8223
|
+
},
|
|
8224
|
+
{
|
|
8225
|
+
heading: "Scenes",
|
|
8226
|
+
body: "When a scene built from photographs of one person is used with a chosen presenter, every output now shows the chosen presenter. Each reference image travels with a name that says what it is, so a scene photograph can lend its world and its styling without lending anyone a face, and asking for several variations gives every variation the same instructions rather than letting the first one copy the reference."
|
|
8227
|
+
}
|
|
8228
|
+
]
|
|
8229
|
+
},
|
|
8230
|
+
{
|
|
8231
|
+
version: "0.6.9",
|
|
8232
|
+
date: "2026-08-29",
|
|
8233
|
+
title: "Refinements keep the shot.",
|
|
8234
|
+
sections: [
|
|
8235
|
+
{
|
|
8236
|
+
heading: "Refine",
|
|
8237
|
+
body: "Refining a shot no longer wears it down. Every refinement keeps the original resolution, an answer that comes back too small is restored to the frame or refused rather than quietly shrinking the thread, and a plain refinement stays a plain refinement instead of silently rebuilding the frame when an engine drifted its shape. The product, the presenter, the brand mark and your references now travel down the whole chain, together with what must hold about each of them, so the tenth refinement knows the shot as well as the first."
|
|
8238
|
+
},
|
|
8239
|
+
{
|
|
8240
|
+
heading: "Presenters",
|
|
8241
|
+
body: "People render with real photographed skin: natural texture and fine lines stay, and the waxy, over-retouched gloss goes. Light still behaves like light, so a hard flash can genuinely shine. Products hold their real-world size in the frame, and a held product sits in a real grip instead of floating beside the hand. Asking for something surreal still wins."
|
|
8242
|
+
},
|
|
8243
|
+
{
|
|
8244
|
+
heading: "Create",
|
|
8245
|
+
body: "Starting a fresh shot always starts fresh: a refinement you left open days ago can no longer greet you as the thing you are about to make. Refining any older shot still works exactly as before, whenever you come back to it. Around the chips, text selects, double-clicks and keyboard selection behave like any editor, and clicking a chip while text is selected opens its picker."
|
|
8246
|
+
}
|
|
8247
|
+
]
|
|
8248
|
+
},
|
|
8078
8249
|
{
|
|
8079
8250
|
version: "0.6.8",
|
|
8080
8251
|
date: "2026-08-29",
|
|
@@ -8986,7 +9157,8 @@ function buildServer(opts) {
|
|
|
8986
9157
|
registerShowcaseRoutes(app, { templatesRoot });
|
|
8987
9158
|
app.get("/api/formats", async () => FORMATS);
|
|
8988
9159
|
async function compileEditBrief(brandId, parentId, brief, engineCaps, opts2) {
|
|
8989
|
-
const
|
|
9160
|
+
const inherited = inheritedIdentityTokens(parentId, (id) => core.store.getNode(id));
|
|
9161
|
+
const borrowed = inherited.tokens;
|
|
8990
9162
|
const already = new Set(
|
|
8991
9163
|
brief.tokens.filter((t) => t.t === "product" || t.t === "character" || t.t === "mark" || t.t === "ref").map((t) => JSON.stringify(t))
|
|
8992
9164
|
);
|
|
@@ -9010,6 +9182,28 @@ function buildServer(opts) {
|
|
|
9010
9182
|
const verdict = scopeOfInstruction(
|
|
9011
9183
|
brief.tokens.filter((t) => t.t === "text").map((t) => t.v).join(" ")
|
|
9012
9184
|
);
|
|
9185
|
+
const inheritedDirectives = [];
|
|
9186
|
+
let inheritedMark = false;
|
|
9187
|
+
for (const tok of inheritedTokens) {
|
|
9188
|
+
if (tok.t === "product") {
|
|
9189
|
+
const rec = (brandJson?.products ?? []).find((x) => x?.id === tok.id);
|
|
9190
|
+
if (rec)
|
|
9191
|
+
inheritedDirectives.push(
|
|
9192
|
+
productEditFidelityDirective(rec.promptName ?? rec.name),
|
|
9193
|
+
...productFactDirectives(rec)
|
|
9194
|
+
);
|
|
9195
|
+
} else if (tok.t === "character") {
|
|
9196
|
+
const rec = (brandJson?.characters ?? []).find((x) => x?.id === tok.id);
|
|
9197
|
+
if (rec)
|
|
9198
|
+
inheritedDirectives.push(
|
|
9199
|
+
characterEditIdentityDirective(rec.promptName ?? rec.name),
|
|
9200
|
+
...characterFactDirectives(rec)
|
|
9201
|
+
);
|
|
9202
|
+
} else if (tok.t === "mark" && !inheritedMark) {
|
|
9203
|
+
inheritedMark = true;
|
|
9204
|
+
inheritedDirectives.push(markEditDirective());
|
|
9205
|
+
}
|
|
9206
|
+
}
|
|
9013
9207
|
const compiled2 = compileBrief(brief, {
|
|
9014
9208
|
brand: brandJson,
|
|
9015
9209
|
images: core.images,
|
|
@@ -9020,6 +9214,7 @@ function buildServer(opts) {
|
|
|
9020
9214
|
editScope: verdict.scope,
|
|
9021
9215
|
editRemoval: verdict.removal ?? false,
|
|
9022
9216
|
inheritedIdentity: inheritedTokens.length > 0,
|
|
9217
|
+
inheritedDirectives,
|
|
9023
9218
|
// Only the explicit op drops the dimension promise: an implicit legacy
|
|
9024
9219
|
// expansion keeps its historical prompt byte for byte.
|
|
9025
9220
|
...opts2?.reshape === "extend" ? { editReshape: "extend" } : {}
|
|
@@ -9032,23 +9227,35 @@ function buildServer(opts) {
|
|
|
9032
9227
|
{ brand: brandJson, images: core.images, engineCaps: uncapped, templateById: sceneById }
|
|
9033
9228
|
);
|
|
9034
9229
|
identityWarnings = identity.warnings;
|
|
9035
|
-
|
|
9230
|
+
const productAngles = /* @__PURE__ */ new Map();
|
|
9231
|
+
inheritedAttachments = identity.attachments.filter((a) => {
|
|
9232
|
+
if (a.role === "product") {
|
|
9233
|
+
const n = (productAngles.get(String(a.id ?? a.hash)) ?? 0) + 1;
|
|
9234
|
+
productAngles.set(String(a.id ?? a.hash), n);
|
|
9235
|
+
return n <= 2;
|
|
9236
|
+
}
|
|
9237
|
+
return a.essential || a.role === "brand" || a.role === "reference";
|
|
9238
|
+
}).map((a) => ({ ...a, inherited: true }));
|
|
9036
9239
|
}
|
|
9037
9240
|
const cap2 = Math.max(0, engineCaps.maxReferenceImages - 1);
|
|
9038
9241
|
const merged = mergeEditAttachments(compiled2.attachments, inheritedAttachments, cap2);
|
|
9039
9242
|
const warnings = [...compiled2.warnings, ...identityWarnings.filter((w) => !compiled2.warnings.includes(w))];
|
|
9243
|
+
if (inherited.truncated)
|
|
9244
|
+
warnings.push("This thread is deeper than 64 steps, so identity attached before that could not be carried.");
|
|
9040
9245
|
if (merged.dropped.length) {
|
|
9041
9246
|
if (engineCaps.maxReferenceImages <= 1) {
|
|
9042
9247
|
warnings.push(
|
|
9043
9248
|
`${engineCaps.displayName} cannot carry reference images, so the identity rides on the source frame alone.`
|
|
9044
9249
|
);
|
|
9045
9250
|
} else {
|
|
9046
|
-
const
|
|
9047
|
-
|
|
9048
|
-
|
|
9049
|
-
|
|
9050
|
-
|
|
9051
|
-
|
|
9251
|
+
const keptLabels = new Set(merged.kept.map((a) => a.label));
|
|
9252
|
+
const names = [...new Set(merged.dropped.map((d) => d.label))].filter((l) => !keptLabels.has(l));
|
|
9253
|
+
if (names.length)
|
|
9254
|
+
warnings.push(
|
|
9255
|
+
`${engineCaps.displayName} reads ${engineCaps.maxReferenceImages} reference images and the frame being refined keeps one, so ${names.join(
|
|
9256
|
+
" and "
|
|
9257
|
+
)} ${names.length === 1 ? "was" : "were"} left out.`
|
|
9258
|
+
);
|
|
9052
9259
|
}
|
|
9053
9260
|
}
|
|
9054
9261
|
return {
|
|
@@ -9172,10 +9379,51 @@ function buildServer(opts) {
|
|
|
9172
9379
|
const out = [];
|
|
9173
9380
|
for (const h of images) {
|
|
9174
9381
|
const buf = core.images.read(h);
|
|
9175
|
-
|
|
9382
|
+
const meta2 = await sharp7(buf).metadata().catch(() => null);
|
|
9383
|
+
if (!meta2?.width || !meta2.height) throw new Error("engine returned an undecodable image");
|
|
9384
|
+
const oriented = (meta2.orientation ?? 1) !== 1;
|
|
9385
|
+
out.push(
|
|
9386
|
+
buf.subarray(0, 8).equals(PNG_SIG) && !oriented ? h : core.images.save(await sharp7(buf).rotate().png().toBuffer())
|
|
9387
|
+
);
|
|
9176
9388
|
}
|
|
9177
9389
|
return out;
|
|
9178
9390
|
}
|
|
9391
|
+
const CROPPABLE_DRIFT = 0.35;
|
|
9392
|
+
function conformToCanvas(nodeId, want) {
|
|
9393
|
+
return async (images) => {
|
|
9394
|
+
const target = want.width / want.height;
|
|
9395
|
+
const out = [];
|
|
9396
|
+
for (const h of images) {
|
|
9397
|
+
const buf = core.images.read(h);
|
|
9398
|
+
const meta2 = await sharp7(buf).metadata();
|
|
9399
|
+
if (!meta2.width || !meta2.height) {
|
|
9400
|
+
out.push(h);
|
|
9401
|
+
continue;
|
|
9402
|
+
}
|
|
9403
|
+
const got = meta2.width / meta2.height;
|
|
9404
|
+
const drift = Math.abs(got - target) / target;
|
|
9405
|
+
if (drift <= SAME_SHAPE_TOL || drift > CROPPABLE_DRIFT) {
|
|
9406
|
+
out.push(h);
|
|
9407
|
+
continue;
|
|
9408
|
+
}
|
|
9409
|
+
const w = got > target ? Math.round(meta2.height * target) : meta2.width;
|
|
9410
|
+
const hpx = got > target ? meta2.height : Math.round(meta2.width / target);
|
|
9411
|
+
const cropped = await sharp7(buf).resize(w, hpx, { fit: "cover", position: "attention" }).png().toBuffer();
|
|
9412
|
+
app.log.info(
|
|
9413
|
+
{ nodeId, got: `${meta2.width}x${meta2.height}`, want: `${w}x${hpx}` },
|
|
9414
|
+
"canvas: cropped a drifted frame to the asked ratio"
|
|
9415
|
+
);
|
|
9416
|
+
out.push(core.images.save(cropped));
|
|
9417
|
+
try {
|
|
9418
|
+
const fresh = core.store.getNode(nodeId);
|
|
9419
|
+
const b = fresh?.brief ?? {};
|
|
9420
|
+
core.store.setBrief(nodeId, { ...b, croppedFrom: [meta2.width, meta2.height] });
|
|
9421
|
+
} catch {
|
|
9422
|
+
}
|
|
9423
|
+
}
|
|
9424
|
+
return out;
|
|
9425
|
+
};
|
|
9426
|
+
}
|
|
9179
9427
|
async function assertAspect(images, expect) {
|
|
9180
9428
|
const want = expect.width / expect.height;
|
|
9181
9429
|
for (const h of images) {
|
|
@@ -9219,7 +9467,13 @@ function buildServer(opts) {
|
|
|
9219
9467
|
const brief = node.brief ?? {};
|
|
9220
9468
|
const raw = result.raw;
|
|
9221
9469
|
const survivors = typeof raw?.requested === "number" && Array.isArray(raw.variantIndexes) ? { requested: raw.requested, variantIndexes: raw.variantIndexes } : {};
|
|
9222
|
-
|
|
9470
|
+
const asked = expect ? { requestedSize: [expect.width, expect.height] } : {};
|
|
9471
|
+
core.store.setBrief(nodeId, { ...brief, rendered: { sizes, ...survivors, ...asked } });
|
|
9472
|
+
if (node.kind === "generation" && engineId === "codex-cli" && expect && expect.width !== expect.height && sizes.some(([w, h]) => w === expect.width && h === expect.height))
|
|
9473
|
+
app.log.warn(
|
|
9474
|
+
{ nodeId },
|
|
9475
|
+
"codex delivered exactly the requested pixels; its image tool cannot pin size - suggests a forbidden shell resize"
|
|
9476
|
+
);
|
|
9223
9477
|
}
|
|
9224
9478
|
} catch {
|
|
9225
9479
|
}
|
|
@@ -9449,7 +9703,13 @@ function buildServer(opts) {
|
|
|
9449
9703
|
const srcBuf = core.images.read(String(srcHash));
|
|
9450
9704
|
const srcMeta = await sharp7(srcBuf).metadata();
|
|
9451
9705
|
if (srcMeta.width && srcMeta.height) expectShape = { width: srcMeta.width, height: srcMeta.height };
|
|
9452
|
-
|
|
9706
|
+
const parentFormat = parent?.brief?.tokens?.find((t) => t?.t === "format");
|
|
9707
|
+
const parentNominal = parentFormat && Number(parentFormat.w) > 0 && Number(parentFormat.h) > 0 ? { width: Number(parentFormat.w), height: Number(parentFormat.h) } : null;
|
|
9708
|
+
const reshapeIntended = reshape === "extend" || reshape === void 0 && !!srcMeta.width && !!srcMeta.height && !!compiled2?.width && !!compiled2?.height && wantsImplicitReshape({ width: compiled2.width, height: compiled2.height }, parentNominal, {
|
|
9709
|
+
width: srcMeta.width,
|
|
9710
|
+
height: srcMeta.height
|
|
9711
|
+
});
|
|
9712
|
+
if (reshapeIntended && srcMeta.width && srcMeta.height && compiled2?.width && compiled2?.height) {
|
|
9453
9713
|
expandPlan = planExpand({ width: srcMeta.width, height: srcMeta.height }, compiled2.width / compiled2.height);
|
|
9454
9714
|
}
|
|
9455
9715
|
if (reshape === "extend" && !expandPlan)
|
|
@@ -9479,8 +9739,12 @@ function buildServer(opts) {
|
|
|
9479
9739
|
...editRefs.length ? { referenceImages: editRefs.map((r) => r.path) } : {},
|
|
9480
9740
|
...editRefs.length ? { referenceRoles: editRefs.map((r) => r.role ?? "reference") } : {},
|
|
9481
9741
|
// An answer at the planned size lets compositeExpand skip its rescale,
|
|
9482
|
-
// which is one whole class of seam misalignment gone when honored.
|
|
9483
|
-
|
|
9742
|
+
// which is one whole class of seam misalignment gone when honored. A
|
|
9743
|
+
// plain refine states the source's own pixels for the same reason:
|
|
9744
|
+
// engines given no size answered at whatever size they liked, the
|
|
9745
|
+
// shrunken answer was stored, and the next refinement inherited it —
|
|
9746
|
+
// the chain that walked shots down to thumbnails.
|
|
9747
|
+
...expandPlan ? { width: expandPlan.width, height: expandPlan.height } : srcMeta.width && srcMeta.height ? { width: srcMeta.width, height: srcMeta.height } : {},
|
|
9484
9748
|
// Only an engine that can genuinely paint a margin is told where the
|
|
9485
9749
|
// picture sits; the rest would ignore it anyway.
|
|
9486
9750
|
...expandPlan && canOutpaint2 ? {
|
|
@@ -9581,6 +9845,43 @@ function buildServer(opts) {
|
|
|
9581
9845
|
const plan = expandPlan;
|
|
9582
9846
|
const original = editedFrom ? core.images.read(editedFrom) : null;
|
|
9583
9847
|
const localScope = kind === "edit" && !plan && editScope === "local" && original;
|
|
9848
|
+
const enforceEditCanvas = async (images) => {
|
|
9849
|
+
const srcMeta = await sharp7(original).metadata();
|
|
9850
|
+
if (!srcMeta.width || !srcMeta.height) return images;
|
|
9851
|
+
const out = [];
|
|
9852
|
+
for (const h of images) {
|
|
9853
|
+
const meta2 = await sharp7(core.images.read(h)).metadata();
|
|
9854
|
+
const got = { width: meta2.width ?? 0, height: meta2.height ?? 0 };
|
|
9855
|
+
const verdict = judgeEditSize({ width: srcMeta.width, height: srcMeta.height }, got);
|
|
9856
|
+
if (verdict.action === "reject")
|
|
9857
|
+
throw new Error(
|
|
9858
|
+
`engine returned ${got.width}x${got.height} for a ${srcMeta.width}x${srcMeta.height} frame: too little of the picture came back to keep at this resolution`
|
|
9859
|
+
);
|
|
9860
|
+
if (verdict.action === "resize") {
|
|
9861
|
+
app.log.info(
|
|
9862
|
+
{ nodeId: node.id, got: `${got.width}x${got.height}`, want: `${srcMeta.width}x${srcMeta.height}` },
|
|
9863
|
+
"edit: normalized the answer to the source canvas"
|
|
9864
|
+
);
|
|
9865
|
+
out.push(
|
|
9866
|
+
core.images.save(
|
|
9867
|
+
await sharp7(core.images.read(h)).resize(srcMeta.width, srcMeta.height, { fit: "fill" }).png().toBuffer()
|
|
9868
|
+
)
|
|
9869
|
+
);
|
|
9870
|
+
try {
|
|
9871
|
+
const fresh = core.store.getNode(node.id);
|
|
9872
|
+
const b = fresh?.brief ?? {};
|
|
9873
|
+
const parentBrief = core.store.getNode(resolvedParentId)?.brief ?? {};
|
|
9874
|
+
core.store.setBrief(node.id, {
|
|
9875
|
+
...b,
|
|
9876
|
+
resizedFrom: [got.width, got.height],
|
|
9877
|
+
resampledHops: (parentBrief.resampledHops ?? 0) + 1
|
|
9878
|
+
});
|
|
9879
|
+
} catch {
|
|
9880
|
+
}
|
|
9881
|
+
} else out.push(h);
|
|
9882
|
+
}
|
|
9883
|
+
return out;
|
|
9884
|
+
};
|
|
9584
9885
|
const post = plan ? expandMethod === "outpaint" ? async (images) => {
|
|
9585
9886
|
const out = [];
|
|
9586
9887
|
for (const h of images) {
|
|
@@ -9609,15 +9910,21 @@ function buildServer(opts) {
|
|
|
9609
9910
|
"expand: chose which frame to keep"
|
|
9610
9911
|
);
|
|
9611
9912
|
return images;
|
|
9612
|
-
} :
|
|
9613
|
-
|
|
9614
|
-
|
|
9615
|
-
const
|
|
9616
|
-
|
|
9617
|
-
|
|
9913
|
+
} : kind === "edit" && original ? async (images) => {
|
|
9914
|
+
let staged = images;
|
|
9915
|
+
if (localScope) {
|
|
9916
|
+
const out = [];
|
|
9917
|
+
for (const h of staged) {
|
|
9918
|
+
const { image, outcome, changed } = await preserveOutsideChange(original, core.images.read(h));
|
|
9919
|
+
app.log.info({ nodeId: node.id, outcome, changed }, "local edit");
|
|
9920
|
+
out.push(outcome === "composited" ? core.images.save(image) : h);
|
|
9921
|
+
}
|
|
9922
|
+
staged = out;
|
|
9923
|
+
} else if (expectShape) {
|
|
9924
|
+
staged = await conformToCanvas(node.id, expectShape)(staged);
|
|
9618
9925
|
}
|
|
9619
|
-
return
|
|
9620
|
-
} : void 0;
|
|
9926
|
+
return enforceEditCanvas(staged);
|
|
9927
|
+
} : kind === "generation" && compiled2?.width && compiled2?.height ? conformToCanvas(node.id, { width: compiled2.width, height: compiled2.height }) : void 0;
|
|
9621
9928
|
const nodeBudgetMs = kind === "generation" && runEngine.capabilities().id === "codex-cli" ? codexNodeBudgetMs(Math.min(Math.max(1, Number(count)), 8)) : void 0;
|
|
9622
9929
|
void runNode(node.id, runEngine, estimate, work, expectShape, post, nodeBudgetMs).catch(
|
|
9623
9930
|
(err) => app.log.error({ err }, "node run failed")
|