scenri 0.6.0 → 0.6.2

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,21 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.6.2](https://github.com/tonygorb/Scenri/compare/v0.6.1...v0.6.2) (2026-08-28)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * a custom scene keeps the figure and treatment its references show ([667cede](https://github.com/tonygorb/Scenri/commit/667cedeb4ebac53bf3d7c5cbcf0f8f80745c7431))
9
+ * a custom scene keeps the figure and treatment its references show ([b6532c9](https://github.com/tonygorb/Scenri/commit/b6532c9b47c404243a3ad0994bda4dd634e109e7))
10
+
11
+ ## [0.6.1](https://github.com/tonygorb/Scenri/compare/v0.6.0...v0.6.1) (2026-08-26)
12
+
13
+
14
+ ### Bug Fixes
15
+
16
+ * refine aspect ratio belongs to the generation being refined ([1177f95](https://github.com/tonygorb/Scenri/commit/1177f955a926eca917096b328e8ec4fd86f913a2))
17
+ * refine aspect ratio belongs to the generation being refined ([2ef201c](https://github.com/tonygorb/Scenri/commit/2ef201c079a8a1c07fdbc18e43f4962d8689a41d))
18
+
3
19
  ## [0.6.0](https://github.com/tonygorb/Scenri/compare/v0.5.1...v0.6.0) (2026-08-26)
4
20
 
5
21
 
package/dist/serve.js CHANGED
@@ -1286,7 +1286,13 @@ var REFERENCE_ROLE_DIRECTIVE = {
1286
1286
  product: "the exact product \u2014 preserve its label, shape, colors and design faithfully; do not redesign it",
1287
1287
  character: "the exact person \u2014 match their face, facial structure, skin, hair and build exactly; their clothing, pose and background are capture context, not styling to reproduce",
1288
1288
  brand: "the brand's own mark \u2014 if the direction calls for the mark to appear, reproduce it exactly as drawn, same colours, letterforms and proportions; otherwise take only its colour and treatment, and never its subject, geometry or composition",
1289
- scene: "a reference for the environment and light only \u2014 take no subject, product or person from it",
1289
+ // Only a figure-led scene attaches one of these now, so this says what that
1290
+ // case actually needs. It used to read "environment and light only - take no
1291
+ // subject or person from it", which handed the model a photograph of a face
1292
+ // treatment and told it to ignore the treatment. A hand-attached reference
1293
+ // says "match ... treatment" and works; this now says the same thing, with the
1294
+ // identity carve-out a scene needs and a lone reference does not.
1295
+ scene: "a reference for this world and for the treatment applied to the figure in it \u2014 match the environment, the light, and the material, density, scale, finish and spread of that treatment, including which parts of the form it covers and how far it reaches; take no identity from the person in it, and no product or brand",
1290
1296
  composition: "a reference for framing, camera angle and pose only \u2014 take no subject, color, material or branding from it",
1291
1297
  style: "a reference for overall treatment and mood only \u2014 take no composition, subject or product detail from it",
1292
1298
  reference: "a reference to match in composition, lighting and treatment"
@@ -2241,10 +2247,10 @@ function presenterBody(req, refCount) {
2241
2247
  return `These are photographs of one person, supplied by the person casting them.${same}${ask} Write a casting sheet: what a photographer would need in order to recognise this person again in a different shot. Do not name, identify, or guess who this person is, and do not use any proper name anywhere in your answer. Describe the person, not the photograph: their build, face and hair belong to them, but the background, crop, pose, clothing and lighting of these pictures belong to a shoot and must be left out. ${OUT_FILE} must be a JSON object with exactly these keys: "promptName": a short noun phrase a generator can be handed, such as "a woman in her early thirties with dark shoulder-length waves"; "presentation": either "woman" or "man"; "descriptor": a three-beat casting caption joined by " \xB7 ", such as "Warm editorial \xB7 dark waves \xB7 composed"; "ageRange": an approximate range such as "early 30s"; "hair": colour, length, texture and how it is worn; "identityNotes": one paragraph naming the two or three features that must survive every generation, drawn from face shape, eyes, nose, mouth, jaw, skin, distinctive marks, and build where it is visible; "negativeConstraints": an array of short refusals for the drift these photographs invite, such as "no youth-smoothing that erases the natural lines"; "suitableCategories": the industries this person would be cast for.${categories} "coverage": an array of at most two short sentences naming a view that is missing and would make this person more consistent, such as "A three-quarter photo would pin the cheekbones down." Use an empty array when the coverage is already good.`;
2242
2248
  }
2243
2249
  function sceneBody(req) {
2244
- const ask = req.instruction ? ` What the person wants from it: ${req.instruction}.` : "";
2250
+ const ask = req.instruction ? ` The person who chose these references says what matters in them: ${req.instruction}. Treat that as the deciding word: whatever it calls essential IS essential even if only one reference shows it, and whatever it tells you to ignore stays out even if every reference contains it.` : "";
2245
2251
  const collections = req.vocabulary?.collections?.length ? ` Choose "collections" only from this list: ${req.vocabulary.collections.join(", ")}.` : "";
2246
2252
  const verticals = req.vocabulary?.verticals?.length ? ` Choose "verticals" only from this list: ${req.vocabulary.verticals.join(", ")}.` : "";
2247
- return `These are references for a place, not a picture to copy.${ask} Extract the reusable visual world behind them: environment, architecture, surfaces and materials, the character of the light, atmosphere, palette, depth, and the photographic language. Never name or describe a brand, a logo, a product, or a person. If a reference contains a bottle, a garment, a model or a mark, that thing is a visitor, not part of the world: leave it out completely, because something else will be staged here later. Describe where a photograph happens, not what is standing in it. Use no placeholders of any kind. ${OUT_FILE} must be a JSON object with exactly these keys: "name": two or three words a person would call this place, such as "Wet Basalt Shore"; "promptName": the same place named for a generator, at most six words; "lighting": a short phrase naming the light, such as "Low directional sunset, long shadows across wet stone"; "description": one sentence a person reads on a card; "subject": "product" if this world suits a staged object, "person" if it suits someone photographed in it, "either" when it truly suits both; "prompt": three or four sentences describing the set itself, in the present tense, naming nothing that is staged in it; "camera": the camera tendency of this world in a short phrase, or an empty string when it has none; "keywords": five to ten single words someone might search for; "collections": one or two themed groupings;${collections} "verticals": the industries this world flatters.${verticals}`;
2253
+ return `These are references for a place, not a picture to copy.${ask} Extract the reusable visual world behind them: environment, architecture, surfaces and materials, the character of the light, atmosphere, palette, depth, and the photographic language. Name materials rather than colours - travertine, cracked clay, waxed canvas, brushed steel, raw concrete - and say how the space layers from foreground through middle ground to background. Say so when a surface is reflective or transmissive: a mirror, a wet floor, chrome, glass, still water. Those govern how everything in the frame is lit, and they are the first thing lost to a generic description. Treat signage and lettering as typographic texture belonging to the environment: say that it is there and what it is made of, and never transcribe the words or name the brand. What you leave out is identity, not presence. Never name or describe a brand, a logo, a product model or a wordmark, and do not use any proper name anywhere in your answer. A person in a reference is recorded only as a figure: their scale in the frame, their distance, their posture, the kind of act the space is arranged around, and how a body catches this light. Never their face, hair, age, wardrobe, or anything that would identify them, and never as a particular person - "a figure at the water's edge", never "a woman in a red coat". A figure is not always something standing in a world. Sometimes the figure IS the world: a close portrait whose entire art direction is what has been done to the person - a face covered in stickers, painted skin, a veil, a mask, fabric wrapped over the head, a body reduced to a silhouette. When that is what the reference is, the treatment is the single most important thing to record, not a detail to drop along with the identity. Record what was done; never who it was done to. Only a living person counts as a figure. A mannequin, a statue, a face on a poster, a billboard or a screen, a reflection with nobody outside it, or a cast shadow is a thing in the environment: describe it in the set, not as a figure. Where several people appear, only the one the composition is built around is the figure; the rest are crowd, and belong in the set with everything else. A product, garment or mark staged in a reference is a visitor: leave the object itself out, but keep what it tells you about the place - the surface it sat on, the scale it implies, how densely the space is dressed. Where several references are attached, the world is what they share; whatever appears in only one of them is a visitor. Use no placeholders of any kind. ${OUT_FILE} must be a JSON object with exactly these keys: "name": two or three words a person would call this place, such as "Wet Basalt Shore"; "promptName": the same place named for a generator, at most six words; "lighting": a short phrase naming the light, such as "Low directional sunset, long shadows across wet stone"; "description": one sentence a person reads on a card; "subject": "product" if this world suits a staged object, "person" if it suits someone photographed in it, "either" when it truly suits both; "prompt": four or five sentences describing the set itself, in the present tense, naming nothing branded and nobody identifiable. Ambient human presence belongs here, written the way the rest of the set is written - a figure far off in the frame, worn seats, a table dressed for two; "figure": when this concept depends on a person being in it, one short phrase for the role they play - their framing, their scale, and what they are doing - such as "someone is seated at the stone ledge, mid-ground, at human scale" or "one person at close portrait range, squared to camera, filling the frame". A portrait counts. If the reference is built around a person and would stop being this concept without one, that is a figure, however much of the frame they occupy. Use an empty string only when the concept genuinely survives with nobody in it, or when the people present are passers-by rather than the point - describe those in "prompt" with the rest of the set instead. This is a different question from "subject": "subject" is who this world flatters, "figure" is whether the concept needs a body at all; "figureTreatment": what has been done TO that figure, when something has - one short phrase, such as "the face entirely covered in overlapping printed stickers" or "the head and shoulders wrapped in translucent fabric". Describe the treatment, what it is made of, and the character of any printing on it - the kind of label or product it imitates, its typographic style, illustration and colour - because that graphic character is usually the point. Say what kind of thing the printing is without naming a real company from the references. Be specific to what is actually in front of you rather than to the general idea: how much of the surface is covered and how much is left bare, the material and finish - glossy vinyl, matte paper, foil, fabric - the size of the pieces, and - separately from how many there are - how far they reach across the form and which parts they land on, since a treatment can be sparse and still cover the whole face. If the references disagree about how heavy it is, give the range rather than picking one. Use an empty string when nothing has been done to them. Leave it empty too when "figure" is empty; "camera": the camera tendency of this world in a short phrase - height, distance, lens feel, depth of field - or an empty string when it has none. Camera belongs here and never in "prompt"; "keywords": five to ten single words someone might search for; "coverage": an array of at most two short sentences naming what another reference would buy, such as "A wider frame would pin down how the room is laid out." Say so here if these references look like different places, or if they are mostly a person or a packshot with too little environment to build a world from. Use an empty array when they are good; "collections": one or two themed groupings;${collections} "verticals": the industries this world flatters.${verticals}`;
2248
2254
  }
2249
2255
  function parseDraft(req, raw) {
2250
2256
  const text = raw.trim().replace(/^```(?:json)?\s*/i, "").replace(/```$/, "").trim();
@@ -2298,6 +2304,8 @@ function parseScene(req, o) {
2298
2304
  }
2299
2305
  if (problems.length) return { ok: false, problems };
2300
2306
  const camera = cap(str(o.camera), 200);
2307
+ const figure = oneLine(o.figure, 120);
2308
+ const figureTreatment = figure ? oneLine(o.figureTreatment, 160) : "";
2301
2309
  return {
2302
2310
  ok: true,
2303
2311
  draft: {
@@ -2310,11 +2318,19 @@ function parseScene(req, o) {
2310
2318
  verticals: pick(o.verticals, req.vocabulary?.verticals, 4),
2311
2319
  keywords: list(o.keywords, 10, 40),
2312
2320
  prompt: cap(prompt, 2e3),
2313
- camera: camera || void 0
2321
+ camera: camera || void 0,
2322
+ figure: figure || void 0,
2323
+ figureTreatment: figureTreatment || void 0,
2324
+ coverage: list(o.coverage, 2, 160)
2314
2325
  }
2315
2326
  };
2316
2327
  }
2317
2328
  var str = (v) => typeof v === "string" ? v.trim() : "";
2329
+ function oneLine(v, max) {
2330
+ const one = str(v).replace(/\s+/g, " ");
2331
+ if (!one || /\{[^}]*\}/.test(one)) return "";
2332
+ return cap(one, max);
2333
+ }
2318
2334
  var cap = (v, max) => v.length > max ? v.slice(0, max).trim() : v;
2319
2335
  function list(v, max, each) {
2320
2336
  if (!Array.isArray(v)) return [];
@@ -3071,6 +3087,28 @@ function shotSpecifiesCamera(text) {
3071
3087
  text
3072
3088
  );
3073
3089
  }
3090
+ function sceneFigureDirectives(opts) {
3091
+ const figure = opts.figure.trim().replace(/[.\s]+$/, "");
3092
+ if (!figure) return [];
3093
+ const treatment = (opts.treatment ?? "").trim().replace(/[.\s]+$/, "");
3094
+ const out = [];
3095
+ if (opts.hasPerson) {
3096
+ out.push(
3097
+ `This world is built around one figure: ${figure}. The attached presenter is that figure. Any person the scene direction describes IS the presenter and never a second person, and their identity comes from their own attached photograph alone, never from anything the scene direction says about a body.`
3098
+ );
3099
+ } else {
3100
+ out.push(
3101
+ `This world is built around one figure: ${figure}. Someone fills that role in the frame, and they are nobody in particular: an anonymous person invented for this photograph only, with no recognisable identity to preserve and nothing about them carried anywhere else. Show them unless the direction above asks for no people.`
3102
+ );
3103
+ }
3104
+ if (treatment) {
3105
+ const who = opts.hasPerson ? "The face and body underneath are still exactly theirs - same structure, same proportions, same build - and any earlier instruction that their features must survive unchanged is a rule about who they are, which this does not alter. " : "";
3106
+ out.push(
3107
+ `The art direction of this world is what has been done to that figure: ${treatment}. Render it as a real physical treatment, following the shape of the face and body it sits on rather than floating in the frame. Spread it across the whole form the way the reference does, reaching every part it covers there - brow, forehead, nose, both cheeks, jaw - instead of massing it in one area and leaving the rest untouched. Reaching wide is not the same as covering more: keep the number of pieces and the bare surface between them exactly as the description says, so a sparse treatment stays sparse while still touching every part of the form. Each piece sits on the plane beneath it, curving and catching light with the surface it is stuck to. ${who}The figure is bodily present and in shot: where the treatment covers or hides them, that is the photograph working as intended and never a reason to leave them out, crop them away, or reduce them to a shadow. If no person appears in this shot, the treatment does not go with them: it is what this world looks like, so it applies to whatever the frame does hold - the product, the surfaces, the set - as real pieces resting on those things. Applied on top, never redesigning them: the product keeps the exact form, colour, material and its own printed label that its reference shows, with the treatment sitting over it. Where the treatment carries printing, render it as genuinely designed print: real letterforms, readable words, numerals, illustration and colour, at the quality of commercial label artwork. Invent the companies - every name, logotype and piece of packaging artwork must be plausible but fictional, resembling no existing brand. That includes near-misses: do not borrow, extend or re-spell a name that appears in any attached reference, and use ordinary words for the produce itself rather than any company that sells it.`
3108
+ );
3109
+ }
3110
+ return out;
3111
+ }
3074
3112
  function sceneGuardDirectives(opts) {
3075
3113
  const out = [];
3076
3114
  if (opts.hasProduct) {
@@ -3113,6 +3151,7 @@ function markLabel(brand, logo) {
3113
3151
  // src/brief.ts
3114
3152
  var PRODUCT_REF_MAX = 3;
3115
3153
  var CHARACTER_REF_MAX = 2;
3154
+ var SCENE_REF_MAX = 1;
3116
3155
  var FORMATS = [
3117
3156
  { id: "square", label: "Square 1:1", w: 1024, h: 1024 },
3118
3157
  { id: "story", label: "Story 9:16", w: 1080, h: 1920 },
@@ -3289,6 +3328,14 @@ function compileBrief(brief, ctx) {
3289
3328
  }
3290
3329
  inlineTemplates.push(t);
3291
3330
  append(composePrompt(t, { fields: brief.templateFields ?? {}, notes: "" }));
3331
+ if (ctx.mode !== "edit" && t.figure) {
3332
+ for (const r of (t.refs ?? []).slice(0, SCENE_REF_MAX)) {
3333
+ const h = assetHash(r?.file);
3334
+ if (h && ctx.images.has(h)) {
3335
+ attachments.push({ role: "scene", id: t.id, label: t.name, hash: h, essential: false });
3336
+ }
3337
+ }
3338
+ }
3292
3339
  break;
3293
3340
  }
3294
3341
  case "format": {
@@ -3347,6 +3394,7 @@ function compileBrief(brief, ctx) {
3347
3394
  // attached one is deliberate.
3348
3395
  "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."
3349
3396
  ] : [];
3397
+ const figureDirectives = scene?.figure ? sceneFigureDirectives({ figure: scene.figure, treatment: scene.figureTreatment, hasPerson }) : [];
3350
3398
  const closeUpDirectives = hasPerson && /\bclose[- ]?up\b|\bmacro\b|\bzoom(?:ed)?\b|\bDOF\b|\bdepth of field\b/i.test(sentence) ? [
3351
3399
  "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."
3352
3400
  ] : [];
@@ -3364,6 +3412,7 @@ function compileBrief(brief, ctx) {
3364
3412
  ...productDirectives,
3365
3413
  ...personDirectives,
3366
3414
  ...pairDirectives,
3415
+ ...figureDirectives,
3367
3416
  ...closeUpDirectives,
3368
3417
  ...otherDirectives,
3369
3418
  ...cameraDirectives,
@@ -3375,8 +3424,8 @@ function compileBrief(brief, ctx) {
3375
3424
  if (allDirectives.length) prompt = `${prompt}${prompt.endsWith(".") ? "" : "."} ${dedupe(allDirectives).join(" ")}`;
3376
3425
  const max = ctx.engineCaps.maxReferenceImages;
3377
3426
  const { kept, dropped } = allocateAttachments(attachments, max);
3378
- if (dropped.length) {
3379
- const names = [...new Set(dropped.map((d) => d.label))];
3427
+ if (dropped.some((d) => d.role !== "scene")) {
3428
+ const names = [...new Set(dropped.filter((d) => d.role !== "scene").map((d) => d.label))];
3380
3429
  warnings.push(
3381
3430
  `${ctx.engineCaps.displayName} reads ${max} reference image${max === 1 ? "" : "s"}, so ${names.join(
3382
3431
  " and "
@@ -3888,6 +3937,20 @@ var brand_schema_default = {
3888
3937
  type: "string",
3889
3938
  description: "What the user asked for in their own words when this scene was built. Kept so a later refinement has the original intent."
3890
3939
  },
3940
+ figure: {
3941
+ type: "string",
3942
+ not: {
3943
+ pattern: "\\{[^}]*\\}"
3944
+ },
3945
+ description: 'The figure this concept depends on, when it depends on one: "one person at close portrait range, squared to camera, filling the frame". A role, never a person - it names the part a body plays in this world, never whose body. Absent means the concept survives with nobody in it; passing human presence belongs in prompt with the rest of the set. A brief binds this to an attached presenter, so a world built around a body and a real presenter never compile as two people.'
3946
+ },
3947
+ figureTreatment: {
3948
+ type: "string",
3949
+ not: {
3950
+ pattern: "\\{[^}]*\\}"
3951
+ },
3952
+ description: "What has been applied to that figure, when something has: stickers over the face, paint, a veil, fabric, reduction to a silhouette. This is art direction, not identity - the scene owns the treatment and the presenter owns the face underneath it, and nothing here is ever written back to a presenter. Names the material and its form, never the words or artwork printed on it. Meaningless without figure."
3953
+ },
3891
3954
  refs: {
3892
3955
  type: "array",
3893
3956
  items: {
@@ -3900,7 +3963,7 @@ var brand_schema_default = {
3900
3963
  }
3901
3964
  }
3902
3965
  },
3903
- description: "The user's inspiration images. Display only: a scene contributes text to a generation, never pixels, so these are never sent to a generator as references."
3966
+ description: "The user's inspiration images. Used to read this scene into words, to draw its preview card, and - only when the scene is built around a figure - one of them is attached to a shot as environment reference. That exception exists because a dense figure treatment cannot survive being described in words. Where it applies, the reference is marked as scene context: the environment and treatment are taken from it, the people, products and marks in it are not."
3904
3967
  },
3905
3968
  preview: {
3906
3969
  $ref: "#/$defs/assetRef",
@@ -5484,8 +5547,13 @@ function sceneRecordFrom(input, base) {
5484
5547
  if (camera) scene.camera = camera;
5485
5548
  const keywords = has("keywords") ? strList(input.keywords, 12, 40) : base?.keywords;
5486
5549
  if (keywords?.length) scene.keywords = keywords;
5487
- const instruction = has("instruction") ? str3(input.instruction, 400) : base?.instruction;
5550
+ const written = has("instruction") ? str3(input.instruction, 400) : "";
5551
+ const instruction = written || base?.instruction;
5488
5552
  if (instruction) scene.instruction = instruction;
5553
+ const figure = has("figure") ? str3(input.figure, 120).replace(/\s+/g, " ") : base?.figure;
5554
+ if (figure && !/\{[^}]*\}/.test(figure)) scene.figure = figure;
5555
+ const treatment = has("figureTreatment") ? str3(input.figureTreatment, 160).replace(/\s+/g, " ") : base?.figureTreatment;
5556
+ if (scene.figure && treatment && !/\{[^}]*\}/.test(treatment)) scene.figureTreatment = treatment;
5489
5557
  if (refs?.length) scene.refs = refs;
5490
5558
  if (previewRef) scene.preview = previewRef;
5491
5559
  return { ok: true, scene };
@@ -5527,7 +5595,7 @@ function presenterRecordFrom(input, base) {
5527
5595
  return { ok: true, presenter };
5528
5596
  }
5529
5597
  function lintSceneProse(brandJson, scene) {
5530
- const haystack = `${scene.prompt} ${scene.description}`.toLowerCase();
5598
+ const haystack = `${scene.prompt} ${scene.description} ${scene.figure ?? ""} ${scene.figureTreatment ?? ""}`.toLowerCase();
5531
5599
  const named = /* @__PURE__ */ new Set();
5532
5600
  const consider = (raw) => {
5533
5601
  const n = String(raw ?? "").trim();
@@ -5576,7 +5644,10 @@ function startAssetBuild(deps, input) {
5576
5644
  const { core } = deps;
5577
5645
  const brand = core.store.getBrand(input.brandId);
5578
5646
  if (!brand) throw Object.assign(new Error("brand not found"), { statusCode: 404 });
5579
- const hashes = input.imageHashes.filter((h) => /^[a-f0-9]{32}$/.test(h) && core.images.has(h));
5647
+ const prior = input.sceneId ? brandSceneById(brand.json, input.sceneId) : void 0;
5648
+ if (input.sceneId && !prior) throw Object.assign(new Error("scene not found"), { statusCode: 404 });
5649
+ const supplied = input.imageHashes.length ? input.imageHashes : (prior?.refs ?? []).map((r) => String(r?.file ?? "").replace(/^asset:/, ""));
5650
+ const hashes = supplied.filter((h) => /^[a-f0-9]{32}$/.test(h) && core.images.has(h));
5580
5651
  if (input.kind === "presenter" && !hashes.length) {
5581
5652
  throw Object.assign(new Error("add at least one photo of this person"), { statusCode: 400 });
5582
5653
  }
@@ -5597,6 +5668,7 @@ function startAssetBuild(deps, input) {
5597
5668
  warnings: [],
5598
5669
  coverage: [],
5599
5670
  facets: strList(input.facets, 8, 40),
5671
+ sceneId: input.sceneId ?? null,
5600
5672
  error: null,
5601
5673
  startedAt: (/* @__PURE__ */ new Date()).toISOString(),
5602
5674
  finished: false
@@ -5906,15 +5978,17 @@ async function crop(core, hash, region) {
5906
5978
  }
5907
5979
  async function runSceneBuild(deps, job, hashes, instruction, signal) {
5908
5980
  const { core } = deps;
5981
+ const prior = job.sceneId ? brandSceneById(core.store.getBrand(job.brandId)?.json ?? {}, job.sceneId) : void 0;
5909
5982
  let draft = null;
5910
5983
  if (deps.analyzer) {
5911
- patch(job, { stage: "analyzing", message: "Reading the references" });
5984
+ patch(job, { stage: "analyzing", message: prior ? "Reading the references again" : "Reading the references" });
5912
5985
  draft = await deps.analyzer.analyze(
5913
5986
  {
5914
5987
  kind: "scene",
5915
5988
  imagePaths: hashes.map((h) => core.images.pathFor(h)),
5916
5989
  name: job.name,
5917
5990
  instruction: instruction || void 0,
5991
+ ...prior ? { priorDraft: prior, correction: instruction || void 0 } : {},
5918
5992
  vocabulary: deps.vocabulary
5919
5993
  },
5920
5994
  signal
@@ -5923,29 +5997,41 @@ async function runSceneBuild(deps, job, hashes, instruction, signal) {
5923
5997
  throw new Error("describe the place in a sentence, or install the Codex CLI to read the references");
5924
5998
  }
5925
5999
  if (signal.aborted) throw new Error("cancelled");
5926
- const built = sceneRecordFrom({
5927
- name: job.name || draft?.name || "New scene",
5928
- promptName: draft?.promptName,
5929
- lighting: draft?.lighting,
5930
- description: draft?.description ?? instruction,
5931
- subject: draft?.subject ?? "either",
5932
- prompt: draft?.prompt ?? instruction,
5933
- camera: draft?.camera,
5934
- collections: draft?.collections,
5935
- verticals: job.facets.length ? job.facets : draft?.verticals,
5936
- keywords: draft?.keywords,
5937
- instruction,
5938
- refHashes: hashes
5939
- });
6000
+ const built = sceneRecordFrom(
6001
+ {
6002
+ name: job.name || draft?.name || "New scene",
6003
+ promptName: draft?.promptName,
6004
+ lighting: draft?.lighting,
6005
+ description: draft?.description ?? instruction,
6006
+ subject: draft?.subject ?? "either",
6007
+ prompt: draft?.prompt ?? instruction,
6008
+ camera: draft?.camera,
6009
+ figure: draft?.figure,
6010
+ figureTreatment: draft?.figureTreatment,
6011
+ collections: draft?.collections,
6012
+ verticals: job.facets.length ? job.facets : draft?.verticals,
6013
+ keywords: draft?.keywords,
6014
+ instruction,
6015
+ refHashes: hashes
6016
+ },
6017
+ prior
6018
+ );
5940
6019
  if (!built.ok) throw new Error(built.error);
5941
6020
  const scene = built.scene;
6021
+ if (draft?.coverage?.length) patch(job, { coverage: draft.coverage });
5942
6022
  let previewHash = null;
5943
6023
  if (deps.engine) {
5944
6024
  patch(job, { stage: "building", steps: 1, message: "Drawing the place" });
5945
6025
  try {
6026
+ const previewRefs = hashes.slice(0, deps.engine.capabilities().maxReferenceImages).map((h) => core.images.pathFor(h));
5946
6027
  previewHash = await trimEdgeBars(
5947
6028
  core,
5948
- await draw(deps, { prompt: scenePreviewPrompt(scene), brandId: job.brandId, signal })
6029
+ await draw(deps, {
6030
+ prompt: scenePreviewPrompt(scene),
6031
+ brandId: job.brandId,
6032
+ ...previewRefs.length ? { referenceImages: previewRefs, referenceRoles: previewRefs.map(() => "scene") } : {},
6033
+ signal
6034
+ })
5949
6035
  );
5950
6036
  scene.preview = `asset:${previewHash}`;
5951
6037
  patch(job, { step: 1, previewHash });
@@ -5959,7 +6045,9 @@ async function runSceneBuild(deps, job, hashes, instruction, signal) {
5959
6045
  const brand = core.store.getBrand(job.brandId);
5960
6046
  const warnings = [...job.warnings, ...lintSceneProse(brand?.json ?? {}, scene)];
5961
6047
  commit(core, job.brandId, (json) => {
5962
- json.scenes = [...brandScenes(json), scene];
6048
+ const rows = brandScenes(json);
6049
+ const at = rows.findIndex((s) => s?.id === scene.id);
6050
+ json.scenes = at >= 0 ? rows.map((s, i) => i === at ? scene : s) : [...rows, scene];
5963
6051
  });
5964
6052
  patch(job, {
5965
6053
  stage: "done",
@@ -5971,7 +6059,8 @@ async function runSceneBuild(deps, job, hashes, instruction, signal) {
5971
6059
  });
5972
6060
  }
5973
6061
  function scenePreviewPrompt(scene) {
5974
- return `Full-bleed photograph filling the entire frame edge to edge with no border, frame, letterbox band or matte of any kind. ${scene.prompt} ${scene.lighting ? `${scene.lighting}. ` : ""}The set is empty: no product, no person, no hands, no text, no logos, no watermarks anywhere in the frame.`;
6062
+ const body = scene.figure ? `A figure is in this photograph: ${scene.figure.replace(/[.\s]+$/, "")}. ` + (scene.figureTreatment ? `The art direction is what has been done to them: ${scene.figureTreatment.replace(/[.\s]+$/, "")}, rendered as a real physical treatment that follows the shape it sits on. ` : "") + "They are nobody in particular: do not reproduce any person from the attached reference images, and give them no recognisable identity. No product, no logos, no watermarks, and no readable words anywhere in the frame." : "The set is empty: no product, no person, no hands, no text, no logos, no watermarks anywhere in the frame.";
6063
+ return `Full-bleed photograph filling the entire frame edge to edge with no border, frame, letterbox band or matte of any kind. ${scene.prompt} ${scene.lighting ? `${scene.lighting}. ` : ""}` + body;
5975
6064
  }
5976
6065
  async function draw(deps, req) {
5977
6066
  const engine = deps.engine;
@@ -5997,6 +6086,12 @@ async function draw(deps, req) {
5997
6086
  function runningAssetBuildCount() {
5998
6087
  return running2.size;
5999
6088
  }
6089
+ function sceneBuildRunning(brandId, sceneId) {
6090
+ for (const b of builds.values()) {
6091
+ if (b.brandId === brandId && b.sceneId === sceneId && !b.finished) return true;
6092
+ }
6093
+ return false;
6094
+ }
6000
6095
  var LOOPBACK = ["localhost", "127.0.0.1", "::1"];
6001
6096
  var ACCESS_COOKIE = "sc_access";
6002
6097
  var LEGACY_ACCESS_COOKIE = "bt_access";
@@ -7372,6 +7467,30 @@ function registerAssetBuildRoutes(app, deps) {
7372
7467
  });
7373
7468
  return { ok: true };
7374
7469
  });
7470
+ app.post("/api/brands/:id/scenes/:sceneId/reread", async (req, reply) => {
7471
+ const brand = brandOr404(req, reply);
7472
+ if (!brand) return;
7473
+ const id = String(req.params.sceneId);
7474
+ const scene = brandScenes(brand.json).find((s) => s.id === id);
7475
+ if (!scene) return reply.status(404).send({ error: "scene not found" });
7476
+ if (!(scene.refs ?? []).length)
7477
+ return reply.status(400).send({ error: "this scene was written from words, so there is nothing to read again" });
7478
+ if (sceneBuildRunning(brand.id, id))
7479
+ return reply.status(409).send({ error: "this scene is already being read again" });
7480
+ const body = req.body ?? {};
7481
+ try {
7482
+ return startAssetBuild(await buildDeps(), {
7483
+ brandId: brand.id,
7484
+ kind: "scene",
7485
+ sceneId: id,
7486
+ name: scene.name,
7487
+ instruction: body.correction == null ? void 0 : String(body.correction),
7488
+ imageHashes: []
7489
+ });
7490
+ } catch (err) {
7491
+ return reply.status(err.statusCode ?? 500).send({ error: err.message ?? "could not start" });
7492
+ }
7493
+ });
7375
7494
  app.post("/api/brands/:id/scenes/:sceneId/preview", async (req, reply) => {
7376
7495
  const brand = brandOr404(req, reply);
7377
7496
  if (!brand) return;
@@ -7380,9 +7499,11 @@ function registerAssetBuildRoutes(app, deps) {
7380
7499
  if (!scene) return reply.status(404).send({ error: "scene not found" });
7381
7500
  const engine = await buildEngine();
7382
7501
  if (!engine) return reply.status(400).send({ error: "no engine here can draw a preview" });
7502
+ const refs = (scene.refs ?? []).map((r) => String(r?.file ?? "").replace(/^asset:/, "")).filter((h) => /^[a-f0-9]{32}$/.test(h) && core.images.has(h)).slice(0, engine.capabilities().maxReferenceImages).map((h) => core.images.pathFor(h));
7383
7503
  const request = {
7384
7504
  prompt: scenePreviewPrompt(scene),
7385
7505
  brand: brandContext(core, brand.id),
7506
+ ...refs.length ? { referenceImages: refs, referenceRoles: refs.map(() => "scene") } : {},
7386
7507
  width: scene.width,
7387
7508
  height: scene.height,
7388
7509
  count: 1
@@ -7823,6 +7944,39 @@ function registerImageRoutes(app, deps) {
7823
7944
 
7824
7945
  // src/release/notes.data.ts
7825
7946
  var RELEASES = [
7947
+ {
7948
+ version: "0.6.2",
7949
+ date: "2026-08-28",
7950
+ title: "Scenes keep what makes them distinctive.",
7951
+ sections: [
7952
+ {
7953
+ heading: "Scenes",
7954
+ body: "A scene built from references that are about a person now keeps that person as part of its art direction: where they sit in the composition, and what has been done to them, whether that is a face covered in stickers or a figure behind fabric. Before, a reference like that was read as an empty room and the idea was lost."
7955
+ },
7956
+ {
7957
+ heading: "Create",
7958
+ body: "Attach a presenter to a scene like that and they take the role, wearing their own face under the treatment. Attach nobody and the person stays anonymous. Ask for no people and the treatment stays on whatever is left in the frame."
7959
+ },
7960
+ {
7961
+ heading: "Building a scene",
7962
+ body: "The notes field is now Direction, and it decides how your references are read. Say what matters in them and what to ignore, and it carries more weight than the pictures alone. You can edit it later from the scene page."
7963
+ },
7964
+ {
7965
+ heading: "Fixes",
7966
+ body: "Reading a scene's references again no longer clears the direction you wrote. Scenes you build yourself now appear on the home shelf beside the built-in ones, and a scene that finishes while you are looking at the library no longer stays hidden until you reload."
7967
+ }
7968
+ ]
7969
+ },
7970
+ {
7971
+ version: "0.6.1",
7972
+ date: "2026-08-26",
7973
+ sections: [
7974
+ {
7975
+ heading: "Shots",
7976
+ body: "Refining a shot now opens at that picture's own shape. The aspect ratio belongs to the shot in front of you rather than to the last one you touched, so refining a portrait shot no longer quietly reframes it to whatever shape a different shot was set to, and two shots open side by side can each hold their own."
7977
+ }
7978
+ ]
7979
+ },
7826
7980
  {
7827
7981
  version: "0.6.0",
7828
7982
  date: "2026-08-26",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "scenri",
3
- "version": "0.6.0",
3
+ "version": "0.6.2",
4
4
  "author": "Tony Gorb <hello@scenri.co>",
5
5
  "license": "AGPL-3.0-only",
6
6
  "type": "module",