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 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 `Generate one flawless, 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)` : "");
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(colors, `edit: ${req.instruction}`, 1024, 1024, req.instruction.length);
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
- if (p.preservationNotes) productDirectives.push(String(p.preservationNotes));
3319
- if (p.negativeConstraints) productDirectives.push(`Avoid: ${p.negativeConstraints}`);
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
- `Its real-world size is ${p.dimensions} \u2014 keep it at true scale relative to everything else in frame.`
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
- if (c.identityNotes) personDirectives.push(String(c.identityNotes));
3352
- if (c.negativeConstraints?.length)
3353
- personDirectives.push(`Avoid: ${[].concat(c.negativeConstraints).join(", ")}`);
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 = 8;
6263
- var tokensOf = (node) => {
6264
- const t = node?.brief?.tokens;
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 identity = tokensOf(node).filter(
6273
- (t) => t.t === "product" || t.t === "character" || t.t === "mark" || t.t === "ref"
6274
- );
6275
- if (identity.length) return identity;
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 borrowed = inheritedIdentityTokens(parentId, (id) => core.store.getNode(id));
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
- inheritedAttachments = identity.attachments.filter((a) => a.essential || a.role === "brand" || a.role === "reference").map((a) => ({ ...a, inherited: true }));
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 names = [...new Set(merged.dropped.map((d) => d.label))];
9033
- warnings.push(
9034
- `${engineCaps.displayName} reads ${engineCaps.maxReferenceImages} reference images and the frame being refined keeps one, so ${names.join(
9035
- " and "
9036
- )} ${names.length === 1 ? "was" : "were"} left out.`
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
- if (srcMeta.width && srcMeta.height && compiled2?.width && compiled2?.height) {
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
- ...expandPlan ? { width: expandPlan.width, height: expandPlan.height } : {},
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
- } : localScope ? async (images) => {
9599
- const out = [];
9600
- for (const h of images) {
9601
- const { image, outcome, changed } = await preserveOutsideChange(original, core.images.read(h));
9602
- app.log.info({ nodeId: node.id, outcome, changed }, "local edit");
9603
- out.push(outcome === "composited" ? core.images.save(image) : h);
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 out;
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(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "scenri",
3
- "version": "0.6.7",
3
+ "version": "0.6.9",
4
4
  "author": "Tony Gorb <hello@scenri.co>",
5
5
  "license": "AGPL-3.0-only",
6
6
  "type": "module",