scenri 0.17.0 → 0.17.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 +17 -0
- package/dist/serve.js +101 -23
- package/package.json +1 -1
- package/studio-dist/assets/{index-te6uQiVY.js → index-DnGUXWUs.js} +24 -24
- package/studio-dist/index.html +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.17.2](https://github.com/tonygorb/Scenri/compare/v0.17.1...v0.17.2) (2026-09-24)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
* **composer:** a parked brief sends a person as they are now ([6c6273d](https://github.com/tonygorb/Scenri/commit/6c6273dd15f7f2e15b666e5affdd0494a876a3ad))
|
|
9
|
+
* **composer:** a parked brief sends a person as they are now ([b816b4b](https://github.com/tonygorb/Scenri/commit/b816b4b31179397212d0f79ddca3629cb00c14fd))
|
|
10
|
+
|
|
11
|
+
## [0.17.1](https://github.com/tonygorb/Scenri/compare/v0.17.0...v0.17.1) (2026-09-24)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
### Bug Fixes
|
|
15
|
+
|
|
16
|
+
* keep the presenter's own body, marks and accessories in every shot ([51ecbf0](https://github.com/tonygorb/Scenri/commit/51ecbf0744e81939e10e04559ffabeff583547e9))
|
|
17
|
+
* keep the presenter's own body, marks and accessories in every shot ([6b6af6a](https://github.com/tonygorb/Scenri/commit/6b6af6a72338871de74d0b24bbf2c0af3dc0cc15))
|
|
18
|
+
* mend a cut trait to its last full sentence, not a fragment ([3c31a67](https://github.com/tonygorb/Scenri/commit/3c31a6725c20818683772131ccfae2fd41168634))
|
|
19
|
+
|
|
3
20
|
## [0.17.0](https://github.com/tonygorb/Scenri/compare/v0.16.0...v0.17.0) (2026-09-24)
|
|
4
21
|
|
|
5
22
|
|
package/dist/serve.js
CHANGED
|
@@ -1355,7 +1355,7 @@ function presenterBody(req, refCount) {
|
|
|
1355
1355
|
const same = refCount > 1 ? " Every attached photograph is the same person." : "";
|
|
1356
1356
|
const ask = req.instruction ? ` The person who supplied these adds: ${req.instruction}.` : "";
|
|
1357
1357
|
const categories = req.vocabulary?.categories?.length ? ` Choose "suitableCategories" only from this list: ${req.vocabulary.categories.join(", ")}.` : "";
|
|
1358
|
-
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; "facial": bone structure in words, the face shape, jaw, cheekbones, eyes and brows a photographer would need to recognise them again; "skin": their skin tone and texture exactly as the photographs show it, never smoothed; "build": their body type and proportions where the photographs show them.` + (req.classifyPhotos ? photosClause(refCount) : "");
|
|
1358
|
+
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. Name every tattoo, scar, birthmark, piercing and anything they always wear, such as glasses, by what it is, exactly where it sits and which side, as the person's own left or right; "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; "facial": bone structure in words, the face shape, jaw, cheekbones, eyes and brows a photographer would need to recognise them again; "skin": their skin tone and texture exactly as the photographs show it, never smoothed; "build": their body type and proportions where the photographs show them.` + (req.classifyPhotos ? photosClause(refCount) : "");
|
|
1359
1359
|
}
|
|
1360
1360
|
function photosClause(refCount) {
|
|
1361
1361
|
return ` Also write "photos": an array with one entry per attached image in attachment order (ref-1.png is index 0, up to index ${Math.max(0, refCount - 1)}), each an object {"index", "view", "usable", "note"}, where "view" is exactly one of "portrait" (head and shoulders, the face large and facing the camera), "front" (full length, standing, facing the camera), "three-quarter" (turned about forty-five degrees, both eyes visible), "back" (full length, facing away from the camera), "left" (full length, the left side of the body to the camera), "right" (full length, the right side of the body to the camera), or "other"; "usable" is true only when the image is sharp, evenly lit, unobstructed, and shows this person clearly enough to stand in as that view; "note" is a few words on why. If the photographs appear to show more than one person, also write "conflict": one sentence saying which images disagree; otherwise leave "conflict" out.`;
|
|
@@ -1397,16 +1397,18 @@ function parsePresenter(req, o) {
|
|
|
1397
1397
|
presentation,
|
|
1398
1398
|
descriptor: cap(str(o.descriptor), 120),
|
|
1399
1399
|
ageRange: cap(str(o.ageRange), 40),
|
|
1400
|
-
|
|
1400
|
+
// Cut at a clause, never mid-word: every shot of a person is told these,
|
|
1401
|
+
// and a hard slice sent "full-body proporti." with each one.
|
|
1402
|
+
hair: trait(o.hair, 120),
|
|
1401
1403
|
identityNotes: cap(identityNotes, 900),
|
|
1402
1404
|
negativeConstraints: list(o.negativeConstraints, 6, 160),
|
|
1403
1405
|
suitableCategories: pick(o.suitableCategories, req.vocabulary?.categories, 6),
|
|
1404
1406
|
coverage: sentences(o.coverage, 2, 240),
|
|
1405
1407
|
// Non-blocking, like scene's `camera`: a model that omits or fumbles
|
|
1406
1408
|
// these must not burn the single retry that exists for a broken contract.
|
|
1407
|
-
...optional("facial",
|
|
1408
|
-
...optional("skin",
|
|
1409
|
-
...optional("build",
|
|
1409
|
+
...optional("facial", trait(o.facial, 300)),
|
|
1410
|
+
...optional("skin", trait(o.skin, 200)),
|
|
1411
|
+
...optional("build", trait(o.build, 200)),
|
|
1410
1412
|
...req.classifyPhotos ? optional("photos", photoFilings(o.photos, req.imagePaths.length)) : {},
|
|
1411
1413
|
...req.classifyPhotos ? optional("conflict", cap(str(o.conflict), 200)) : {}
|
|
1412
1414
|
}
|
|
@@ -1474,6 +1476,16 @@ function oneLine(v, max) {
|
|
|
1474
1476
|
return (/\s/.test(one[max] ?? "") ? cut : cut.replace(/\s+\S*$/, "")).replace(/[\s,;:-]+$/, "");
|
|
1475
1477
|
}
|
|
1476
1478
|
var cap = (v, max) => v.length > max ? v.slice(0, max).trim() : v;
|
|
1479
|
+
function trait(v, max) {
|
|
1480
|
+
const one = str(v).replace(/\s+/g, " ");
|
|
1481
|
+
if (one.length <= max) return one;
|
|
1482
|
+
const cut = one.slice(0, max);
|
|
1483
|
+
const sentence = cut.lastIndexOf(". ");
|
|
1484
|
+
if (sentence >= max * 0.6) return cut.slice(0, sentence + 1);
|
|
1485
|
+
const clause = Math.max(cut.lastIndexOf(", "), cut.lastIndexOf("; "));
|
|
1486
|
+
if (clause > max / 3) return cut.slice(0, clause).trim();
|
|
1487
|
+
return cut.replace(/\s+\S*$/, "").replace(/[\s,;:-]+$/, "");
|
|
1488
|
+
}
|
|
1477
1489
|
function sentences(v, max, each) {
|
|
1478
1490
|
if (!Array.isArray(v)) return [];
|
|
1479
1491
|
return v.map((x) => oneLine(x, each)).filter(Boolean).slice(0, max);
|
|
@@ -2274,6 +2286,7 @@ async function resolvePresenterImages(core, templatesRoot, presenter) {
|
|
|
2274
2286
|
...presenter.negativeConstraints?.length ? { negativeConstraints: presenter.negativeConstraints } : {},
|
|
2275
2287
|
...presenter.skin ? { skin: presenter.skin } : {},
|
|
2276
2288
|
...presenter.facial ? { facial: presenter.facial } : {},
|
|
2289
|
+
...presenter.hair ? { hair: presenter.hair } : {},
|
|
2277
2290
|
...presenter.build ? { build: presenter.build } : {},
|
|
2278
2291
|
shots
|
|
2279
2292
|
};
|
|
@@ -4469,11 +4482,11 @@ function presenterRecordFrom(input, base) {
|
|
|
4469
4482
|
if (source && PRESENTER_SOURCES.includes(source)) presenter.source = source;
|
|
4470
4483
|
const likeness = has("likeness") ? likenessOf(input.likeness) : base?.likeness;
|
|
4471
4484
|
if (likeness) presenter.likeness = likeness;
|
|
4472
|
-
const facial = has("facial") ?
|
|
4485
|
+
const facial = has("facial") ? phrase(input.facial, 300) : base?.facial;
|
|
4473
4486
|
if (facial) presenter.facial = facial;
|
|
4474
|
-
const skin = has("skin") ?
|
|
4487
|
+
const skin = has("skin") ? phrase(input.skin, 200) : base?.skin;
|
|
4475
4488
|
if (skin) presenter.skin = skin;
|
|
4476
|
-
const build = has("build") ?
|
|
4489
|
+
const build = has("build") ? phrase(input.build, 200) : base?.build;
|
|
4477
4490
|
if (build) presenter.build = build;
|
|
4478
4491
|
const edits = has("identityEdits") ? identityEditsOf(input.identityEdits) : base?.identityEdits;
|
|
4479
4492
|
if (edits?.length) presenter.identityEdits = edits;
|
|
@@ -4577,6 +4590,7 @@ var SEAT_TIER = {
|
|
|
4577
4590
|
composition: 1,
|
|
4578
4591
|
style: 2
|
|
4579
4592
|
};
|
|
4593
|
+
var PRESENTER_FLOOR = 2;
|
|
4580
4594
|
var ROLE_PRIORITY = {
|
|
4581
4595
|
product: 0,
|
|
4582
4596
|
character: 1,
|
|
@@ -4587,7 +4601,7 @@ var ROLE_PRIORITY = {
|
|
|
4587
4601
|
composition: 6,
|
|
4588
4602
|
style: 7
|
|
4589
4603
|
};
|
|
4590
|
-
function allocateAttachments(attachments, cap2) {
|
|
4604
|
+
function allocateAttachments(attachments, cap2, opts = {}) {
|
|
4591
4605
|
const max = Math.max(0, cap2);
|
|
4592
4606
|
const indexed = attachments.map((a, i) => ({ a, i }));
|
|
4593
4607
|
const legacyOrder = [...indexed].sort(
|
|
@@ -4595,12 +4609,22 @@ function allocateAttachments(attachments, cap2) {
|
|
|
4595
4609
|
);
|
|
4596
4610
|
const groupOf = (a) => `${a.role}:${a.id ?? a.hash}`;
|
|
4597
4611
|
const kept = /* @__PURE__ */ new Set();
|
|
4598
|
-
const keptGroups = /* @__PURE__ */ new
|
|
4612
|
+
const keptGroups = /* @__PURE__ */ new Map();
|
|
4599
4613
|
const admit = (x) => {
|
|
4600
4614
|
kept.add(x.i);
|
|
4601
|
-
keptGroups.
|
|
4615
|
+
keptGroups.set(groupOf(x.a), (keptGroups.get(groupOf(x.a)) ?? 0) + 1);
|
|
4602
4616
|
};
|
|
4603
4617
|
const seatOrder = [...indexed].sort((x, y) => SEAT_TIER[x.a.role] - SEAT_TIER[y.a.role] || x.i - y.i);
|
|
4618
|
+
if (opts.identityFloor) {
|
|
4619
|
+
for (const x of seatOrder) {
|
|
4620
|
+
if (kept.size >= max) break;
|
|
4621
|
+
if ((x.a.role === "product" || x.a.role === "character") && !keptGroups.has(groupOf(x.a))) admit(x);
|
|
4622
|
+
}
|
|
4623
|
+
for (const x of seatOrder) {
|
|
4624
|
+
if (kept.size >= max) break;
|
|
4625
|
+
if (x.a.role === "character" && !kept.has(x.i) && (keptGroups.get(groupOf(x.a)) ?? 0) < PRESENTER_FLOOR) admit(x);
|
|
4626
|
+
}
|
|
4627
|
+
}
|
|
4604
4628
|
for (const x of seatOrder) {
|
|
4605
4629
|
if (kept.size >= max) break;
|
|
4606
4630
|
if (!kept.has(x.i) && !keptGroups.has(groupOf(x.a))) admit(x);
|
|
@@ -4866,7 +4890,7 @@ async function pickBuildEngine(engines, opts = {}) {
|
|
|
4866
4890
|
// src/briefDirectives.ts
|
|
4867
4891
|
function productFidelityDirective(attached) {
|
|
4868
4892
|
if (attached <= 1) {
|
|
4869
|
-
return "The attached product image is the exact product: preserve its label, shape, colors and proportions faithfully, and do not redesign it. Where the image shows it more than once, side by side from the front and the back or at several angles, those are views of one product: this shot contains exactly one of it. It is
|
|
4893
|
+
return "The attached product image is the exact product: preserve its label, shape, colors and proportions faithfully, and do not redesign it. Where the image shows it more than once, side by side from the front and the back or at several angles, those are views of one product: this shot contains exactly one of it. It is the only view of this product in this shot. Any face, side or detail not visible in it is unknown \u2014 keep those plain and consistent with the visible materials and color, and do not invent hardware, text, seams, closures, ornament or branding on them. Prefer a composition that shows the product from the view the reference gives, unless this shot's own direction asks for another view.";
|
|
4870
4894
|
}
|
|
4871
4895
|
return "The attached product images all show the exact product to feature: preserve its label, shape and proportions faithfully, do not redesign it, and never treat an extra image as an additional product. The first product image is the authority for its color, finish and material. Where another image differs in color or finish, it shows the same product in another colorway \u2014 never blend colorways, and render the one the first image shows. Any face not visible in them is unknown \u2014 keep it plain and consistent with the materials the first image shows, and do not invent detail on it. If the direction above explicitly asks for more than one colorway, that explicit request wins.";
|
|
4872
4896
|
}
|
|
@@ -4909,7 +4933,12 @@ function productFactDirectives(p) {
|
|
|
4909
4933
|
}
|
|
4910
4934
|
function characterFactDirectives(c) {
|
|
4911
4935
|
const out = [];
|
|
4912
|
-
if (c.identityNotes)
|
|
4936
|
+
if (c.identityNotes) {
|
|
4937
|
+
const notes = String(c.identityNotes).trim().replace(/[\s,;:]+$/, "");
|
|
4938
|
+
out.push(
|
|
4939
|
+
`${c.promptName ?? c.name}'s own, in every shot and never capture context, as part of who they are: ${notes}${/[.!?]$/.test(notes) ? "" : "."}`
|
|
4940
|
+
);
|
|
4941
|
+
}
|
|
4913
4942
|
if (c.negativeConstraints?.length) out.push(`Avoid: ${[].concat(c.negativeConstraints).join(", ")}`);
|
|
4914
4943
|
return out;
|
|
4915
4944
|
}
|
|
@@ -4920,7 +4949,25 @@ function characterEditIdentityDirective(name) {
|
|
|
4920
4949
|
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.`;
|
|
4921
4950
|
}
|
|
4922
4951
|
function referenceIdentityGuard() {
|
|
4923
|
-
return "A reference shot lends its composition, lighting and treatment, never its cast: the attached presenter is the only source of person identity in this shot, and any person visible in a reference shot is a stand-in whose place the presenter takes. This holds even where the direction asks to use someone from a reference \u2014 the attached presenter is that someone.";
|
|
4952
|
+
return "A reference shot lends its composition, lighting and treatment, never its cast: the attached presenter is the only source of person identity in this shot, and any person visible in a reference shot is a stand-in whose place the presenter takes. The stand-in's clothes are not part of the pose: unless this shot's own words dress the presenter, dress them for this place in clothes of their own that show their own build, never in the stand-in's outfit. This holds even where the direction asks to use someone from a reference \u2014 the attached presenter is that someone.";
|
|
4953
|
+
}
|
|
4954
|
+
var HAIR_STYLED = /\b(hair|hairstyle|updo|bun|ponytail|braids?|braided|plaits?|fringe|bangs|slicked|cornrows)\b/i;
|
|
4955
|
+
function hairDirective(who, hair, userWords) {
|
|
4956
|
+
const h = hair.trim().replace(/[\s.,;:]+$/, "");
|
|
4957
|
+
return HAIR_STYLED.test(userWords) ? `${who}'s hair keeps its own colour, length and texture (${h}), styled the way this shot's words ask.` : `${who}'s hair, exactly as their references show it: ${h}.`;
|
|
4958
|
+
}
|
|
4959
|
+
function mendedTrait(text, cap2) {
|
|
4960
|
+
if (!text) return text;
|
|
4961
|
+
const t = text.trim();
|
|
4962
|
+
if (t.length < cap2 - 1 || /[.!?]$/.test(t)) return t;
|
|
4963
|
+
return wholeClauses(t);
|
|
4964
|
+
}
|
|
4965
|
+
function wholeClauses(t) {
|
|
4966
|
+
const sentence = t.lastIndexOf(". ");
|
|
4967
|
+
if (sentence >= t.length * 0.6) return t.slice(0, sentence + 1);
|
|
4968
|
+
const clause = Math.max(t.lastIndexOf(", "), t.lastIndexOf("; "));
|
|
4969
|
+
if (clause > t.length / 3) return `${t.slice(0, clause).replace(/[\s,;:]+$/, "")}.`;
|
|
4970
|
+
return sentence > 0 ? t.slice(0, sentence + 1) : t;
|
|
4924
4971
|
}
|
|
4925
4972
|
function markEditDirective() {
|
|
4926
4973
|
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. Every character it carries stays intact, including the smallest secondary lettering, in its original script and reading direction \u2014 never translated, transliterated or re-spelled.";
|
|
@@ -5269,15 +5316,20 @@ function compileBrief(brief, ctx) {
|
|
|
5269
5316
|
"Their expression and gaze in the reference are neutral capture conditions too, not a look to reproduce: give them the genuine expression this exact moment and place call for, the effort, delight, focus, calm or ease a real person would show doing exactly this, and let their eyes go where the direction and the moment put them, never a copy of the reference's straight-to-lens look, while their face stays unmistakably theirs."
|
|
5270
5317
|
);
|
|
5271
5318
|
personDirectives.push(...characterFactDirectives(c));
|
|
5272
|
-
|
|
5319
|
+
const skin = mendedTrait(c.skin, 200);
|
|
5320
|
+
const facial = mendedTrait(c.facial, 300);
|
|
5321
|
+
const build = mendedTrait(c.build, 200);
|
|
5322
|
+
const hair = mendedTrait(c.hair, 120);
|
|
5323
|
+
if (skin)
|
|
5273
5324
|
personDirectives.push(
|
|
5274
|
-
`${c.promptName ?? c.name}'s skin, exactly as the reference photographs show it: ${
|
|
5325
|
+
`${c.promptName ?? c.name}'s skin, exactly as the reference photographs show it: ${skin.replace(/[.\s]+$/, "")}.`
|
|
5275
5326
|
);
|
|
5276
|
-
if (
|
|
5327
|
+
if (facial)
|
|
5277
5328
|
personDirectives.push(
|
|
5278
|
-
`${c.promptName ?? c.name}'s face, which must survive every generation unchanged: ${
|
|
5329
|
+
`${c.promptName ?? c.name}'s face, which must survive every generation unchanged: ${facial.replace(/[.\s]+$/, "")}.`
|
|
5279
5330
|
);
|
|
5280
|
-
if (
|
|
5331
|
+
if (hair) personDirectives.push(hairDirective(c.promptName ?? c.name, hair, userWords));
|
|
5332
|
+
if (build) personDirectives.push(`${c.promptName ?? c.name}'s build: ${build.replace(/[.\s]+$/, "")}.`);
|
|
5281
5333
|
} else {
|
|
5282
5334
|
warnings.push(`${c.name} has no usable photo, so they are named but not attached.`);
|
|
5283
5335
|
unattachable.push({
|
|
@@ -5442,7 +5494,13 @@ function compileBrief(brief, ctx) {
|
|
|
5442
5494
|
}
|
|
5443
5495
|
}
|
|
5444
5496
|
const max = ctx.engineCaps.maxReferenceImages;
|
|
5445
|
-
const {
|
|
5497
|
+
const {
|
|
5498
|
+
kept,
|
|
5499
|
+
dropped: budgetDropped,
|
|
5500
|
+
seated
|
|
5501
|
+
} = allocateAttachments(attachments, max, {
|
|
5502
|
+
identityFloor: ctx.mode !== "edit"
|
|
5503
|
+
});
|
|
5446
5504
|
const presentKeys = new Set((ctx.presentAttachments ?? kept).map((a) => `${a.role}:${a.hash}`));
|
|
5447
5505
|
const resolveDirective = (d) => {
|
|
5448
5506
|
if (typeof d === "string") return d;
|
|
@@ -5461,7 +5519,7 @@ function compileBrief(brief, ctx) {
|
|
|
5461
5519
|
if (presentKeys.has(key) || absentSeen.has(key)) continue;
|
|
5462
5520
|
if (a.role === "reference") {
|
|
5463
5521
|
absentSeen.add(key);
|
|
5464
|
-
const words = ctx.wordsFor?.(a.hash) ?? null;
|
|
5522
|
+
const words = hasPerson ? null : ctx.wordsFor?.(a.hash) ?? null;
|
|
5465
5523
|
absentDirectives.push(
|
|
5466
5524
|
words ? `A reference shot was not attached this time; it showed ${words}. Match that composition, lighting and treatment.` : "A reference image was attached but not sent this time."
|
|
5467
5525
|
);
|
|
@@ -11361,8 +11419,8 @@ function traitOf(sentence) {
|
|
|
11361
11419
|
function mergeIdentityEdits(existing, adjustment) {
|
|
11362
11420
|
const next = str4(adjustment, IDENTITY_EDIT_CHARS).replace(/\s+/g, " ");
|
|
11363
11421
|
if (!next) return existing.slice(-IDENTITY_EDITS_MAX);
|
|
11364
|
-
const
|
|
11365
|
-
const kept = existing.filter((e) => e.toLowerCase() !== next.toLowerCase() && (!
|
|
11422
|
+
const trait2 = traitOf(next);
|
|
11423
|
+
const kept = existing.filter((e) => e.toLowerCase() !== next.toLowerCase() && (!trait2 || traitOf(e) !== trait2));
|
|
11366
11424
|
return [...kept, next].slice(-IDENTITY_EDITS_MAX);
|
|
11367
11425
|
}
|
|
11368
11426
|
async function filePhotos(deps, id, signal) {
|
|
@@ -13942,6 +14000,26 @@ function registerImageRoutes(app, deps) {
|
|
|
13942
14000
|
|
|
13943
14001
|
// src/release/notes.data.ts
|
|
13944
14002
|
var RELEASES = [
|
|
14003
|
+
{
|
|
14004
|
+
version: "0.17.2",
|
|
14005
|
+
date: "2026-09-24",
|
|
14006
|
+
sections: [
|
|
14007
|
+
{
|
|
14008
|
+
heading: "Presenters",
|
|
14009
|
+
body: "A brief left waiting in Create brings its presenters back as they are now, so a shot started after you edit someone uses their new pictures."
|
|
14010
|
+
}
|
|
14011
|
+
]
|
|
14012
|
+
},
|
|
14013
|
+
{
|
|
14014
|
+
version: "0.17.1",
|
|
14015
|
+
date: "2026-09-24",
|
|
14016
|
+
sections: [
|
|
14017
|
+
{
|
|
14018
|
+
heading: "Presenters",
|
|
14019
|
+
body: "A presenter keeps their own face, hair, build, glasses and tattoos when a scene or a reference picture shows someone else, with every mark on its own side. A picture used for a pose no longer dresses them in that person's clothes, and their descriptions are no longer cut off mid-word."
|
|
14020
|
+
}
|
|
14021
|
+
]
|
|
14022
|
+
},
|
|
13945
14023
|
{
|
|
13946
14024
|
version: "0.17.0",
|
|
13947
14025
|
date: "2026-09-24",
|