scenri 0.6.9 → 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 +14 -0
- package/dist/serve.js +152 -35
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.6.10](https://github.com/tonygorb/Scenri/compare/v0.6.9...v0.6.10) (2026-08-29)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
* **engine-codex:** ask for the tool's own grid point, not our nominal pixels ([a0cc518](https://github.com/tonygorb/Scenri/commit/a0cc518c2e79702f728bc7d00edddf874d3b2dc3))
|
|
9
|
+
* **engine-codex:** bind references by role-named files and give every take the same instruction ([2e645b2](https://github.com/tonygorb/Scenri/commit/2e645b2d34dc7006f23792cad04ad9170f3bae28))
|
|
10
|
+
* **engine-codex:** bind references by role-named files and give every take the same instruction ([416999d](https://github.com/tonygorb/Scenri/commit/416999d85ad984e8e5ee4df06e12a6edf09ed84a))
|
|
11
|
+
* **engine-codex:** never let the model resize; the frame rides as ratio language ([a5907a0](https://github.com/tonygorb/Scenri/commit/a5907a0a2a247eb5e369d3eae94884880cdb6cc3))
|
|
12
|
+
* **engine-codex:** never let the model resize; the frame rides as ratio language ([063316a](https://github.com/tonygorb/Scenri/commit/063316ad15fe01000d112cf240ad10a6e5707ccf))
|
|
13
|
+
* validate and orient engine images before storing; crop drifted frames to the asked ratio ([b88c039](https://github.com/tonygorb/Scenri/commit/b88c0392ff97a103301617ad23ddfcb500303e90))
|
|
14
|
+
* validate and orient engine images before storing; crop drifted frames to the asked ratio ([3cbd462](https://github.com/tonygorb/Scenri/commit/3cbd462df35b322cd55c1e3b6e8c47b767ee4880))
|
|
15
|
+
* widen the crop net to the drift real briefs actually produce ([491aa3c](https://github.com/tonygorb/Scenri/commit/491aa3c9f03e1511188a3fc71a2de817a9aff289))
|
|
16
|
+
|
|
3
17
|
## [0.6.9](https://github.com/tonygorb/Scenri/compare/v0.6.8...v0.6.9) (2026-08-29)
|
|
4
18
|
|
|
5
19
|
|
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() {
|
|
@@ -2479,6 +2501,27 @@ var CODEX_POOL = 2;
|
|
|
2479
2501
|
function codexNodeBudgetMs(count) {
|
|
2480
2502
|
return Math.ceil(Math.max(1, count) / CODEX_POOL) * DEFAULT_TIMEOUT_MS2 + 6e4;
|
|
2481
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
|
+
}
|
|
2482
2525
|
function createCodexEngine(opts) {
|
|
2483
2526
|
const { saveImage } = opts;
|
|
2484
2527
|
const platform = opts.platform ?? process.platform;
|
|
@@ -2511,7 +2554,9 @@ function createCodexEngine(opts) {
|
|
|
2511
2554
|
}
|
|
2512
2555
|
const hashes = [];
|
|
2513
2556
|
for (const name of outFiles) {
|
|
2514
|
-
|
|
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));
|
|
2515
2560
|
}
|
|
2516
2561
|
return hashes;
|
|
2517
2562
|
}
|
|
@@ -2558,6 +2603,11 @@ function createCodexEngine(opts) {
|
|
|
2558
2603
|
* attached — and on an edit the first one attached is `input.png`, the
|
|
2559
2604
|
* shot being edited. A refine carrying a full identity payload was
|
|
2560
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.
|
|
2561
2611
|
*/
|
|
2562
2612
|
maxReferenceImages: 5,
|
|
2563
2613
|
/*
|
|
@@ -2589,8 +2639,9 @@ function createCodexEngine(opts) {
|
|
|
2589
2639
|
(_, i) => async () => withWorkDir(async (dir) => {
|
|
2590
2640
|
const args = execArgs(dir);
|
|
2591
2641
|
let refBytes = 0;
|
|
2642
|
+
const names = refFileNames(roles, refs.length);
|
|
2592
2643
|
for (const [idx, ref] of refs.entries()) {
|
|
2593
|
-
const dest = join(dir,
|
|
2644
|
+
const dest = join(dir, names[idx]);
|
|
2594
2645
|
await copyFile(ref, dest);
|
|
2595
2646
|
refBytes += (await stat(dest)).size;
|
|
2596
2647
|
args.splice(args.length - 1, 0, `--image=${dest}`);
|
|
@@ -2666,9 +2717,15 @@ function createCodexEngine(opts) {
|
|
|
2666
2717
|
await copyFile(editRefs[i], join(dir, name));
|
|
2667
2718
|
refLines.push(`${name} shows ${EDIT_REFERENCE_ROLE_DIRECTIVE[role]}`);
|
|
2668
2719
|
}
|
|
2669
|
-
const promptText = `Edit input.png using your image generation/editing tool: ${req.instruction}.` + (refLines.length ? ` ${refLines.join(". ")}.` : "") +
|
|
2670
|
-
//
|
|
2671
|
-
|
|
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.`;
|
|
2672
2729
|
const args = execArgs(dir);
|
|
2673
2730
|
for (const name of ["input.png", ...refLines.map((_, i) => `${editRoles[i] ?? "reference"}-${i + 1}.png`)]) {
|
|
2674
2731
|
args.splice(args.length - 1, 0, `--image=${join(dir, name)}`);
|
|
@@ -2692,16 +2749,29 @@ function createCodexEngine(opts) {
|
|
|
2692
2749
|
}
|
|
2693
2750
|
function buildPrompt2(req, index, roles) {
|
|
2694
2751
|
const roleDirective = REFERENCE_ROLE_DIRECTIVE;
|
|
2695
|
-
const
|
|
2696
|
-
|
|
2697
|
-
|
|
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);
|
|
2698
2756
|
return (
|
|
2699
2757
|
// "professional-grade", not "flawless": the audit of the waxy-presenter
|
|
2700
2758
|
// report traced part of the plastic, over-perfected rendering to that
|
|
2701
2759
|
// one unconditional word. Still "image", never "photograph" - this
|
|
2702
2760
|
// wrapper also generates graphic assets. Independently revertible on
|
|
2703
2761
|
// render evidence.
|
|
2704
|
-
`Generate one professional-grade image immediately using your image generation tool, ${
|
|
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)` : "")
|
|
2705
2775
|
);
|
|
2706
2776
|
}
|
|
2707
2777
|
}
|
|
@@ -6442,28 +6512,6 @@ Continue: the same surface, the same light direction, the same colour temperatur
|
|
|
6442
6512
|
Constraints: change only the blurred margin; keep the sharp photograph unchanged in position, scale and content.
|
|
6443
6513
|
Avoid: new objects, products, people, text or watermarks.` + own;
|
|
6444
6514
|
}
|
|
6445
|
-
var NAMED_RATIOS = [
|
|
6446
|
-
["1:1", 1],
|
|
6447
|
-
["4:5", 4 / 5],
|
|
6448
|
-
["5:4", 5 / 4],
|
|
6449
|
-
["2:3", 2 / 3],
|
|
6450
|
-
["3:2", 3 / 2],
|
|
6451
|
-
["3:4", 3 / 4],
|
|
6452
|
-
["4:3", 4 / 3],
|
|
6453
|
-
["9:16", 9 / 16],
|
|
6454
|
-
["16:9", 16 / 9],
|
|
6455
|
-
["2:1", 2],
|
|
6456
|
-
["1:2", 0.5]
|
|
6457
|
-
];
|
|
6458
|
-
function ratioLabel(width, height) {
|
|
6459
|
-
const ratio = width / height;
|
|
6460
|
-
for (const [label, value] of NAMED_RATIOS) {
|
|
6461
|
-
if (Math.abs(ratio - value) / value < 0.02) return label;
|
|
6462
|
-
}
|
|
6463
|
-
const gcd = (a, b) => b ? gcd(b, a % b) : a;
|
|
6464
|
-
const d = gcd(width, height) || 1;
|
|
6465
|
-
return `${Math.round(width / d)}:${Math.round(height / d)}`;
|
|
6466
|
-
}
|
|
6467
6515
|
function reframeInstruction(plan, source, direction) {
|
|
6468
6516
|
const wider = plan.axis === "width";
|
|
6469
6517
|
const where = wider ? "to the left and to the right" : "above and below";
|
|
@@ -8164,6 +8212,21 @@ function registerImageRoutes(app, deps) {
|
|
|
8164
8212
|
|
|
8165
8213
|
// src/release/notes.data.ts
|
|
8166
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
|
+
},
|
|
8167
8230
|
{
|
|
8168
8231
|
version: "0.6.9",
|
|
8169
8232
|
date: "2026-08-29",
|
|
@@ -9316,10 +9379,51 @@ function buildServer(opts) {
|
|
|
9316
9379
|
const out = [];
|
|
9317
9380
|
for (const h of images) {
|
|
9318
9381
|
const buf = core.images.read(h);
|
|
9319
|
-
|
|
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
|
+
);
|
|
9320
9388
|
}
|
|
9321
9389
|
return out;
|
|
9322
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
|
+
}
|
|
9323
9427
|
async function assertAspect(images, expect) {
|
|
9324
9428
|
const want = expect.width / expect.height;
|
|
9325
9429
|
for (const h of images) {
|
|
@@ -9363,7 +9467,13 @@ function buildServer(opts) {
|
|
|
9363
9467
|
const brief = node.brief ?? {};
|
|
9364
9468
|
const raw = result.raw;
|
|
9365
9469
|
const survivors = typeof raw?.requested === "number" && Array.isArray(raw.variantIndexes) ? { requested: raw.requested, variantIndexes: raw.variantIndexes } : {};
|
|
9366
|
-
|
|
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
|
+
);
|
|
9367
9477
|
}
|
|
9368
9478
|
} catch {
|
|
9369
9479
|
}
|
|
@@ -9760,7 +9870,12 @@ function buildServer(opts) {
|
|
|
9760
9870
|
try {
|
|
9761
9871
|
const fresh = core.store.getNode(node.id);
|
|
9762
9872
|
const b = fresh?.brief ?? {};
|
|
9763
|
-
core.store.
|
|
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
|
+
});
|
|
9764
9879
|
} catch {
|
|
9765
9880
|
}
|
|
9766
9881
|
} else out.push(h);
|
|
@@ -9805,9 +9920,11 @@ function buildServer(opts) {
|
|
|
9805
9920
|
out.push(outcome === "composited" ? core.images.save(image) : h);
|
|
9806
9921
|
}
|
|
9807
9922
|
staged = out;
|
|
9923
|
+
} else if (expectShape) {
|
|
9924
|
+
staged = await conformToCanvas(node.id, expectShape)(staged);
|
|
9808
9925
|
}
|
|
9809
9926
|
return enforceEditCanvas(staged);
|
|
9810
|
-
} : void 0;
|
|
9927
|
+
} : kind === "generation" && compiled2?.width && compiled2?.height ? conformToCanvas(node.id, { width: compiled2.width, height: compiled2.height }) : void 0;
|
|
9811
9928
|
const nodeBudgetMs = kind === "generation" && runEngine.capabilities().id === "codex-cli" ? codexNodeBudgetMs(Math.min(Math.max(1, Number(count)), 8)) : void 0;
|
|
9812
9929
|
void runNode(node.id, runEngine, estimate, work, expectShape, post, nodeBudgetMs).catch(
|
|
9813
9930
|
(err) => app.log.error({ err }, "node run failed")
|