scenri 0.6.1 → 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 +8 -0
- package/dist/serve.js +172 -28
- package/package.json +1 -1
- package/studio-dist/assets/index-D4u0ePAn.js +103 -0
- package/studio-dist/index.html +1 -1
- package/studio-dist/assets/index-CYdC4kte.js +0 -103
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
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
|
+
|
|
3
11
|
## [0.6.1](https://github.com/tonygorb/Scenri/compare/v0.6.0...v0.6.1) (2026-08-26)
|
|
4
12
|
|
|
5
13
|
|
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
|
-
|
|
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 ? `
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
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
|
|
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
|
-
|
|
5928
|
-
|
|
5929
|
-
|
|
5930
|
-
|
|
5931
|
-
|
|
5932
|
-
|
|
5933
|
-
|
|
5934
|
-
|
|
5935
|
-
|
|
5936
|
-
|
|
5937
|
-
|
|
5938
|
-
|
|
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, {
|
|
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
|
-
|
|
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
|
-
|
|
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,29 @@ 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
|
+
},
|
|
7826
7970
|
{
|
|
7827
7971
|
version: "0.6.1",
|
|
7828
7972
|
date: "2026-08-26",
|