scenri 0.6.7 → 0.6.9
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 +29 -0
- package/dist/serve.js +249 -45
- package/package.json +1 -1
- package/studio-dist/assets/index-CGjGxrsJ.js +103 -0
- package/studio-dist/index.html +1 -1
- package/templates/contour-key-portrait.json +1 -1
- package/templates/rembrandt-triangle-portrait.json +1 -1
- package/studio-dist/assets/index-B8Town4N.js +0 -103
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,34 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.6.9](https://github.com/tonygorb/Scenri/compare/v0.6.8...v0.6.9) (2026-08-29)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
* a chip click with text selected still opens the picker ([34ea025](https://github.com/tonygorb/Scenri/commit/34ea0257d59920ed0a3abe93771329d75217162b))
|
|
9
|
+
* a chip click with text selected still opens the picker ([1f85306](https://github.com/tonygorb/Scenri/commit/1f85306ef4f19734208921e7c3167acb8596b89b))
|
|
10
|
+
* a refine target never follows you into a fresh Create ([43d32d5](https://github.com/tonygorb/Scenri/commit/43d32d58c3ab679015d800bdbb5b0c02c0444013))
|
|
11
|
+
* a refine target never follows you into a fresh Create ([7e15714](https://github.com/tonygorb/Scenri/commit/7e157141f820ae86a0281f3b949262797baed96a))
|
|
12
|
+
* a refinement chain carries the whole identity record ([4688f0f](https://github.com/tonygorb/Scenri/commit/4688f0f934294789f8fa9222fdc39becba4f0ce8))
|
|
13
|
+
* a refinement chain carries the whole identity record ([f174480](https://github.com/tonygorb/Scenri/commit/f174480c552347d20e110ad2dfc206f70d800aa9))
|
|
14
|
+
* a refinement edits at the source's real shape and holds its resolution ([ccd9be1](https://github.com/tonygorb/Scenri/commit/ccd9be16b2fd609824b5332335c66cf5cc7c06bb))
|
|
15
|
+
* a refinement edits at the source's real shape and holds its resolution ([d2b56af](https://github.com/tonygorb/Scenri/commit/d2b56affead40c51fe7f57bd073b07e8f1e7088d))
|
|
16
|
+
* **codex:** drop 'flawless' from the generation wrapper ([3190495](https://github.com/tonygorb/Scenri/commit/319049580bc97f07d85ddb9cb298451c247cf339))
|
|
17
|
+
* **codex:** drop 'flawless' from the generation wrapper ([7e07836](https://github.com/tonygorb/Scenri/commit/7e078363a0298972944e3b2f2877b6fcc36b992a))
|
|
18
|
+
* photographic skin, real product scale, real hands ([96b8729](https://github.com/tonygorb/Scenri/commit/96b87290a711f0ab250473e116e728493790e155))
|
|
19
|
+
* photographic skin, real product scale, real hands ([51ff91e](https://github.com/tonygorb/Scenri/commit/51ff91e9a71740ab93301a3a4a76216a4734f916))
|
|
20
|
+
* two portrait scenes stop commanding retouch ([58c6542](https://github.com/tonygorb/Scenri/commit/58c6542aeb1bba3e7ca73366a9ed60ecc788bbf3))
|
|
21
|
+
* two portrait scenes stop commanding retouch ([ca92794](https://github.com/tonygorb/Scenri/commit/ca92794636bc7bbe74454d6c37122a16989ed784))
|
|
22
|
+
|
|
23
|
+
## [0.6.8](https://github.com/tonygorb/Scenri/compare/v0.6.7...v0.6.8) (2026-08-29)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
### Bug Fixes
|
|
27
|
+
|
|
28
|
+
* a created scene never refills the next one, and text stays selected ([10d518f](https://github.com/tonygorb/Scenri/commit/10d518f8cc8ba8600f2ad31f72a0c2aea196a530))
|
|
29
|
+
* a created scene never refills the next one, and text stays selected ([3707774](https://github.com/tonygorb/Scenri/commit/3707774dbd1f981919ed96dbf2788f4dc5a316d5))
|
|
30
|
+
* closing a creation ends it, and one Undo brings it back ([24f43ba](https://github.com/tonygorb/Scenri/commit/24f43ba5bcfeb5359e99b5e82a806e5224765462))
|
|
31
|
+
|
|
3
32
|
## [0.6.7](https://github.com/tonygorb/Scenri/compare/v0.6.6...v0.6.7) (2026-08-29)
|
|
4
33
|
|
|
5
34
|
|
package/dist/serve.js
CHANGED
|
@@ -1483,7 +1483,10 @@ function createOpenRouterEngine(opts) {
|
|
|
1483
1483
|
...editRefs.map((p) => ({ type: "image_url", image_url: { url: dataUrl(p) } }))
|
|
1484
1484
|
]
|
|
1485
1485
|
}
|
|
1486
|
-
]
|
|
1486
|
+
],
|
|
1487
|
+
// Mirrors generate: an edit given no shape answered at whatever ratio
|
|
1488
|
+
// the model felt like, and the drift compounded down refine chains.
|
|
1489
|
+
...req.width && req.height ? { image_config: { aspect_ratio: aspectRatioFor(req.width, req.height) } } : {}
|
|
1487
1490
|
};
|
|
1488
1491
|
const json = await post(key, body, signal);
|
|
1489
1492
|
const [buf] = extractImages(json);
|
|
@@ -2692,7 +2695,14 @@ function createCodexEngine(opts) {
|
|
|
2692
2695
|
const refDirectives = roles.map(
|
|
2693
2696
|
(role, i) => roles.length > 1 ? `Attached image ${i + 1} is ${roleDirective[role]}.` : `The attached image is ${roleDirective[role]}.`
|
|
2694
2697
|
).join(" ");
|
|
2695
|
-
return
|
|
2698
|
+
return (
|
|
2699
|
+
// "professional-grade", not "flawless": the audit of the waxy-presenter
|
|
2700
|
+
// report traced part of the plastic, over-perfected rendering to that
|
|
2701
|
+
// one unconditional word. Still "image", never "photograph" - this
|
|
2702
|
+
// wrapper also generates graphic assets. Independently revertible on
|
|
2703
|
+
// render evidence.
|
|
2704
|
+
`Generate one professional-grade image immediately using your image generation tool, ${req.width}x${req.height}: ${req.prompt}.` + (refDirectives ? ` ${refDirectives}` : "") + ` Do not browse the web or explore files. Save the image in the current directory as out-1.png (you may run the commands needed to save and resize it). Nothing else.` + (index > 0 ? ` (variant ${index + 1} \u2014 same brief, different composition)` : "")
|
|
2705
|
+
);
|
|
2696
2706
|
}
|
|
2697
2707
|
}
|
|
2698
2708
|
|
|
@@ -2773,7 +2783,13 @@ function createDemoEngine(saveImage) {
|
|
|
2773
2783
|
},
|
|
2774
2784
|
async edit(req) {
|
|
2775
2785
|
const colors = paletteOf(req);
|
|
2776
|
-
const buf = await render(
|
|
2786
|
+
const buf = await render(
|
|
2787
|
+
colors,
|
|
2788
|
+
`edit: ${req.instruction}`,
|
|
2789
|
+
req.width ?? 1024,
|
|
2790
|
+
req.height ?? 1024,
|
|
2791
|
+
req.instruction.length
|
|
2792
|
+
);
|
|
2777
2793
|
return { images: [saveImage(buf)], costUsd: 0 };
|
|
2778
2794
|
}
|
|
2779
2795
|
};
|
|
@@ -2916,6 +2932,7 @@ async function resolvePresenterImages(core, templatesRoot, presenter) {
|
|
|
2916
2932
|
name: presenter.name,
|
|
2917
2933
|
...presenter.identityNotes ? { identityNotes: presenter.identityNotes } : {},
|
|
2918
2934
|
...presenter.negativeConstraints?.length ? { negativeConstraints: presenter.negativeConstraints } : {},
|
|
2935
|
+
...presenter.skin ? { skin: presenter.skin } : {},
|
|
2919
2936
|
shots
|
|
2920
2937
|
};
|
|
2921
2938
|
}
|
|
@@ -3019,7 +3036,14 @@ async function resolveDemoProductImages(core, templatesRoot, product) {
|
|
|
3019
3036
|
// brushed steel case read as steel rather than plastic. They were being
|
|
3020
3037
|
// dropped here, so the compiler never saw them.
|
|
3021
3038
|
...product.materials ? { materials: product.materials } : {},
|
|
3022
|
-
...product.primaryColors ? { primaryColors: product.primaryColors } : {}
|
|
3039
|
+
...product.primaryColors ? { primaryColors: product.primaryColors } : {},
|
|
3040
|
+
// The description is the only text a demo product has that carries scale
|
|
3041
|
+
// ("sized for a facial serum or treatment oil") - dropping it here is why
|
|
3042
|
+
// a cream jar could render the size of a basketball. And category is what
|
|
3043
|
+
// arms the compiler's apparel guard; without it no demo garment ever
|
|
3044
|
+
// earned the "present it as a product" line.
|
|
3045
|
+
...product.description ? { description: product.description } : {},
|
|
3046
|
+
...product.category ? { category: product.category } : {}
|
|
3023
3047
|
};
|
|
3024
3048
|
}
|
|
3025
3049
|
async function brandJsonWithResolvedDemoProducts(core, templatesRoot, demoProducts, brandJson, tokens) {
|
|
@@ -3127,6 +3151,38 @@ function editPreservationDirective(scope, opts) {
|
|
|
3127
3151
|
function inheritedIdentityDirective() {
|
|
3128
3152
|
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
3153
|
}
|
|
3154
|
+
function productFactDirectives(p) {
|
|
3155
|
+
const out = [];
|
|
3156
|
+
if (p.preservationNotes) out.push(String(p.preservationNotes));
|
|
3157
|
+
if (p.negativeConstraints) out.push(`Avoid: ${p.negativeConstraints}`);
|
|
3158
|
+
const materials = p.materials ?? p.material;
|
|
3159
|
+
if (materials) out.push(`Its materials and finish: ${materials}.`);
|
|
3160
|
+
if (p.primaryColors) out.push(`Its actual colors: ${p.primaryColors}.`);
|
|
3161
|
+
if (p.dimensions)
|
|
3162
|
+
out.push(`Its real-world size is ${p.dimensions} \u2014 keep it at true scale relative to everything else in frame.`);
|
|
3163
|
+
return out;
|
|
3164
|
+
}
|
|
3165
|
+
function characterFactDirectives(c) {
|
|
3166
|
+
const out = [];
|
|
3167
|
+
if (c.identityNotes) out.push(String(c.identityNotes));
|
|
3168
|
+
if (c.negativeConstraints?.length) out.push(`Avoid: ${[].concat(c.negativeConstraints).join(", ")}`);
|
|
3169
|
+
return out;
|
|
3170
|
+
}
|
|
3171
|
+
function productEditFidelityDirective(name) {
|
|
3172
|
+
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.`;
|
|
3173
|
+
}
|
|
3174
|
+
function characterEditIdentityDirective(name) {
|
|
3175
|
+
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.`;
|
|
3176
|
+
}
|
|
3177
|
+
function markEditDirective() {
|
|
3178
|
+
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.";
|
|
3179
|
+
}
|
|
3180
|
+
function personSkinDirective() {
|
|
3181
|
+
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.";
|
|
3182
|
+
}
|
|
3183
|
+
function productHandlingDirective() {
|
|
3184
|
+
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.";
|
|
3185
|
+
}
|
|
3130
3186
|
function garmentDisplayDirective() {
|
|
3131
3187
|
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
3188
|
}
|
|
@@ -3315,14 +3371,10 @@ function compileBrief(brief, ctx) {
|
|
|
3315
3371
|
attachments.push({ role: "product", id: p.id, label: p.name, hash: h, essential: i === 0 });
|
|
3316
3372
|
});
|
|
3317
3373
|
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)
|
|
3374
|
+
productDirectives.push(...productFactDirectives(p));
|
|
3375
|
+
if (p.description && !p.dimensions)
|
|
3324
3376
|
productDirectives.push(
|
|
3325
|
-
`
|
|
3377
|
+
`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
3378
|
);
|
|
3327
3379
|
} else {
|
|
3328
3380
|
warnings.push(`${p.name} has no usable photo, so it is named but not attached.`);
|
|
@@ -3348,9 +3400,11 @@ function compileBrief(brief, ctx) {
|
|
|
3348
3400
|
personDirectives.push(
|
|
3349
3401
|
"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
3402
|
);
|
|
3351
|
-
|
|
3352
|
-
if (c.
|
|
3353
|
-
personDirectives.push(
|
|
3403
|
+
personDirectives.push(...characterFactDirectives(c));
|
|
3404
|
+
if (c.skin)
|
|
3405
|
+
personDirectives.push(
|
|
3406
|
+
`${c.promptName ?? c.name}'s skin, exactly as the reference photographs show it: ${c.skin}.`
|
|
3407
|
+
);
|
|
3354
3408
|
} else {
|
|
3355
3409
|
warnings.push(`${c.name} has no usable photo, so they are named but not attached.`);
|
|
3356
3410
|
}
|
|
@@ -3466,7 +3520,8 @@ function compileBrief(brief, ctx) {
|
|
|
3466
3520
|
// the compiler: fired only when a person is attached, worded like
|
|
3467
3521
|
// the mark exception — the ban stays for invented people, the
|
|
3468
3522
|
// 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."
|
|
3523
|
+
"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.",
|
|
3524
|
+
productHandlingDirective()
|
|
3470
3525
|
] : [];
|
|
3471
3526
|
const figureDirectives = scene?.figure ? sceneFigureDirectives({
|
|
3472
3527
|
figure: scene.figure,
|
|
@@ -3476,13 +3531,15 @@ function compileBrief(brief, ctx) {
|
|
|
3476
3531
|
// deliberately in play; attachments are fully collected by this point.
|
|
3477
3532
|
hasMark: attachments.some((a) => a.role === "brand")
|
|
3478
3533
|
}) : [];
|
|
3534
|
+
if (hasPerson) personDirectives.push(personSkinDirective());
|
|
3479
3535
|
const closeUpDirectives = hasPerson && /\bclose[- ]?up\b|\bmacro\b|\bzoom(?:ed)?\b|\bDOF\b|\bdepth of field\b/i.test(sentence) ? [
|
|
3480
3536
|
"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
3537
|
] : [];
|
|
3482
3538
|
const brandLines = brandRuleDirectives(ctx.brand);
|
|
3483
3539
|
const preservation = ctx.mode === "edit" ? [
|
|
3484
3540
|
...ctx.editReshape === "extend" ? [] : [editPreservationDirective(ctx.editScope ?? "global", { removal: ctx.editRemoval })],
|
|
3485
|
-
...ctx.inheritedIdentity ? [inheritedIdentityDirective()] : []
|
|
3541
|
+
...ctx.inheritedIdentity ? [inheritedIdentityDirective()] : [],
|
|
3542
|
+
...ctx.inheritedDirectives ?? []
|
|
3486
3543
|
] : [];
|
|
3487
3544
|
const apparelUnworn = ctx.mode !== "edit" && !hasPerson && attachments.some((a) => {
|
|
3488
3545
|
if (a.role !== "product" || !a.id) return false;
|
|
@@ -6259,23 +6316,34 @@ function registerAccessGuard(app, opts = {}) {
|
|
|
6259
6316
|
}
|
|
6260
6317
|
|
|
6261
6318
|
// src/editIdentity.ts
|
|
6262
|
-
var MAX_HOPS =
|
|
6263
|
-
var
|
|
6264
|
-
|
|
6265
|
-
return Array.isArray(t) ? t : [];
|
|
6266
|
-
};
|
|
6319
|
+
var MAX_HOPS = 64;
|
|
6320
|
+
var IDENTITY_KINDS = /* @__PURE__ */ new Set(["product", "character", "mark", "ref"]);
|
|
6321
|
+
var identityOf = (list2) => Array.isArray(list2) ? list2.filter((t) => IDENTITY_KINDS.has(t?.t)) : [];
|
|
6267
6322
|
function inheritedIdentityTokens(parentId, getNode) {
|
|
6268
6323
|
let id = parentId;
|
|
6324
|
+
const visited = /* @__PURE__ */ new Set();
|
|
6269
6325
|
for (let hop = 0; hop < MAX_HOPS && id; hop++) {
|
|
6326
|
+
if (visited.has(id)) return { tokens: [], truncated: false };
|
|
6327
|
+
visited.add(id);
|
|
6270
6328
|
const node = getNode(id);
|
|
6271
|
-
if (!node || node.kind === "root") return [];
|
|
6272
|
-
const
|
|
6273
|
-
|
|
6274
|
-
);
|
|
6275
|
-
if (
|
|
6329
|
+
if (!node || node.kind === "root") return { tokens: [], truncated: false };
|
|
6330
|
+
const brief = node.brief;
|
|
6331
|
+
const own = identityOf(brief?.tokens);
|
|
6332
|
+
const carried = identityOf(brief?.inherited);
|
|
6333
|
+
if (own.length || carried.length) {
|
|
6334
|
+
const seen = /* @__PURE__ */ new Set();
|
|
6335
|
+
const tokens = [];
|
|
6336
|
+
for (const t of [...own, ...carried]) {
|
|
6337
|
+
const key = JSON.stringify(t);
|
|
6338
|
+
if (seen.has(key)) continue;
|
|
6339
|
+
seen.add(key);
|
|
6340
|
+
tokens.push(t);
|
|
6341
|
+
}
|
|
6342
|
+
return { tokens, truncated: false };
|
|
6343
|
+
}
|
|
6276
6344
|
id = node.parentId;
|
|
6277
6345
|
}
|
|
6278
|
-
return [];
|
|
6346
|
+
return { tokens: [], truncated: id !== null };
|
|
6279
6347
|
}
|
|
6280
6348
|
|
|
6281
6349
|
// src/editScopeRules.ts
|
|
@@ -6356,6 +6424,13 @@ function planExpand(source, targetRatio) {
|
|
|
6356
6424
|
axis: "height"
|
|
6357
6425
|
};
|
|
6358
6426
|
}
|
|
6427
|
+
function wantsImplicitReshape(requested, parentNominal, sourcePixels) {
|
|
6428
|
+
const base = parentNominal ?? sourcePixels;
|
|
6429
|
+
if (!(requested.width > 0 && requested.height > 0 && base.width > 0 && base.height > 0)) return false;
|
|
6430
|
+
const want = requested.width / requested.height;
|
|
6431
|
+
const have = base.width / base.height;
|
|
6432
|
+
return Math.abs(want - have) / have > 0.01;
|
|
6433
|
+
}
|
|
6359
6434
|
function expandInstruction(plan, direction) {
|
|
6360
6435
|
const where = plan.axis === "width" ? "left and right" : "top and bottom";
|
|
6361
6436
|
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." : "";
|
|
@@ -6406,6 +6481,20 @@ Keep: every object in the same place at the same size, the same camera position
|
|
|
6406
6481
|
Avoid: new objects, products, people, text or watermarks; do not recompose, recentre, crop or resize anything already visible.` + own;
|
|
6407
6482
|
}
|
|
6408
6483
|
|
|
6484
|
+
// src/editSizeRules.ts
|
|
6485
|
+
var SAME_SHAPE_TOL = 0.01;
|
|
6486
|
+
var SHRINK_FLOOR = 0.8;
|
|
6487
|
+
function judgeEditSize(src, got) {
|
|
6488
|
+
if (!(src.width > 0 && src.height > 0 && got.width > 0 && got.height > 0)) return { action: "keep" };
|
|
6489
|
+
if (got.width === src.width && got.height === src.height) return { action: "keep" };
|
|
6490
|
+
const want = src.width / src.height;
|
|
6491
|
+
const have = got.width / got.height;
|
|
6492
|
+
if (Math.abs(have - want) / want > SAME_SHAPE_TOL) return { action: "keep" };
|
|
6493
|
+
const scale = Math.max(got.width, got.height) / Math.max(src.width, src.height);
|
|
6494
|
+
if (scale < SHRINK_FLOOR) return { action: "reject", scale };
|
|
6495
|
+
return { action: "resize", scale };
|
|
6496
|
+
}
|
|
6497
|
+
|
|
6409
6498
|
// src/cropRules.ts
|
|
6410
6499
|
function planCrop(source, targetRatio) {
|
|
6411
6500
|
if (!(source.width > 0 && source.height > 0 && targetRatio > 0)) return null;
|
|
@@ -8075,6 +8164,39 @@ function registerImageRoutes(app, deps) {
|
|
|
8075
8164
|
|
|
8076
8165
|
// src/release/notes.data.ts
|
|
8077
8166
|
var RELEASES = [
|
|
8167
|
+
{
|
|
8168
|
+
version: "0.6.9",
|
|
8169
|
+
date: "2026-08-29",
|
|
8170
|
+
title: "Refinements keep the shot.",
|
|
8171
|
+
sections: [
|
|
8172
|
+
{
|
|
8173
|
+
heading: "Refine",
|
|
8174
|
+
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."
|
|
8175
|
+
},
|
|
8176
|
+
{
|
|
8177
|
+
heading: "Presenters",
|
|
8178
|
+
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."
|
|
8179
|
+
},
|
|
8180
|
+
{
|
|
8181
|
+
heading: "Create",
|
|
8182
|
+
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."
|
|
8183
|
+
}
|
|
8184
|
+
]
|
|
8185
|
+
},
|
|
8186
|
+
{
|
|
8187
|
+
version: "0.6.8",
|
|
8188
|
+
date: "2026-08-29",
|
|
8189
|
+
sections: [
|
|
8190
|
+
{
|
|
8191
|
+
heading: "Create dialogs",
|
|
8192
|
+
body: "A new product, presenter or scene starts from an empty form, every time. Closing one now ends it rather than keeping what you typed for later, so nothing you walked away from can turn up in the next one, and the message that says so offers Undo if you did not mean to close it. A build that fails still hands everything back, photographs included, so trying again never asks for them twice. A reference you remove stays removed, even when another upload was still running, and a scene that was built while the app was restarting no longer leaves its references waiting in the next form."
|
|
8193
|
+
},
|
|
8194
|
+
{
|
|
8195
|
+
heading: "Create",
|
|
8196
|
+
body: "Selecting text in the brief with the mouse holds, instead of clearing the moment you let go."
|
|
8197
|
+
}
|
|
8198
|
+
]
|
|
8199
|
+
},
|
|
8078
8200
|
{
|
|
8079
8201
|
version: "0.6.7",
|
|
8080
8202
|
date: "2026-08-29",
|
|
@@ -8972,7 +9094,8 @@ function buildServer(opts) {
|
|
|
8972
9094
|
registerShowcaseRoutes(app, { templatesRoot });
|
|
8973
9095
|
app.get("/api/formats", async () => FORMATS);
|
|
8974
9096
|
async function compileEditBrief(brandId, parentId, brief, engineCaps, opts2) {
|
|
8975
|
-
const
|
|
9097
|
+
const inherited = inheritedIdentityTokens(parentId, (id) => core.store.getNode(id));
|
|
9098
|
+
const borrowed = inherited.tokens;
|
|
8976
9099
|
const already = new Set(
|
|
8977
9100
|
brief.tokens.filter((t) => t.t === "product" || t.t === "character" || t.t === "mark" || t.t === "ref").map((t) => JSON.stringify(t))
|
|
8978
9101
|
);
|
|
@@ -8996,6 +9119,28 @@ function buildServer(opts) {
|
|
|
8996
9119
|
const verdict = scopeOfInstruction(
|
|
8997
9120
|
brief.tokens.filter((t) => t.t === "text").map((t) => t.v).join(" ")
|
|
8998
9121
|
);
|
|
9122
|
+
const inheritedDirectives = [];
|
|
9123
|
+
let inheritedMark = false;
|
|
9124
|
+
for (const tok of inheritedTokens) {
|
|
9125
|
+
if (tok.t === "product") {
|
|
9126
|
+
const rec = (brandJson?.products ?? []).find((x) => x?.id === tok.id);
|
|
9127
|
+
if (rec)
|
|
9128
|
+
inheritedDirectives.push(
|
|
9129
|
+
productEditFidelityDirective(rec.promptName ?? rec.name),
|
|
9130
|
+
...productFactDirectives(rec)
|
|
9131
|
+
);
|
|
9132
|
+
} else if (tok.t === "character") {
|
|
9133
|
+
const rec = (brandJson?.characters ?? []).find((x) => x?.id === tok.id);
|
|
9134
|
+
if (rec)
|
|
9135
|
+
inheritedDirectives.push(
|
|
9136
|
+
characterEditIdentityDirective(rec.promptName ?? rec.name),
|
|
9137
|
+
...characterFactDirectives(rec)
|
|
9138
|
+
);
|
|
9139
|
+
} else if (tok.t === "mark" && !inheritedMark) {
|
|
9140
|
+
inheritedMark = true;
|
|
9141
|
+
inheritedDirectives.push(markEditDirective());
|
|
9142
|
+
}
|
|
9143
|
+
}
|
|
8999
9144
|
const compiled2 = compileBrief(brief, {
|
|
9000
9145
|
brand: brandJson,
|
|
9001
9146
|
images: core.images,
|
|
@@ -9006,6 +9151,7 @@ function buildServer(opts) {
|
|
|
9006
9151
|
editScope: verdict.scope,
|
|
9007
9152
|
editRemoval: verdict.removal ?? false,
|
|
9008
9153
|
inheritedIdentity: inheritedTokens.length > 0,
|
|
9154
|
+
inheritedDirectives,
|
|
9009
9155
|
// Only the explicit op drops the dimension promise: an implicit legacy
|
|
9010
9156
|
// expansion keeps its historical prompt byte for byte.
|
|
9011
9157
|
...opts2?.reshape === "extend" ? { editReshape: "extend" } : {}
|
|
@@ -9018,23 +9164,35 @@ function buildServer(opts) {
|
|
|
9018
9164
|
{ brand: brandJson, images: core.images, engineCaps: uncapped, templateById: sceneById }
|
|
9019
9165
|
);
|
|
9020
9166
|
identityWarnings = identity.warnings;
|
|
9021
|
-
|
|
9167
|
+
const productAngles = /* @__PURE__ */ new Map();
|
|
9168
|
+
inheritedAttachments = identity.attachments.filter((a) => {
|
|
9169
|
+
if (a.role === "product") {
|
|
9170
|
+
const n = (productAngles.get(String(a.id ?? a.hash)) ?? 0) + 1;
|
|
9171
|
+
productAngles.set(String(a.id ?? a.hash), n);
|
|
9172
|
+
return n <= 2;
|
|
9173
|
+
}
|
|
9174
|
+
return a.essential || a.role === "brand" || a.role === "reference";
|
|
9175
|
+
}).map((a) => ({ ...a, inherited: true }));
|
|
9022
9176
|
}
|
|
9023
9177
|
const cap2 = Math.max(0, engineCaps.maxReferenceImages - 1);
|
|
9024
9178
|
const merged = mergeEditAttachments(compiled2.attachments, inheritedAttachments, cap2);
|
|
9025
9179
|
const warnings = [...compiled2.warnings, ...identityWarnings.filter((w) => !compiled2.warnings.includes(w))];
|
|
9180
|
+
if (inherited.truncated)
|
|
9181
|
+
warnings.push("This thread is deeper than 64 steps, so identity attached before that could not be carried.");
|
|
9026
9182
|
if (merged.dropped.length) {
|
|
9027
9183
|
if (engineCaps.maxReferenceImages <= 1) {
|
|
9028
9184
|
warnings.push(
|
|
9029
9185
|
`${engineCaps.displayName} cannot carry reference images, so the identity rides on the source frame alone.`
|
|
9030
9186
|
);
|
|
9031
9187
|
} else {
|
|
9032
|
-
const
|
|
9033
|
-
|
|
9034
|
-
|
|
9035
|
-
|
|
9036
|
-
|
|
9037
|
-
|
|
9188
|
+
const keptLabels = new Set(merged.kept.map((a) => a.label));
|
|
9189
|
+
const names = [...new Set(merged.dropped.map((d) => d.label))].filter((l) => !keptLabels.has(l));
|
|
9190
|
+
if (names.length)
|
|
9191
|
+
warnings.push(
|
|
9192
|
+
`${engineCaps.displayName} reads ${engineCaps.maxReferenceImages} reference images and the frame being refined keeps one, so ${names.join(
|
|
9193
|
+
" and "
|
|
9194
|
+
)} ${names.length === 1 ? "was" : "were"} left out.`
|
|
9195
|
+
);
|
|
9038
9196
|
}
|
|
9039
9197
|
}
|
|
9040
9198
|
return {
|
|
@@ -9435,7 +9593,13 @@ function buildServer(opts) {
|
|
|
9435
9593
|
const srcBuf = core.images.read(String(srcHash));
|
|
9436
9594
|
const srcMeta = await sharp7(srcBuf).metadata();
|
|
9437
9595
|
if (srcMeta.width && srcMeta.height) expectShape = { width: srcMeta.width, height: srcMeta.height };
|
|
9438
|
-
|
|
9596
|
+
const parentFormat = parent?.brief?.tokens?.find((t) => t?.t === "format");
|
|
9597
|
+
const parentNominal = parentFormat && Number(parentFormat.w) > 0 && Number(parentFormat.h) > 0 ? { width: Number(parentFormat.w), height: Number(parentFormat.h) } : null;
|
|
9598
|
+
const reshapeIntended = reshape === "extend" || reshape === void 0 && !!srcMeta.width && !!srcMeta.height && !!compiled2?.width && !!compiled2?.height && wantsImplicitReshape({ width: compiled2.width, height: compiled2.height }, parentNominal, {
|
|
9599
|
+
width: srcMeta.width,
|
|
9600
|
+
height: srcMeta.height
|
|
9601
|
+
});
|
|
9602
|
+
if (reshapeIntended && srcMeta.width && srcMeta.height && compiled2?.width && compiled2?.height) {
|
|
9439
9603
|
expandPlan = planExpand({ width: srcMeta.width, height: srcMeta.height }, compiled2.width / compiled2.height);
|
|
9440
9604
|
}
|
|
9441
9605
|
if (reshape === "extend" && !expandPlan)
|
|
@@ -9465,8 +9629,12 @@ function buildServer(opts) {
|
|
|
9465
9629
|
...editRefs.length ? { referenceImages: editRefs.map((r) => r.path) } : {},
|
|
9466
9630
|
...editRefs.length ? { referenceRoles: editRefs.map((r) => r.role ?? "reference") } : {},
|
|
9467
9631
|
// An answer at the planned size lets compositeExpand skip its rescale,
|
|
9468
|
-
// which is one whole class of seam misalignment gone when honored.
|
|
9469
|
-
|
|
9632
|
+
// which is one whole class of seam misalignment gone when honored. A
|
|
9633
|
+
// plain refine states the source's own pixels for the same reason:
|
|
9634
|
+
// engines given no size answered at whatever size they liked, the
|
|
9635
|
+
// shrunken answer was stored, and the next refinement inherited it —
|
|
9636
|
+
// the chain that walked shots down to thumbnails.
|
|
9637
|
+
...expandPlan ? { width: expandPlan.width, height: expandPlan.height } : srcMeta.width && srcMeta.height ? { width: srcMeta.width, height: srcMeta.height } : {},
|
|
9470
9638
|
// Only an engine that can genuinely paint a margin is told where the
|
|
9471
9639
|
// picture sits; the rest would ignore it anyway.
|
|
9472
9640
|
...expandPlan && canOutpaint2 ? {
|
|
@@ -9567,6 +9735,38 @@ function buildServer(opts) {
|
|
|
9567
9735
|
const plan = expandPlan;
|
|
9568
9736
|
const original = editedFrom ? core.images.read(editedFrom) : null;
|
|
9569
9737
|
const localScope = kind === "edit" && !plan && editScope === "local" && original;
|
|
9738
|
+
const enforceEditCanvas = async (images) => {
|
|
9739
|
+
const srcMeta = await sharp7(original).metadata();
|
|
9740
|
+
if (!srcMeta.width || !srcMeta.height) return images;
|
|
9741
|
+
const out = [];
|
|
9742
|
+
for (const h of images) {
|
|
9743
|
+
const meta2 = await sharp7(core.images.read(h)).metadata();
|
|
9744
|
+
const got = { width: meta2.width ?? 0, height: meta2.height ?? 0 };
|
|
9745
|
+
const verdict = judgeEditSize({ width: srcMeta.width, height: srcMeta.height }, got);
|
|
9746
|
+
if (verdict.action === "reject")
|
|
9747
|
+
throw new Error(
|
|
9748
|
+
`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`
|
|
9749
|
+
);
|
|
9750
|
+
if (verdict.action === "resize") {
|
|
9751
|
+
app.log.info(
|
|
9752
|
+
{ nodeId: node.id, got: `${got.width}x${got.height}`, want: `${srcMeta.width}x${srcMeta.height}` },
|
|
9753
|
+
"edit: normalized the answer to the source canvas"
|
|
9754
|
+
);
|
|
9755
|
+
out.push(
|
|
9756
|
+
core.images.save(
|
|
9757
|
+
await sharp7(core.images.read(h)).resize(srcMeta.width, srcMeta.height, { fit: "fill" }).png().toBuffer()
|
|
9758
|
+
)
|
|
9759
|
+
);
|
|
9760
|
+
try {
|
|
9761
|
+
const fresh = core.store.getNode(node.id);
|
|
9762
|
+
const b = fresh?.brief ?? {};
|
|
9763
|
+
core.store.setBrief(node.id, { ...b, resizedFrom: [got.width, got.height] });
|
|
9764
|
+
} catch {
|
|
9765
|
+
}
|
|
9766
|
+
} else out.push(h);
|
|
9767
|
+
}
|
|
9768
|
+
return out;
|
|
9769
|
+
};
|
|
9570
9770
|
const post = plan ? expandMethod === "outpaint" ? async (images) => {
|
|
9571
9771
|
const out = [];
|
|
9572
9772
|
for (const h of images) {
|
|
@@ -9595,14 +9795,18 @@ function buildServer(opts) {
|
|
|
9595
9795
|
"expand: chose which frame to keep"
|
|
9596
9796
|
);
|
|
9597
9797
|
return images;
|
|
9598
|
-
} :
|
|
9599
|
-
|
|
9600
|
-
|
|
9601
|
-
const
|
|
9602
|
-
|
|
9603
|
-
|
|
9798
|
+
} : kind === "edit" && original ? async (images) => {
|
|
9799
|
+
let staged = images;
|
|
9800
|
+
if (localScope) {
|
|
9801
|
+
const out = [];
|
|
9802
|
+
for (const h of staged) {
|
|
9803
|
+
const { image, outcome, changed } = await preserveOutsideChange(original, core.images.read(h));
|
|
9804
|
+
app.log.info({ nodeId: node.id, outcome, changed }, "local edit");
|
|
9805
|
+
out.push(outcome === "composited" ? core.images.save(image) : h);
|
|
9806
|
+
}
|
|
9807
|
+
staged = out;
|
|
9604
9808
|
}
|
|
9605
|
-
return
|
|
9809
|
+
return enforceEditCanvas(staged);
|
|
9606
9810
|
} : void 0;
|
|
9607
9811
|
const nodeBudgetMs = kind === "generation" && runEngine.capabilities().id === "codex-cli" ? codexNodeBudgetMs(Math.min(Math.max(1, Number(count)), 8)) : void 0;
|
|
9608
9812
|
void runNode(node.id, runEngine, estimate, work, expectShape, post, nodeBudgetMs).catch(
|