scenri 0.19.1 → 0.20.1
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 +77 -0
- package/README.md +1 -1
- package/dist/{chunk-4PJPJGGP.js → chunk-YNL3WQOL.js} +9 -4
- package/dist/{cli-6LLQ5INA.js → cli-WRNWT6P7.js} +14 -8
- package/dist/index.js +2 -2
- package/dist/serve.js +679 -253
- package/dist/{src-7RKZJC7Y.js → src-URYJIC5Y.js} +3 -3
- package/package.json +1 -1
- package/studio-dist/assets/0.16.0-local-access-D7XQonLd.webp +0 -0
- package/studio-dist/assets/0.17.0-select-several-DdAw3yQ3.webp +0 -0
- package/studio-dist/assets/0.17.1-presenter-identity-BfgRpajU.webp +0 -0
- package/studio-dist/assets/0.17.2-presenter-prompt-BiaRpVJN.webp +0 -0
- package/studio-dist/assets/0.18.0-portrait-standard-CrJ6Ixm2.webp +0 -0
- package/studio-dist/assets/0.19.0-home-examples-Dv7HjKk9.webp +0 -0
- package/studio-dist/assets/0.20.0-made-in-place-B0L_kS__.webp +0 -0
- package/studio-dist/assets/index-DluRyMMu.js +108 -0
- package/studio-dist/assets/{index-Cc29XMiU.css → index-rUsrfUXL.css} +1 -1
- package/studio-dist/assets/whatsnew-stage-DlyjaAn6.webp +0 -0
- package/studio-dist/index.html +2 -2
- package/studio-dist/assets/index-C8NkERt4.js +0 -108
package/dist/serve.js
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
import { createUpdateChecker, classify,
|
|
1
|
+
import { createUpdateChecker, classify, isReleaseTriplet, findNpm, stageVersion } from './chunk-YK37H6DM.js';
|
|
2
2
|
import { shouldAdoptRunning, anotherScenriLines, portBusyLines } from './chunk-CMLJGPYC.js';
|
|
3
|
-
import { SchemaTooNewError, createCore, BUDGET_EXHAUSTED, SpendCapError, EDIT_REFERENCE_ROLE_DIRECTIVE, REFERENCE_ROLE_DIRECTIVE, ratioLabel, searchTerms, nameMatches, budgetSize, SCHEMA_VERSION, ASPECT_TOLERANCE } from './chunk-
|
|
3
|
+
import { SchemaTooNewError, defaultHome, createCore, BUDGET_EXHAUSTED, SpendCapError, EDIT_REFERENCE_ROLE_DIRECTIVE, REFERENCE_ROLE_DIRECTIVE, ratioLabel, searchTerms, nameMatches, budgetSize, SCHEMA_VERSION, ASPECT_TOLERANCE } from './chunk-YNL3WQOL.js';
|
|
4
4
|
import { detectInstallKind } from './chunk-PAIAXRAC.js';
|
|
5
5
|
export { detectInstallKind } from './chunk-PAIAXRAC.js';
|
|
6
6
|
import { readMeta, repoSlug } from './chunk-Y3ZPBPLP.js';
|
|
7
7
|
import { compareSemver, newestStaged } from './chunk-4MAFHYAD.js';
|
|
8
8
|
import { powershellPath, PS_UTF8 } from './chunk-2IGINCPU.js';
|
|
9
|
-
import { dirname, join,
|
|
9
|
+
import { dirname, join, basename, normalize } from 'path';
|
|
10
10
|
import { fileURLToPath } from 'url';
|
|
11
|
-
import fs, { existsSync, readFileSync, mkdirSync, createReadStream, rmSync,
|
|
11
|
+
import fs, { existsSync, readdirSync, readFileSync, mkdirSync, createReadStream, rmSync, statSync, renameSync, lstatSync, unlinkSync } from 'fs';
|
|
12
12
|
import { homedir, tmpdir, networkInterfaces } from 'os';
|
|
13
13
|
import { readFile, stat, copyFile, lstat, readdir, mkdtemp, rm, access, rmdir, rename, unlink, writeFile } from 'fs/promises';
|
|
14
14
|
import { spawn, execFile } from 'child_process';
|
|
@@ -229,14 +229,24 @@ var DEFAULT_MODEL2 = "black-forest-labs/flux-schnell";
|
|
|
229
229
|
var DEFAULT_EDIT_MODEL = "black-forest-labs/flux-kontext-pro";
|
|
230
230
|
var DEFAULT_EXPAND_MODEL = "bria/expand-image";
|
|
231
231
|
var DEFAULT_POLL_INTERVAL_MS = 1500;
|
|
232
|
-
var DEFAULT_TIMEOUT_MS =
|
|
232
|
+
var DEFAULT_TIMEOUT_MS = 6e5;
|
|
233
|
+
var MIN_CANCEL_AFTER_S = 5;
|
|
234
|
+
var CANCEL_TIMEOUT_MS = 5e3;
|
|
233
235
|
var GENERATE_COST_PER_IMAGE_USD = 3e-3;
|
|
234
236
|
var EDIT_COST_USD = 0.04;
|
|
235
237
|
function nearestAspectRatio(width, height) {
|
|
236
238
|
const candidates = [
|
|
237
239
|
["1:1", 1],
|
|
238
240
|
["16:9", 16 / 9],
|
|
239
|
-
["9
|
|
241
|
+
["21:9", 21 / 9],
|
|
242
|
+
["3:2", 3 / 2],
|
|
243
|
+
["2:3", 2 / 3],
|
|
244
|
+
["4:5", 4 / 5],
|
|
245
|
+
["5:4", 5 / 4],
|
|
246
|
+
["3:4", 3 / 4],
|
|
247
|
+
["4:3", 4 / 3],
|
|
248
|
+
["9:16", 9 / 16],
|
|
249
|
+
["9:21", 9 / 21]
|
|
240
250
|
];
|
|
241
251
|
const ratio = width / height;
|
|
242
252
|
let best = candidates[0];
|
|
@@ -307,7 +317,12 @@ function createReplicateEngine(opts) {
|
|
|
307
317
|
headers: {
|
|
308
318
|
Authorization: `Bearer ${key}`,
|
|
309
319
|
"Content-Type": "application/json",
|
|
310
|
-
Prefer: "wait"
|
|
320
|
+
Prefer: "wait",
|
|
321
|
+
// Replicate's own copy of the ceiling, measured from creation, so a
|
|
322
|
+
// prediction nobody here can cancel any more (the app quit, or a Stop
|
|
323
|
+
// landed while this request was still open and no id had come back)
|
|
324
|
+
// still stops running, and billing, on its own.
|
|
325
|
+
"Cancel-After": `${Math.max(MIN_CANCEL_AFTER_S, Math.ceil(timeoutMs / 1e3))}s`
|
|
311
326
|
},
|
|
312
327
|
body: JSON.stringify({ input }),
|
|
313
328
|
signal
|
|
@@ -315,6 +330,18 @@ function createReplicateEngine(opts) {
|
|
|
315
330
|
if (!res.ok) throw await httpError(res, "prediction create");
|
|
316
331
|
return asPrediction(await res.json(), "prediction create");
|
|
317
332
|
}
|
|
333
|
+
async function cancelPrediction(key, prediction) {
|
|
334
|
+
const url = typeof prediction.urls?.cancel === "string" ? prediction.urls.cancel : typeof prediction.id === "string" ? `${API_BASE}/predictions/${prediction.id}/cancel` : null;
|
|
335
|
+
if (!url) return;
|
|
336
|
+
try {
|
|
337
|
+
await fetchImpl(url, {
|
|
338
|
+
method: "POST",
|
|
339
|
+
headers: { Authorization: `Bearer ${key}` },
|
|
340
|
+
signal: AbortSignal.timeout(CANCEL_TIMEOUT_MS)
|
|
341
|
+
});
|
|
342
|
+
} catch {
|
|
343
|
+
}
|
|
344
|
+
}
|
|
318
345
|
async function waitForCompletion(key, initial, signal) {
|
|
319
346
|
let prediction = initial;
|
|
320
347
|
if (prediction.status === "succeeded") return prediction;
|
|
@@ -327,26 +354,34 @@ function createReplicateEngine(opts) {
|
|
|
327
354
|
}
|
|
328
355
|
const getUrl = prediction.urls?.get;
|
|
329
356
|
if (typeof getUrl !== "string" || getUrl.length === 0) {
|
|
357
|
+
await cancelPrediction(key, prediction);
|
|
330
358
|
throw new Error(
|
|
331
359
|
`Replicate prediction did not succeed (status: ${prediction.status ?? "unknown"}) and no polling URL was provided`
|
|
332
360
|
);
|
|
333
361
|
}
|
|
334
362
|
const deadline = Date.now() + timeoutMs;
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
363
|
+
try {
|
|
364
|
+
for (; ; ) {
|
|
365
|
+
if (Date.now() >= deadline) {
|
|
366
|
+
throw new Error(`Replicate prediction timed out after ${timeoutMs}ms`);
|
|
367
|
+
}
|
|
368
|
+
await sleep(pollIntervalMs, signal);
|
|
369
|
+
const res = await fetchImpl(getUrl, {
|
|
370
|
+
headers: { Authorization: `Bearer ${key}` },
|
|
371
|
+
signal
|
|
372
|
+
});
|
|
373
|
+
if (!res.ok) throw await httpError(res, "prediction poll");
|
|
374
|
+
prediction = asPrediction(await res.json(), "prediction poll");
|
|
375
|
+
if (prediction.status === "succeeded") return prediction;
|
|
376
|
+
if (prediction.status === "failed" || prediction.status === "canceled") {
|
|
377
|
+
throw failureError(prediction);
|
|
378
|
+
}
|
|
338
379
|
}
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
signal
|
|
343
|
-
});
|
|
344
|
-
if (!res.ok) throw await httpError(res, "prediction poll");
|
|
345
|
-
prediction = asPrediction(await res.json(), "prediction poll");
|
|
346
|
-
if (prediction.status === "succeeded") return prediction;
|
|
347
|
-
if (prediction.status === "failed" || prediction.status === "canceled") {
|
|
348
|
-
throw failureError(prediction);
|
|
380
|
+
} catch (err) {
|
|
381
|
+
if (prediction.status !== "failed" && prediction.status !== "canceled") {
|
|
382
|
+
await cancelPrediction(key, initial);
|
|
349
383
|
}
|
|
384
|
+
throw err;
|
|
350
385
|
}
|
|
351
386
|
}
|
|
352
387
|
async function downloadOutputs(prediction, signal) {
|
|
@@ -387,8 +422,8 @@ function createReplicateEngine(opts) {
|
|
|
387
422
|
// bria/expand-image: the picture plus the canvas it belongs in.
|
|
388
423
|
supportsOutpaint: true,
|
|
389
424
|
// 0, deliberately — see the same note in the fal adapter. generate()
|
|
390
|
-
// sends only prompt
|
|
391
|
-
// of 1 was a promise this adapter never kept.
|
|
425
|
+
// sends no reference image, only its prompt and settings, so a
|
|
426
|
+
// declared capacity of 1 was a promise this adapter never kept.
|
|
392
427
|
maxReferenceImages: 0
|
|
393
428
|
};
|
|
394
429
|
},
|
|
@@ -409,7 +444,13 @@ function createReplicateEngine(opts) {
|
|
|
409
444
|
{
|
|
410
445
|
prompt: req.prompt,
|
|
411
446
|
num_outputs: req.count,
|
|
412
|
-
aspect_ratio: nearestAspectRatio(req.width, req.height)
|
|
447
|
+
aspect_ratio: nearestAspectRatio(req.width, req.height),
|
|
448
|
+
// Left alone, flux-schnell answers in webp at quality 80: a lossy
|
|
449
|
+
// frame the server then re-encodes to png anyway. A png arrives as
|
|
450
|
+
// delivered. The edit models need no such line: kontext-pro already
|
|
451
|
+
// answers in png, and bria's expander has no format field. The
|
|
452
|
+
// generate input assumes the flux-schnell schema, as aspect_ratio does.
|
|
453
|
+
output_format: "png"
|
|
413
454
|
},
|
|
414
455
|
signal
|
|
415
456
|
);
|
|
@@ -490,24 +531,41 @@ async function sourceImageToDataUri(source) {
|
|
|
490
531
|
const buf = await readFile(source);
|
|
491
532
|
return `data:${mimeForPath(source)};base64,${buf.toString("base64")}`;
|
|
492
533
|
}
|
|
493
|
-
function
|
|
534
|
+
function readReply(json) {
|
|
494
535
|
if (typeof json !== "object" || json === null) {
|
|
495
536
|
throw new Error("fal.ai response was not a JSON object");
|
|
496
537
|
}
|
|
497
|
-
const
|
|
538
|
+
const reply = json;
|
|
539
|
+
if (!Array.isArray(reply.images) && typeof reply.image === "object" && reply.image !== null) {
|
|
540
|
+
const url = reply.image.url;
|
|
541
|
+
if (typeof url !== "string" || url.length === 0) {
|
|
542
|
+
throw new Error('fal.ai response image missing "url"');
|
|
543
|
+
}
|
|
544
|
+
return { urls: [url], slots: [0] };
|
|
545
|
+
}
|
|
546
|
+
const images = reply.images;
|
|
498
547
|
if (!Array.isArray(images)) {
|
|
499
|
-
throw new Error('fal.ai response missing "images" array');
|
|
548
|
+
throw new Error('fal.ai response missing "images" array or "image" object');
|
|
500
549
|
}
|
|
501
550
|
if (images.length === 0) {
|
|
502
551
|
throw new Error("fal.ai response contained no images");
|
|
503
552
|
}
|
|
504
|
-
|
|
553
|
+
const urls = images.map((img, i) => {
|
|
505
554
|
const url = img?.url;
|
|
506
555
|
if (typeof url !== "string" || url.length === 0) {
|
|
507
556
|
throw new Error(`fal.ai response images[${i}] missing "url"`);
|
|
508
557
|
}
|
|
509
558
|
return url;
|
|
510
559
|
});
|
|
560
|
+
const flagged = Array.isArray(reply.has_nsfw_concepts) ? reply.has_nsfw_concepts : [];
|
|
561
|
+
const slots = urls.map((_, i) => i).filter((i) => flagged[i] !== true);
|
|
562
|
+
if (slots.length === 0) {
|
|
563
|
+
throw new Error("fal.ai flagged every image as unsafe and returned only black frames, so none were kept");
|
|
564
|
+
}
|
|
565
|
+
return { urls: slots.map((i) => urls[i]), slots };
|
|
566
|
+
}
|
|
567
|
+
function extractImageUrls(json) {
|
|
568
|
+
return readReply(json).urls;
|
|
511
569
|
}
|
|
512
570
|
function createFalEngine(opts) {
|
|
513
571
|
const fetchImpl = opts.fetchImpl ?? globalThis.fetch;
|
|
@@ -600,13 +658,31 @@ function createFalEngine(opts) {
|
|
|
600
658
|
{
|
|
601
659
|
prompt: req.prompt,
|
|
602
660
|
image_size: { width: req.width, height: req.height },
|
|
603
|
-
num_images: req.count
|
|
661
|
+
num_images: req.count,
|
|
662
|
+
// fal defaults to jpeg. png costs nothing extra, and a shot that
|
|
663
|
+
// may be refined or extended later should not start out lossy.
|
|
664
|
+
output_format: "png"
|
|
604
665
|
},
|
|
605
666
|
signal
|
|
606
667
|
);
|
|
607
|
-
const urls =
|
|
668
|
+
const { urls, slots } = readReply(json);
|
|
608
669
|
const images = await saveAll(urls, signal);
|
|
609
|
-
|
|
670
|
+
const costUsd = req.count * GENERATE_COST_PER_IMAGE_USD2;
|
|
671
|
+
if (slots.length === req.count) return { images, costUsd, raw: json };
|
|
672
|
+
const dropped = req.count - slots.length;
|
|
673
|
+
return {
|
|
674
|
+
images,
|
|
675
|
+
costUsd,
|
|
676
|
+
raw: {
|
|
677
|
+
...json,
|
|
678
|
+
requested: req.count,
|
|
679
|
+
variantIndexes: slots,
|
|
680
|
+
partialFailures: Array.from(
|
|
681
|
+
{ length: dropped },
|
|
682
|
+
() => "fal.ai flagged this image as unsafe and returned a black frame, so it was not kept"
|
|
683
|
+
)
|
|
684
|
+
}
|
|
685
|
+
};
|
|
610
686
|
},
|
|
611
687
|
async edit(req, signal) {
|
|
612
688
|
const key = requireKey();
|
|
@@ -631,7 +707,12 @@ function createFalEngine(opts) {
|
|
|
631
707
|
const images2 = await saveAll(urls2, signal);
|
|
632
708
|
return { images: images2, costUsd: EDIT_COST_USD2, raw: json2 };
|
|
633
709
|
}
|
|
634
|
-
const json = await postJson(
|
|
710
|
+
const json = await postJson(
|
|
711
|
+
editModel,
|
|
712
|
+
key,
|
|
713
|
+
{ prompt: req.instruction, image_url: imageUrl, output_format: "png" },
|
|
714
|
+
signal
|
|
715
|
+
);
|
|
635
716
|
const urls = extractImageUrls(json);
|
|
636
717
|
const images = await saveAll(urls, signal);
|
|
637
718
|
return { images, costUsd: EDIT_COST_USD2, raw: json };
|
|
@@ -948,6 +1029,7 @@ function createRunner(opts = {}) {
|
|
|
948
1029
|
async function run4(args, signal, io) {
|
|
949
1030
|
const exe = await resolution();
|
|
950
1031
|
const timeoutMs = io?.timeoutMs ?? defaultTimeoutMs;
|
|
1032
|
+
const launchedAs = args[0] === "exec" ? fingerprintFor(exe, knownVersion) : null;
|
|
951
1033
|
return new Promise((resolve, reject) => {
|
|
952
1034
|
let child;
|
|
953
1035
|
try {
|
|
@@ -1031,7 +1113,11 @@ function createRunner(opts = {}) {
|
|
|
1031
1113
|
});
|
|
1032
1114
|
child.on("exit", (code) => {
|
|
1033
1115
|
if (code === 0) {
|
|
1034
|
-
finish("ok",
|
|
1116
|
+
finish("ok", () => {
|
|
1117
|
+
if (launchedAs && resolved && launchedAs === fingerprintFor(resolved, knownVersion))
|
|
1118
|
+
noteConnection("proven");
|
|
1119
|
+
resolve();
|
|
1120
|
+
});
|
|
1035
1121
|
return;
|
|
1036
1122
|
}
|
|
1037
1123
|
const limit = /You've hit your usage limit\.(?:[^\n]*?\btry again at ([^.\n]+)\.)?/.exec(stderr);
|
|
@@ -1266,8 +1352,9 @@ function createRunner(opts = {}) {
|
|
|
1266
1352
|
if (outcome === "unproven") return;
|
|
1267
1353
|
const exe = resolved;
|
|
1268
1354
|
if (!exe) return;
|
|
1355
|
+
const wasRefused = connCache?.outcome === "refused";
|
|
1269
1356
|
connCache = { outcome, failure, at: Date.now(), fingerprint: fingerprintFor(exe, knownVersion) };
|
|
1270
|
-
cached = null;
|
|
1357
|
+
if (outcome === "refused" || wasRefused) cached = null;
|
|
1271
1358
|
}
|
|
1272
1359
|
function invalidateProbe() {
|
|
1273
1360
|
cached = null;
|
|
@@ -1357,7 +1444,7 @@ function createCodexAnalyzer(opts = {}) {
|
|
|
1357
1444
|
if (parsed.ok) return parsed.size;
|
|
1358
1445
|
problems = parsed.problems;
|
|
1359
1446
|
}
|
|
1360
|
-
|
|
1447
|
+
return null;
|
|
1361
1448
|
});
|
|
1362
1449
|
}
|
|
1363
1450
|
};
|
|
@@ -2040,6 +2127,7 @@ function demoOptionsFromEnv(env) {
|
|
|
2040
2127
|
if (env.SCENRI_DEMO_FAIL_EDIT === "1") out.failEdit = true;
|
|
2041
2128
|
const refs = Number(env.SCENRI_DEMO_REFS);
|
|
2042
2129
|
if (env.SCENRI_DEMO_REFS && Number.isInteger(refs) && refs > 0) out.maxReferenceImages = refs;
|
|
2130
|
+
if (env.SCENRI_DEMO_PHOTOS) out.photosDir = env.SCENRI_DEMO_PHOTOS;
|
|
2043
2131
|
return out;
|
|
2044
2132
|
}
|
|
2045
2133
|
function sleep2(ms, signal) {
|
|
@@ -2065,7 +2153,21 @@ function createDemoEngine(saveImage, opts = {}) {
|
|
|
2065
2153
|
return hexes.length ? hexes : ["#4B5563", "#9CA3AF"];
|
|
2066
2154
|
};
|
|
2067
2155
|
const esc2 = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").slice(0, 90);
|
|
2156
|
+
let calls = 0;
|
|
2157
|
+
const photos = (() => {
|
|
2158
|
+
if (!opts.photosDir) return [];
|
|
2159
|
+
try {
|
|
2160
|
+
return readdirSync(opts.photosDir).filter((f) => /\.(jpe?g|png|webp)$/i.test(f)).map((f) => join(opts.photosDir, f));
|
|
2161
|
+
} catch {
|
|
2162
|
+
return [];
|
|
2163
|
+
}
|
|
2164
|
+
})();
|
|
2068
2165
|
async function render(colors, label, w, h, seed) {
|
|
2166
|
+
if (photos.length) {
|
|
2167
|
+
calls += 1;
|
|
2168
|
+
const file = photos[(seed * 7919 + calls * 104729) % photos.length];
|
|
2169
|
+
return sharp22(file).resize(w, h, { fit: "cover" }).png().toBuffer();
|
|
2170
|
+
}
|
|
2069
2171
|
const c0 = colors[seed % colors.length];
|
|
2070
2172
|
const c1 = colors[(seed + 1) % colors.length] ?? "#111111";
|
|
2071
2173
|
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${h}">
|
|
@@ -4479,8 +4581,10 @@ function mergeScrape(existing, scraped) {
|
|
|
4479
4581
|
}
|
|
4480
4582
|
|
|
4481
4583
|
// src/assetRecords.ts
|
|
4584
|
+
var TYPED_TEXT_MAX = 4e3;
|
|
4585
|
+
var COMPOSED_TEXT_MAX = 8e3;
|
|
4482
4586
|
var IDENTITY_EDITS_MAX = 8;
|
|
4483
|
-
var IDENTITY_EDIT_CHARS =
|
|
4587
|
+
var IDENTITY_EDIT_CHARS = TYPED_TEXT_MAX;
|
|
4484
4588
|
var PRESENTER_SOURCES = ["synthetic", "photos"];
|
|
4485
4589
|
var LIKENESS_VERSION = "v1";
|
|
4486
4590
|
var PRESENTER_ID_PREFIX = "up";
|
|
@@ -4550,7 +4654,7 @@ function identityEditsOf(v) {
|
|
|
4550
4654
|
return out;
|
|
4551
4655
|
}
|
|
4552
4656
|
var SUBJECTS2 = /* @__PURE__ */ new Set(["product", "person", "either"]);
|
|
4553
|
-
var SCENE_INSTRUCTION_MAX =
|
|
4657
|
+
var SCENE_INSTRUCTION_MAX = COMPOSED_TEXT_MAX;
|
|
4554
4658
|
function sceneRecordFrom(input, base) {
|
|
4555
4659
|
const has = (k) => input[k] !== void 0;
|
|
4556
4660
|
const prompt = has("prompt") ? phrase(input.prompt, 2e3) : base?.prompt ?? "";
|
|
@@ -5003,21 +5107,31 @@ var toMarkPng = async (buf) => {
|
|
|
5003
5107
|
}
|
|
5004
5108
|
return out;
|
|
5005
5109
|
};
|
|
5110
|
+
var SENDABLE_FORMATS = /* @__PURE__ */ new Set(["png", "jpeg", "webp", "gif"]);
|
|
5006
5111
|
var cappedRefs = /* @__PURE__ */ new Map();
|
|
5007
5112
|
async function capReferenceEdge(core, path, maxEdge) {
|
|
5008
5113
|
const key = `${path}#${maxEdge}`;
|
|
5009
5114
|
const hit = cappedRefs.get(key);
|
|
5010
5115
|
if (hit && existsSync(hit)) return hit;
|
|
5011
|
-
let
|
|
5116
|
+
let format;
|
|
5117
|
+
let buf = null;
|
|
5012
5118
|
try {
|
|
5013
5119
|
const meta = await sharp22(path).metadata();
|
|
5014
|
-
|
|
5015
|
-
|
|
5016
|
-
|
|
5120
|
+
format = meta.format;
|
|
5121
|
+
const oversized = (meta.width ?? 0) > maxEdge || (meta.height ?? 0) > maxEdge;
|
|
5122
|
+
if (oversized || !SENDABLE_FORMATS.has(String(format))) {
|
|
5123
|
+
buf = await sharp22(path).resize({ width: maxEdge, height: maxEdge, fit: "inside", withoutEnlargement: true }).png().toBuffer();
|
|
5017
5124
|
}
|
|
5018
5125
|
} catch {
|
|
5019
|
-
return path;
|
|
5126
|
+
if (!existsSync(path) || SENDABLE_FORMATS.has(String(format))) return path;
|
|
5127
|
+
throw Object.assign(
|
|
5128
|
+
new Error(
|
|
5129
|
+
"One of the reference pictures is in a format that cannot be read. Save it as a JPEG or PNG and add it again."
|
|
5130
|
+
),
|
|
5131
|
+
{ statusCode: 400 }
|
|
5132
|
+
);
|
|
5020
5133
|
}
|
|
5134
|
+
const out = buf ? core.images.pathFor(core.images.save(buf)) : path;
|
|
5021
5135
|
cappedRefs.set(key, out);
|
|
5022
5136
|
return out;
|
|
5023
5137
|
}
|
|
@@ -5290,8 +5404,11 @@ function productEditFidelityDirective(name) {
|
|
|
5290
5404
|
function characterEditIdentityDirective(name) {
|
|
5291
5405
|
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 capture uniform of their reference photographs (${CAPTURE_UNIFORM}) or to all-white or all-cream plain basics like it.`;
|
|
5292
5406
|
}
|
|
5293
|
-
function referenceIdentityGuard() {
|
|
5294
|
-
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."
|
|
5407
|
+
function referenceIdentityGuard(people = 1) {
|
|
5408
|
+
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. Not a recoloured copy of it either: each garment and the shoes are a different kind from the stand-in's, in none of the stand-in's colours. This holds even where the direction asks to use someone from a reference \u2014 the attached presenter is that someone. " + // Nothing said who else may be in the frame, and read word for word a café
|
|
5409
|
+
// doorway filled with customers, a barista and people on the street in 3
|
|
5410
|
+
// of 4 (2026-09-27). People come from presenters, never from a place.
|
|
5411
|
+
(people > 1 ? "The attached presenters are the only people" : "The presenter is the only person") + " in this image unless this shot's own words ask for others: no customers, staff or passers-by, and nobody glimpsed through glass or in the distance.";
|
|
5295
5412
|
}
|
|
5296
5413
|
function referenceProductGuard() {
|
|
5297
5414
|
return "A reference shot lends its composition, lighting and treatment, never its product: the attached product photo is the only source of product identity in this shot. Any product, package or object a reference shot features in that place only marks where the attached product goes, and the attached product takes it at its own real size, with its own shape, label and colours.";
|
|
@@ -5869,6 +5986,22 @@ function compileBrief(input, ctx) {
|
|
|
5869
5986
|
);
|
|
5870
5987
|
}
|
|
5871
5988
|
}
|
|
5989
|
+
const sent = new Set(ctx.sourceHash ? [ctx.sourceHash] : []);
|
|
5990
|
+
const identitySent = /* @__PURE__ */ new Set();
|
|
5991
|
+
const copies = /* @__PURE__ */ new Set();
|
|
5992
|
+
for (const a of attachments) {
|
|
5993
|
+
if (a.role !== "product" && a.role !== "character") continue;
|
|
5994
|
+
const own = `${a.role}:${a.id}:${a.hash}`;
|
|
5995
|
+
if (identitySent.has(own)) copies.add(a);
|
|
5996
|
+
identitySent.add(own);
|
|
5997
|
+
sent.add(a.hash);
|
|
5998
|
+
}
|
|
5999
|
+
for (const a of attachments) {
|
|
6000
|
+
if (a.role === "product" || a.role === "character") continue;
|
|
6001
|
+
if (sent.has(a.hash)) copies.add(a);
|
|
6002
|
+
sent.add(a.hash);
|
|
6003
|
+
}
|
|
6004
|
+
for (let i = attachments.length - 1; i >= 0; i--) if (copies.has(attachments[i])) attachments.splice(i, 1);
|
|
5872
6005
|
const max = ctx.engineCaps.maxReferenceImages;
|
|
5873
6006
|
const {
|
|
5874
6007
|
kept,
|
|
@@ -5961,7 +6094,7 @@ function compileBrief(input, ctx) {
|
|
|
5961
6094
|
const rec = (ctx.brand?.products ?? []).find((x) => x?.id === a.id);
|
|
5962
6095
|
return String(rec?.category ?? "").toLowerCase() === "apparel";
|
|
5963
6096
|
}) ? [garmentDisplayDirective()] : [];
|
|
5964
|
-
const refGuard = ctx.mode !== "edit" && hasPerson && kept.some((a) => a.role === "reference") ? [referenceIdentityGuard()] : [];
|
|
6097
|
+
const refGuard = ctx.mode !== "edit" && hasPerson && kept.some((a) => a.role === "reference") ? [referenceIdentityGuard(people)] : [];
|
|
5965
6098
|
const framed = ctx.mode !== "edit" && kept.some((a) => a.role === "reference" && frameRefs.has(a.hash));
|
|
5966
6099
|
const refProductGuard = ctx.mode !== "edit" && productId && kept.some((a) => a.role === "reference") ? [referenceProductGuard()] : [];
|
|
5967
6100
|
const allDirectives = [
|
|
@@ -6129,6 +6262,9 @@ function createContentFetcher(deps) {
|
|
|
6129
6262
|
const expected = deps.sha256 !== void 0 ? deps.sha256 : custom ? null : CONTENT_SHA256;
|
|
6130
6263
|
const enabled = () => env.SCENRI_NO_CONTENT_FETCH !== "1" && deps.store.getSetting("content.enabled") !== "false";
|
|
6131
6264
|
let inflight = null;
|
|
6265
|
+
let gaveUp = false;
|
|
6266
|
+
let installs = 0;
|
|
6267
|
+
let stale = null;
|
|
6132
6268
|
async function download() {
|
|
6133
6269
|
const root = contentCacheRoot(env);
|
|
6134
6270
|
if (!deps.store.getSetting("content.disclosed")) {
|
|
@@ -6170,15 +6306,25 @@ function createContentFetcher(deps) {
|
|
|
6170
6306
|
async function ensure() {
|
|
6171
6307
|
if (!enabled()) return { ok: false, updated: false, error: null };
|
|
6172
6308
|
if (!contentCacheStale(env, custom)) return { ok: true, updated: false, error: null };
|
|
6173
|
-
inflight ??= download().
|
|
6309
|
+
inflight ??= download().then((result) => {
|
|
6310
|
+
if (result.updated) installs += 1;
|
|
6311
|
+
else if (!result.ok) gaveUp = true;
|
|
6312
|
+
return result;
|
|
6313
|
+
}).finally(() => {
|
|
6174
6314
|
inflight = null;
|
|
6315
|
+
stale = null;
|
|
6175
6316
|
});
|
|
6176
6317
|
return inflight;
|
|
6177
6318
|
}
|
|
6319
|
+
function state() {
|
|
6320
|
+
if (!enabled() || gaveUp) return { arriving: false, installs };
|
|
6321
|
+
stale ??= contentCacheStale(env, custom);
|
|
6322
|
+
return { arriving: stale, installs };
|
|
6323
|
+
}
|
|
6178
6324
|
function schedule() {
|
|
6179
6325
|
setTimeout(() => void ensure(), 3e3).unref();
|
|
6180
6326
|
}
|
|
6181
|
-
return { enabled, ensure, schedule };
|
|
6327
|
+
return { enabled, ensure, schedule, state };
|
|
6182
6328
|
}
|
|
6183
6329
|
async function inspectMark(buf, toPng2) {
|
|
6184
6330
|
const png = await toPng2(buf);
|
|
@@ -7670,7 +7816,8 @@ async function discoverCatalog(opts) {
|
|
|
7670
7816
|
const estimatedTotal = discovered.estimatedTotal ?? discovered.productUrls.length;
|
|
7671
7817
|
let empty = null;
|
|
7672
7818
|
if (!estimatedTotal) {
|
|
7673
|
-
|
|
7819
|
+
const unreachable = await neverAnswered(baseUrl, fetchImpl, opts.signal);
|
|
7820
|
+
empty = unreachable ? { code: "unreachable", message: unreachable, retryable: true } : {
|
|
7674
7821
|
code: "empty_catalog",
|
|
7675
7822
|
message: detection.platform === "generic" ? "No public product catalog found. This store may be JavaScript-rendered or require authentication." : `No products discovered on this ${detection.platform} store.`
|
|
7676
7823
|
};
|
|
@@ -7702,6 +7849,17 @@ async function discoverCatalog(opts) {
|
|
|
7702
7849
|
}
|
|
7703
7850
|
};
|
|
7704
7851
|
}
|
|
7852
|
+
async function neverAnswered(baseUrl, fetchImpl, signal) {
|
|
7853
|
+
try {
|
|
7854
|
+
const res = await httpGet(baseUrl, { fetchImpl, signal, retries: 1, timeoutMs: 15e3 });
|
|
7855
|
+
await res.body?.cancel().catch(() => {
|
|
7856
|
+
});
|
|
7857
|
+
return null;
|
|
7858
|
+
} catch {
|
|
7859
|
+
if (signal?.aborted) return null;
|
|
7860
|
+
return `Could not reach ${new URL(baseUrl).hostname}.`;
|
|
7861
|
+
}
|
|
7862
|
+
}
|
|
7705
7863
|
|
|
7706
7864
|
// ../catalog/src/robots.ts
|
|
7707
7865
|
var ALLOW_ALL = { rules: [], crawlDelayMs: 0 };
|
|
@@ -8000,6 +8158,16 @@ async function runJob(deps, jobId, brandId, url, signal, only) {
|
|
|
8000
8158
|
platform = found.detection.platform;
|
|
8001
8159
|
discovered = found.estimatedTotal;
|
|
8002
8160
|
warnings = found.progress.warnings;
|
|
8161
|
+
if (found.empty?.code === "unreachable") {
|
|
8162
|
+
patch3({
|
|
8163
|
+
stage: "failed",
|
|
8164
|
+
errors: [found.empty],
|
|
8165
|
+
warnings: found.progress.warnings,
|
|
8166
|
+
message: found.empty.message,
|
|
8167
|
+
finished: true
|
|
8168
|
+
});
|
|
8169
|
+
return;
|
|
8170
|
+
}
|
|
8003
8171
|
if (found.empty || !found.estimatedTotal) {
|
|
8004
8172
|
const source = core.catalog.upsertSource(brandId, found.baseUrl, platform);
|
|
8005
8173
|
patch3({ sourceId: source.id, platform });
|
|
@@ -9830,13 +9998,17 @@ function resolveSize(dimensions, stored2) {
|
|
|
9830
9998
|
}
|
|
9831
9999
|
async function drawAtScale(o) {
|
|
9832
10000
|
const span = spanCm(o.size.largestCm);
|
|
10001
|
+
const edge = o.engine.capabilities().maxReferenceEdge;
|
|
10002
|
+
const [scenePath, productPath] = await Promise.all(
|
|
10003
|
+
[o.plan.sceneHash, o.plan.productHash].map((h) => o.images.pathFor(h)).map((p) => edge ? capReferenceEdge({ images: o.images }, p, edge) : p)
|
|
10004
|
+
);
|
|
9833
10005
|
const plates = {
|
|
9834
10006
|
prompt: platePrompt({ span, light: o.plan.light, shot: o.plan.shot }),
|
|
9835
10007
|
brand: o.brand,
|
|
9836
10008
|
width: o.width,
|
|
9837
10009
|
height: o.height,
|
|
9838
10010
|
count: o.count,
|
|
9839
|
-
referenceImages: [
|
|
10011
|
+
referenceImages: [scenePath],
|
|
9840
10012
|
referenceRoles: ["scene"]
|
|
9841
10013
|
};
|
|
9842
10014
|
const instruction = placeInstruction({
|
|
@@ -9876,7 +10048,7 @@ async function drawAtScale(o) {
|
|
|
9876
10048
|
instruction,
|
|
9877
10049
|
sourceImage: o.images.pathFor(plate),
|
|
9878
10050
|
brand: o.brand,
|
|
9879
|
-
referenceImages: [
|
|
10051
|
+
referenceImages: [productPath],
|
|
9880
10052
|
referenceRoles: ["product"],
|
|
9881
10053
|
width: o.width,
|
|
9882
10054
|
height: o.height
|
|
@@ -9918,17 +10090,28 @@ async function drawAtScale(o) {
|
|
|
9918
10090
|
raw: { requested: o.count, variantIndexes: slots, partialFailures: reasons }
|
|
9919
10091
|
};
|
|
9920
10092
|
}
|
|
9921
|
-
|
|
9922
|
-
|
|
9923
|
-
function createProductSizes(core, reader) {
|
|
10093
|
+
var MISS_MS = 24 * 60 * 60 * 1e3;
|
|
10094
|
+
var missKey = (brandId, productId) => `product-size-miss:${brandId}:${productId}`;
|
|
10095
|
+
function createProductSizes(core, reader, clock = Date.now) {
|
|
9924
10096
|
const reading = /* @__PURE__ */ new Map();
|
|
9925
10097
|
const stored2 = (brandId, id) => readSize(core.store.getSetting(sizeKey(brandId, id)));
|
|
9926
10098
|
const known = (brandId, p) => resolveSize(p.dimensions, stored2(brandId, p.id));
|
|
10099
|
+
const missed = (brandId, id, photo) => {
|
|
10100
|
+
try {
|
|
10101
|
+
const miss = JSON.parse(core.store.getSetting(missKey(brandId, id)) || "null");
|
|
10102
|
+
const age = clock() - Number(miss?.at);
|
|
10103
|
+
return miss?.photo === photo && age >= 0 && age < MISS_MS;
|
|
10104
|
+
} catch {
|
|
10105
|
+
return false;
|
|
10106
|
+
}
|
|
10107
|
+
};
|
|
9927
10108
|
return {
|
|
9928
10109
|
known,
|
|
9929
10110
|
async ensure(brandId, p, signal) {
|
|
9930
10111
|
const now2 = known(brandId, p);
|
|
9931
10112
|
if (now2 || !p.photo) return now2;
|
|
10113
|
+
const name = basename(p.photo);
|
|
10114
|
+
if (missed(brandId, p.id, name)) return null;
|
|
9932
10115
|
const key = sizeKey(brandId, p.id);
|
|
9933
10116
|
let read2 = reading.get(key);
|
|
9934
10117
|
if (!read2) {
|
|
@@ -9936,13 +10119,21 @@ function createProductSizes(core, reader) {
|
|
|
9936
10119
|
read2 = (async () => {
|
|
9937
10120
|
const analyzer = await reader();
|
|
9938
10121
|
if (!analyzer?.measure) return null;
|
|
9939
|
-
|
|
9940
|
-
|
|
9941
|
-
|
|
9942
|
-
|
|
10122
|
+
let got = null;
|
|
10123
|
+
try {
|
|
10124
|
+
got = await analyzer.measure(
|
|
10125
|
+
{ imagePath: photo, name: p.name, ...p.description ? { description: p.description } : {} },
|
|
10126
|
+
signal
|
|
10127
|
+
);
|
|
10128
|
+
} catch {
|
|
10129
|
+
return null;
|
|
10130
|
+
}
|
|
9943
10131
|
const n = Number(got?.largestCm);
|
|
9944
10132
|
const text = String(got?.text ?? "").trim();
|
|
9945
|
-
if (!text || !Number.isFinite(n) || n <= 0)
|
|
10133
|
+
if (!text || !Number.isFinite(n) || n <= 0) {
|
|
10134
|
+
core.store.setSetting(missKey(brandId, p.id), JSON.stringify({ photo: name, at: clock() }));
|
|
10135
|
+
return null;
|
|
10136
|
+
}
|
|
9946
10137
|
const size = { text, largestCm: n, by: "estimate" };
|
|
9947
10138
|
core.store.setSetting(key, JSON.stringify(size));
|
|
9948
10139
|
return size;
|
|
@@ -10935,12 +11126,13 @@ function registerLogoRoutes(app, deps) {
|
|
|
10935
11126
|
return allowed.includes(v) ? v : null;
|
|
10936
11127
|
};
|
|
10937
11128
|
app.post("/api/brands/:id/logos", async (req, reply) => {
|
|
11129
|
+
if (!core.store.getBrand(req.params.id)) return reply.status(404).send({ error: "brand not found" });
|
|
11130
|
+
const part = await readImagePart(core, req, toMarkPng);
|
|
11131
|
+
if ("error" in part) return reply.status(400).send({ error: part.error });
|
|
10938
11132
|
const brand = core.store.getBrand(req.params.id);
|
|
10939
11133
|
if (!brand) return reply.status(404).send({ error: "brand not found" });
|
|
10940
11134
|
const json = { ...brand.json };
|
|
10941
11135
|
const logos = [...readLogos(json)];
|
|
10942
|
-
const part = await readImagePart(core, req, toMarkPng);
|
|
10943
|
-
if ("error" in part) return reply.status(400).send({ error: part.error });
|
|
10944
11136
|
const existing = logos.findIndex((l) => assetHash(l?.file) === part.hash);
|
|
10945
11137
|
const role = enumField(
|
|
10946
11138
|
part.fields?.role?.value,
|
|
@@ -11643,8 +11835,8 @@ function sweepPresenterDrafts(core) {
|
|
|
11643
11835
|
}
|
|
11644
11836
|
return swept;
|
|
11645
11837
|
}
|
|
11646
|
-
var KEEP_ITEM_CHARS =
|
|
11647
|
-
var SETUP_CHARS =
|
|
11838
|
+
var KEEP_ITEM_CHARS = TYPED_TEXT_MAX;
|
|
11839
|
+
var SETUP_CHARS = 64 * TYPED_TEXT_MAX;
|
|
11648
11840
|
var KEEP_ITEMS_MAX = 12;
|
|
11649
11841
|
var SOURCES_MAX = 8;
|
|
11650
11842
|
function keepItemsOf(raw, has) {
|
|
@@ -11686,18 +11878,18 @@ async function createPresenterDraft(deps, input) {
|
|
|
11686
11878
|
const brand = core.store.getBrand(input.brandId);
|
|
11687
11879
|
if (!brand) throw fail("brand not found", 404);
|
|
11688
11880
|
const source = input.source === "synthetic" ? "synthetic" : "photos";
|
|
11689
|
-
const direction = str4(input.direction,
|
|
11881
|
+
const direction = str4(input.direction, COMPOSED_TEXT_MAX);
|
|
11690
11882
|
const keep = str4(input.keep, 240);
|
|
11691
11883
|
const keepItems = keepItemsOf(input.keepItems, (h) => core.images.has(h));
|
|
11692
11884
|
const sources = [
|
|
11693
11885
|
...new Set((input.imageHashes ?? []).map(String).filter((h) => HASH.test(h) && core.images.has(h)))
|
|
11694
11886
|
].slice(0, SOURCES_MAX);
|
|
11695
11887
|
if (source === "synthetic" && !direction) throw fail("describe who they are in a sentence", 400);
|
|
11696
|
-
if (source === "synthetic" && !deps.engine) throw fail("no engine here can draw a person", 400);
|
|
11697
11888
|
if (source === "photos" && !sources.length) throw fail("add at least one photo of this person", 400);
|
|
11698
11889
|
if (source === "photos" && !input.attestation) {
|
|
11699
11890
|
throw fail("confirm you have permission to use this person's likeness", 400);
|
|
11700
11891
|
}
|
|
11892
|
+
if (!deps.engine) throw fail("no engine here can draw a person", 400);
|
|
11701
11893
|
if (source === "photos" && closing) throw restarting();
|
|
11702
11894
|
const clientKey = str4(input.clientKey, 80);
|
|
11703
11895
|
const made = clientKey ? listPresenterDrafts(core, brand.id).find(
|
|
@@ -11965,7 +12157,7 @@ async function generateView(deps, id, view, opts = {}) {
|
|
|
11965
12157
|
if (closing) throw restarting();
|
|
11966
12158
|
if (isExtra(view) && !rec.extras) throw fail("extra views are built on request", 400);
|
|
11967
12159
|
if (rec.source === "synthetic" && !rec.direction?.trim()) {
|
|
11968
|
-
const said = str4(opts.adjustment,
|
|
12160
|
+
const said = str4(opts.adjustment, TYPED_TEXT_MAX);
|
|
11969
12161
|
if (!said) throw fail("describe who they are in a sentence", 400);
|
|
11970
12162
|
rec = mutate(core, id, (r) => {
|
|
11971
12163
|
r.direction = said;
|
|
@@ -11977,7 +12169,7 @@ async function generateView(deps, id, view, opts = {}) {
|
|
|
11977
12169
|
if (rec.views[dep].status !== "approved") throw fail(`approve the ${VIEW_LABEL[dep]} first`, 400);
|
|
11978
12170
|
}
|
|
11979
12171
|
if (!engine || !engine.capabilities().maxReferenceImages) throw fail("no engine here can draw a person", 400);
|
|
11980
|
-
const adjustment = str4(opts.adjustment,
|
|
12172
|
+
const adjustment = str4(opts.adjustment, TYPED_TEXT_MAX) || void 0;
|
|
11981
12173
|
const decide = opts.decide === "auto" ? "auto" : void 0;
|
|
11982
12174
|
const before = rec.views[view].status;
|
|
11983
12175
|
const saved = mutate(core, id, (r) => {
|
|
@@ -12315,35 +12507,17 @@ async function redoView(deps, id, view) {
|
|
|
12315
12507
|
r.decisions = [...r.decisions, { view, what: "again", at: (/* @__PURE__ */ new Date()).toISOString() }].slice(-RESULTS_MAX);
|
|
12316
12508
|
});
|
|
12317
12509
|
}
|
|
12318
|
-
async function usePhotoForView(deps, id, view, hash) {
|
|
12319
|
-
if (!isView(view)) throw fail("no such view", 400);
|
|
12320
|
-
if (running3.has(id)) throw fail("a view is still being drawn", 409);
|
|
12321
|
-
const rec = getPresenterDraft(deps.core, id);
|
|
12322
|
-
if (!rec) throw fail("draft not found", 404);
|
|
12323
|
-
if (rec.source !== "photos" || !rec.sources.includes(hash)) throw fail("that is not one of their photos", 400);
|
|
12324
|
-
return mutate(deps.core, id, (r) => {
|
|
12325
|
-
const slot = r.views[view];
|
|
12326
|
-
if (slot.hash === hash && slot.origin === "photo") return;
|
|
12327
|
-
const gone = [slot.hash, slot.prior].filter((h) => !!h && !r.sources.includes(h));
|
|
12328
|
-
r.views[view] = {
|
|
12329
|
-
...emptySlot(),
|
|
12330
|
-
attempts: slot.attempts,
|
|
12331
|
-
rejected: [...slot.rejected, ...gone],
|
|
12332
|
-
status: "approved",
|
|
12333
|
-
hash,
|
|
12334
|
-
origin: "photo"
|
|
12335
|
-
};
|
|
12336
|
-
staleDependents(r, view);
|
|
12337
|
-
});
|
|
12338
|
-
}
|
|
12339
12510
|
async function updatePresenterDraft(core, id, patch3) {
|
|
12340
12511
|
return mutate(core, id, (r) => {
|
|
12341
12512
|
if (patch3.extras !== void 0) r.extras = patch3.extras === true;
|
|
12342
12513
|
if (patch3.name !== void 0) r.name = str4(patch3.name, 60);
|
|
12343
12514
|
if (Array.isArray(patch3.facets))
|
|
12344
12515
|
r.facets = patch3.facets.map((f) => str4(f, 40)).filter(Boolean).slice(0, 8);
|
|
12345
|
-
if (patch3.direction !== void 0) r.direction = str4(patch3.direction,
|
|
12346
|
-
if (patch3.setup !== void 0)
|
|
12516
|
+
if (patch3.direction !== void 0) r.direction = str4(patch3.direction, COMPOSED_TEXT_MAX) || void 0;
|
|
12517
|
+
if (patch3.setup !== void 0) {
|
|
12518
|
+
const setup = str4(patch3.setup, Number.POSITIVE_INFINITY);
|
|
12519
|
+
if (setup.length <= SETUP_CHARS) r.setup = setup || void 0;
|
|
12520
|
+
}
|
|
12347
12521
|
const items = keepItemsOf(patch3.keepItems, (h) => core.images.has(h));
|
|
12348
12522
|
if (items) writeItems(r, items);
|
|
12349
12523
|
else {
|
|
@@ -12376,19 +12550,15 @@ async function saveNew(deps, rec, hooks) {
|
|
|
12376
12550
|
const { core } = deps;
|
|
12377
12551
|
const id = rec.id;
|
|
12378
12552
|
if (!rec.name.trim()) throw fail("give them a name", 400);
|
|
12379
|
-
const
|
|
12380
|
-
(v) => v !== "portrait" && rec.views[v].origin === "generated" && !!rec.views[v].hash
|
|
12381
|
-
);
|
|
12382
|
-
const blind = !deps.engine && !drawnSet;
|
|
12383
|
-
const required = blind ? ["portrait"] : [...CORE_VIEWS, ...EXTRA_VIEWS.filter((v) => rec.views[v].status !== "empty")];
|
|
12553
|
+
const required = [...CORE_VIEWS, ...EXTRA_VIEWS.filter((v) => rec.views[v].status !== "empty")];
|
|
12384
12554
|
for (const v of required) {
|
|
12385
12555
|
const s = rec.views[v];
|
|
12386
12556
|
if (s.status === "stale") throw fail(`redo the ${VIEW_LABEL[v]}: it was built on a view you changed`, 400);
|
|
12387
12557
|
if (s.status !== "approved" || !s.hash) throw fail(`approve the ${VIEW_LABEL[v]} first`, 400);
|
|
12558
|
+
if (rec.sources.includes(s.hash)) throw fail(`draw the ${VIEW_LABEL[v]} first`, 400);
|
|
12388
12559
|
}
|
|
12389
|
-
const
|
|
12390
|
-
const
|
|
12391
|
-
const angles = blind ? ["portrait"] : [...required];
|
|
12560
|
+
const shots = required.map((v) => rec.views[v].hash);
|
|
12561
|
+
const angles = [...required];
|
|
12392
12562
|
const portraitFile = `asset:${shots[0]}`;
|
|
12393
12563
|
const sourceFiles = rec.sources.map((h) => `asset:${h}`);
|
|
12394
12564
|
const mode = presenterCropMode(portraitFile, sourceFiles, "portrait");
|
|
@@ -12777,12 +12947,16 @@ function createSceneExamples(deps) {
|
|
|
12777
12947
|
const engineId = engine.capabilities().id;
|
|
12778
12948
|
const brand = deps.brandContext(brandId);
|
|
12779
12949
|
const path = (h) => deps.core.images.pathFor(h);
|
|
12950
|
+
const edge = engine.capabilities().maxReferenceEdge;
|
|
12780
12951
|
return async (source, instruction, refs, role) => {
|
|
12952
|
+
const referenceImages = await Promise.all(
|
|
12953
|
+
refs.map(path).map((p) => edge ? capReferenceEdge(deps.core, p, edge) : p)
|
|
12954
|
+
);
|
|
12781
12955
|
const req = {
|
|
12782
12956
|
instruction,
|
|
12783
12957
|
sourceImage: path(source),
|
|
12784
12958
|
brand,
|
|
12785
|
-
referenceImages
|
|
12959
|
+
referenceImages,
|
|
12786
12960
|
referenceRoles: refs.map((_, i) => Array.isArray(role) ? role[i] : role),
|
|
12787
12961
|
width: scene.width,
|
|
12788
12962
|
height: scene.height
|
|
@@ -13161,7 +13335,7 @@ function createSceneExamples(deps) {
|
|
|
13161
13335
|
// src/sceneStudio.ts
|
|
13162
13336
|
var SHOT_READ = `This is one of their own product shots. Read only the place in it: the surface, the architecture, the materials, the light, the atmosphere and the layout. The product and any person in it are that shot's cast, not the place: leave both out and leave "figure" empty, and never transcribe its text or logos. If the shot shows too little of its place to build a world from, say so in "coverage" and describe only what is visible`;
|
|
13163
13337
|
var STUDIO_PICTURES_MAX = 4;
|
|
13164
|
-
var ASK_MAX =
|
|
13338
|
+
var ASK_MAX = TYPED_TEXT_MAX;
|
|
13165
13339
|
var KEEP_PER_BRAND2 = 24;
|
|
13166
13340
|
var KEEP_MS2 = 24 * 60 * 60 * 1e3;
|
|
13167
13341
|
var jobs = /* @__PURE__ */ new Map();
|
|
@@ -13169,6 +13343,17 @@ var running4 = /* @__PURE__ */ new Map();
|
|
|
13169
13343
|
var tasks2 = /* @__PURE__ */ new Map();
|
|
13170
13344
|
var readHolds = /* @__PURE__ */ new Map();
|
|
13171
13345
|
var READ_HOLDS_KEPT = 500;
|
|
13346
|
+
var holdsKey = (hash) => `scene-holds:${hash}`;
|
|
13347
|
+
function heldIn(deps, hash) {
|
|
13348
|
+
const known = readHolds.get(hash);
|
|
13349
|
+
if (known) return known;
|
|
13350
|
+
try {
|
|
13351
|
+
const kept = deps.core.store.getSetting(holdsKey(hash));
|
|
13352
|
+
return kept ? holdsFrom(JSON.parse(kept)) : void 0;
|
|
13353
|
+
} catch {
|
|
13354
|
+
return void 0;
|
|
13355
|
+
}
|
|
13356
|
+
}
|
|
13172
13357
|
var fail2 = (message, statusCode = 400) => Object.assign(new Error(message), { statusCode });
|
|
13173
13358
|
var oneLine2 = (v, max) => String(v ?? "").replace(/\s+/g, " ").trim().slice(0, max);
|
|
13174
13359
|
function nameFromWords(words) {
|
|
@@ -13355,7 +13540,9 @@ function startSceneStudioJob(deps, input) {
|
|
|
13355
13540
|
const checked = readingFrom(input.reading);
|
|
13356
13541
|
if (!checked.ok) throw fail2(checked.error);
|
|
13357
13542
|
reading = checked.reading;
|
|
13358
|
-
if (reading.holds?.length === 0 && !hashes.every((h) =>
|
|
13543
|
+
if (reading.holds?.length === 0 && !hashes.every((h) => heldIn(deps, h)?.length === 0)) delete reading.holds;
|
|
13544
|
+
else if (reading.holds === void 0 && hashes.length && hashes.every((h) => heldIn(deps, h)?.length === 0))
|
|
13545
|
+
reading.holds = [];
|
|
13359
13546
|
}
|
|
13360
13547
|
const ask = oneLine2(input.ask, ASK_MAX);
|
|
13361
13548
|
if (kind === "make") {
|
|
@@ -13431,7 +13618,13 @@ async function read(deps, job, input, signal) {
|
|
|
13431
13618
|
const read2 = readingOfDraft(draft, fallback);
|
|
13432
13619
|
const holds = imagePaths.length ? holdsFrom(draft.holds) : prior?.holds;
|
|
13433
13620
|
if (imagePaths.length && holds) {
|
|
13434
|
-
for (const h of hashes)
|
|
13621
|
+
for (const h of hashes) {
|
|
13622
|
+
readHolds.set(h, holds);
|
|
13623
|
+
try {
|
|
13624
|
+
deps.core.store.setSetting(holdsKey(h), JSON.stringify(holds));
|
|
13625
|
+
} catch {
|
|
13626
|
+
}
|
|
13627
|
+
}
|
|
13435
13628
|
while (readHolds.size > READ_HOLDS_KEPT) readHolds.delete(readHolds.keys().next().value);
|
|
13436
13629
|
}
|
|
13437
13630
|
const hero = read2.hero ?? prior?.hero;
|
|
@@ -14100,15 +14293,6 @@ function registerPresenterDraftRoutes(app, deps) {
|
|
|
14100
14293
|
const hash = String(req.body?.hash ?? "");
|
|
14101
14294
|
return answer(reply, async () => restoreView(await buildDeps(), draft.id, viewOf(req), hash));
|
|
14102
14295
|
});
|
|
14103
|
-
app.post("/api/brands/:id/presenter-drafts/:draftId/views/:view/use-photo", async (req, reply) => {
|
|
14104
|
-
const draft = draftOr404(req, reply);
|
|
14105
|
-
if (!draft) return;
|
|
14106
|
-
const body = req.body ?? {};
|
|
14107
|
-
return answer(
|
|
14108
|
-
reply,
|
|
14109
|
-
async () => usePhotoForView(await buildDeps(), draft.id, viewOf(req), String(body.hash ?? ""))
|
|
14110
|
-
);
|
|
14111
|
-
});
|
|
14112
14296
|
app.post("/api/brands/:id/presenter-drafts/:draftId/save", async (req, reply) => {
|
|
14113
14297
|
const draft = draftOr404(req, reply);
|
|
14114
14298
|
if (!draft) return;
|
|
@@ -14487,7 +14671,11 @@ function registerProjectRoutes(app, deps) {
|
|
|
14487
14671
|
nodes: core.store.recentActivity(brand.id, limit),
|
|
14488
14672
|
jobs: core.catalog.listRecentJobs(brand.id),
|
|
14489
14673
|
studio: listStudioWork(core, brand.id, deps.sceneExampleJobs?.(brand.id) ?? []),
|
|
14490
|
-
boot: BOOT
|
|
14674
|
+
boot: BOOT,
|
|
14675
|
+
// Machine-scoped like `boot`, and read on the same tick: whether library
|
|
14676
|
+
// pictures are still arriving, so the studio shows them as on their way
|
|
14677
|
+
// rather than missing, and reads its catalogs again once they land.
|
|
14678
|
+
content: app.content?.state()
|
|
14491
14679
|
};
|
|
14492
14680
|
});
|
|
14493
14681
|
app.get("/api/brands/:id/workspace", async (req, reply) => {
|
|
@@ -14891,18 +15079,60 @@ function registerImageRoutes(app, deps) {
|
|
|
14891
15079
|
}
|
|
14892
15080
|
|
|
14893
15081
|
// src/release/notes.data.ts
|
|
15082
|
+
var HEADLINES_KEPT = 5;
|
|
15083
|
+
var FIRST_PUBLIC = "0.2.0";
|
|
14894
15084
|
var RELEASES = [
|
|
15085
|
+
{
|
|
15086
|
+
version: "0.20.1",
|
|
15087
|
+
date: "2026-09-27",
|
|
15088
|
+
title: "Use in a shot starts a new prompt with just that pick",
|
|
15089
|
+
sections: [
|
|
15090
|
+
{
|
|
15091
|
+
heading: "Create",
|
|
15092
|
+
body: "From Products, Presenters or Scenes, Use in a shot opens a new prompt holding only what you picked. If you were writing one, a note offers Undo to bring it back."
|
|
15093
|
+
},
|
|
15094
|
+
{
|
|
15095
|
+
heading: "Fixes",
|
|
15096
|
+
body: "An example you leave and come back to keeps its shape and quality, even if its scene view was still loading. A pick sent to Create while it is open joins your prompt instead of replacing it."
|
|
15097
|
+
}
|
|
15098
|
+
]
|
|
15099
|
+
},
|
|
15100
|
+
{
|
|
15101
|
+
version: "0.20.0",
|
|
15102
|
+
date: "2026-09-27",
|
|
15103
|
+
title: "Pictures develop in place, and What's new has its own page",
|
|
15104
|
+
announce: true,
|
|
15105
|
+
sections: [
|
|
15106
|
+
{
|
|
15107
|
+
heading: "Create",
|
|
15108
|
+
body: "While a picture is being made, a quiet swirl holds its place, and the picture develops out of it when it is ready. Refine a shot and the new refinement opens at once, swirling until it lands.",
|
|
15109
|
+
image: {
|
|
15110
|
+
file: "0.20.0-made-in-place.webp",
|
|
15111
|
+
alt: "Two shots from one prompt in Create: the first has landed, and the second is still being made, a swirl with its clock."
|
|
15112
|
+
}
|
|
15113
|
+
},
|
|
15114
|
+
{
|
|
15115
|
+
heading: "What's new",
|
|
15116
|
+
body: "Every update is on one page now, newest first: open What's new from Help, or from Settings > Updates. After a big update, a short window introduces it once."
|
|
15117
|
+
},
|
|
15118
|
+
{
|
|
15119
|
+
heading: "Fixes",
|
|
15120
|
+
body: "Try again starts one new picture, never two, and presenter and scene chats take long descriptions. Beside a reference picture, your presenter keeps their own clothes and no strangers join the shot."
|
|
15121
|
+
}
|
|
15122
|
+
]
|
|
15123
|
+
},
|
|
14895
15124
|
{
|
|
14896
15125
|
version: "0.19.1",
|
|
14897
15126
|
date: "2026-09-26",
|
|
15127
|
+
title: "Codex keeps no copy of your pictures",
|
|
14898
15128
|
sections: [
|
|
14899
15129
|
{
|
|
14900
|
-
heading: "
|
|
15130
|
+
heading: "Privacy",
|
|
14901
15131
|
body: "Codex keeps no copy of a picture once Scenri has it, neither a saved conversation nor a second file, and its plugins stay off while Scenri runs it."
|
|
14902
15132
|
},
|
|
14903
15133
|
{
|
|
14904
15134
|
heading: "Library",
|
|
14905
|
-
body: "The library download is checked
|
|
15135
|
+
body: "The library download is checked before anything is installed, and one that does not match what this version expects is never used."
|
|
14906
15136
|
},
|
|
14907
15137
|
{
|
|
14908
15138
|
heading: "Fixes",
|
|
@@ -14913,29 +15143,31 @@ var RELEASES = [
|
|
|
14913
15143
|
{
|
|
14914
15144
|
version: "0.19.0",
|
|
14915
15145
|
date: "2026-09-26",
|
|
14916
|
-
title: "A new
|
|
15146
|
+
title: "A new library of products, presenters and scenes",
|
|
15147
|
+
announce: true,
|
|
14917
15148
|
sections: [
|
|
14918
15149
|
{
|
|
14919
15150
|
heading: "Library",
|
|
14920
|
-
body: "
|
|
15151
|
+
body: "The library now holds 30 products, 19 presenters and 42 scenes, with 110 examples on Home. Cards stay sharp at every size they are shown.",
|
|
15152
|
+
image: {
|
|
15153
|
+
file: "0.19.0-home-examples.webp",
|
|
15154
|
+
alt: "Home in a full-size window: the row of example kinds under the top bar, and the wall of examples made with the new library."
|
|
15155
|
+
}
|
|
14921
15156
|
},
|
|
14922
15157
|
{
|
|
14923
15158
|
heading: "Scenes",
|
|
14924
|
-
body: "
|
|
15159
|
+
body: "Scene cards show each place in use, not empty. Any picture of a scene can be the frame a shot follows: press Use this view on the scene's page, or pick it from the scene's chip in your prompt."
|
|
14925
15160
|
},
|
|
14926
15161
|
{
|
|
14927
15162
|
heading: "Create",
|
|
14928
|
-
body: "
|
|
14929
|
-
},
|
|
14930
|
-
{
|
|
14931
|
-
heading: "Fixes",
|
|
14932
|
-
body: "Presenter and scene conversations recover from a failed press, a retry or a restart without losing their place. A brand kit rename made in a second window stays, Tab leaves a brief that holds a chip, a shot sent right after switching brands is filed in the brand on screen, and a deleted presenter's revisions stay out of a brand export."
|
|
15163
|
+
body: "Presenters are dressed for the place, and lean or sit only on things that could hold them. A scene's close-up moves in on the product, keeping the person only where they wear it."
|
|
14933
15164
|
}
|
|
14934
15165
|
]
|
|
14935
15166
|
},
|
|
14936
15167
|
{
|
|
14937
15168
|
version: "0.18.2",
|
|
14938
15169
|
date: "2026-09-26",
|
|
15170
|
+
title: "The first library download finishes on a slow connection",
|
|
14939
15171
|
sections: [
|
|
14940
15172
|
{
|
|
14941
15173
|
heading: "Fixes",
|
|
@@ -14946,119 +15178,142 @@ var RELEASES = [
|
|
|
14946
15178
|
{
|
|
14947
15179
|
version: "0.18.1",
|
|
14948
15180
|
date: "2026-09-26",
|
|
15181
|
+
title: "Pictures made with Codex use far less of your ChatGPT plan",
|
|
15182
|
+
announce: true,
|
|
14949
15183
|
sections: [
|
|
14950
15184
|
{
|
|
14951
15185
|
heading: "Codex",
|
|
14952
|
-
body: "
|
|
15186
|
+
body: "Each picture is drawn once, and the finished picture is no longer pasted back into Codex's own conversation. It needs Codex CLI 0.157.1 or newer, and Scenri says when yours is too old."
|
|
14953
15187
|
},
|
|
14954
15188
|
{
|
|
14955
|
-
heading: "
|
|
14956
|
-
body: "A phone with the
|
|
15189
|
+
heading: "Local access",
|
|
15190
|
+
body: "A phone or tablet signed in with the code can no longer delete the library, change provider keys or spend caps, install an update or shut Scenri down. Only the computer running Scenri can."
|
|
14957
15191
|
},
|
|
14958
15192
|
{
|
|
14959
15193
|
heading: "Fixes",
|
|
14960
|
-
body: "A library download that stalls now
|
|
15194
|
+
body: "A library download that stalls now stops, and the library you already have keeps working."
|
|
14961
15195
|
}
|
|
14962
15196
|
]
|
|
14963
15197
|
},
|
|
14964
15198
|
{
|
|
14965
15199
|
version: "0.18.0",
|
|
14966
15200
|
date: "2026-09-25",
|
|
15201
|
+
title: "New shots start in portrait at Standard quality",
|
|
14967
15202
|
sections: [
|
|
14968
15203
|
{
|
|
14969
15204
|
heading: "Create",
|
|
14970
|
-
body: "A new shot
|
|
15205
|
+
body: "A new shot starts in portrait, with one picture at Standard quality. A setting you already saved still wins.",
|
|
15206
|
+
image: {
|
|
15207
|
+
file: "0.18.0-portrait-standard.webp",
|
|
15208
|
+
alt: "The settings a new shot starts with, Portrait, one picture and Standard, with the shape list open on Portrait."
|
|
15209
|
+
}
|
|
14971
15210
|
},
|
|
14972
15211
|
{
|
|
14973
15212
|
heading: "Library",
|
|
14974
|
-
body: "
|
|
15213
|
+
body: "Keep a product, presenter or scene with the star on its own page, yours or Scenri's. A product's size shows as a chip and changes in Details."
|
|
14975
15214
|
},
|
|
14976
15215
|
{
|
|
14977
15216
|
heading: "Shots",
|
|
14978
|
-
body: "
|
|
14979
|
-
},
|
|
14980
|
-
{
|
|
14981
|
-
heading: "Activity",
|
|
14982
|
-
body: "An empty Activity panel is the heading and one sentence."
|
|
15217
|
+
body: "On a shot, Download comes first, then Archive or Delete, then the Keepers star."
|
|
14983
15218
|
}
|
|
14984
15219
|
]
|
|
14985
15220
|
},
|
|
14986
15221
|
{
|
|
14987
15222
|
version: "0.17.2",
|
|
14988
15223
|
date: "2026-09-24",
|
|
15224
|
+
title: "A waiting prompt uses your presenters as they are now",
|
|
14989
15225
|
sections: [
|
|
14990
15226
|
{
|
|
14991
15227
|
heading: "Presenters",
|
|
14992
|
-
body: "A
|
|
15228
|
+
body: "A prompt left waiting in Create uses each presenter as they are now, so a shot started after you edit someone gets their new pictures.",
|
|
15229
|
+
image: {
|
|
15230
|
+
file: "0.17.2-presenter-prompt.webp",
|
|
15231
|
+
alt: "A waiting prompt, Amara and Kwame by a window, naming two of your presenters as chips with their faces."
|
|
15232
|
+
}
|
|
14993
15233
|
}
|
|
14994
15234
|
]
|
|
14995
15235
|
},
|
|
14996
15236
|
{
|
|
14997
15237
|
version: "0.17.1",
|
|
14998
15238
|
date: "2026-09-24",
|
|
15239
|
+
title: "Presenters keep their own face, hair and build",
|
|
14999
15240
|
sections: [
|
|
15000
15241
|
{
|
|
15001
15242
|
heading: "Presenters",
|
|
15002
|
-
body: "A presenter keeps their own face, hair, build, glasses and tattoos when a scene or
|
|
15243
|
+
body: "A presenter keeps their own face, hair, build, glasses and tattoos when a scene or reference picture shows someone else. A picture used for a pose no longer dresses them in that person's clothes.",
|
|
15244
|
+
image: {
|
|
15245
|
+
file: "0.17.1-presenter-identity.webp",
|
|
15246
|
+
alt: "Kwame, one of your presenters, as his page opens: his face, what he is filed under, and his hair and build."
|
|
15247
|
+
}
|
|
15003
15248
|
}
|
|
15004
15249
|
]
|
|
15005
15250
|
},
|
|
15006
15251
|
{
|
|
15007
15252
|
version: "0.17.0",
|
|
15008
15253
|
date: "2026-09-24",
|
|
15254
|
+
title: "Select several of your own products, presenters or scenes",
|
|
15255
|
+
announce: true,
|
|
15009
15256
|
sections: [
|
|
15010
15257
|
{
|
|
15011
15258
|
heading: "Library",
|
|
15012
|
-
body: "
|
|
15259
|
+
body: "Select your own products, presenters or scenes from their cards, then keep or delete them together from the bar at the bottom of the page. Rename one from its menu. Bookmarks are now called Keepers.",
|
|
15260
|
+
image: {
|
|
15261
|
+
file: "0.17.0-select-several.webp",
|
|
15262
|
+
alt: "Five of your scenes in a row, three of them picked, and the bar that keeps or deletes them together."
|
|
15263
|
+
}
|
|
15013
15264
|
},
|
|
15014
15265
|
{
|
|
15015
|
-
heading: "
|
|
15016
|
-
body: "Home, Create and
|
|
15266
|
+
heading: "Tabs",
|
|
15267
|
+
body: "Home, Create, Products, Presenters and Scenes share one tab row, with search and size at its end. On a phone, Create keeps its tabs readable and puts the set and the sort in one button beside search."
|
|
15017
15268
|
},
|
|
15018
15269
|
{
|
|
15019
15270
|
heading: "Fixes",
|
|
15020
|
-
body: "Tab counts on Create no longer jump while the
|
|
15271
|
+
body: "Tab counts on Create no longer jump while it loads, a tab chosen while scrolled down starts at the top, and closing and opening the Assets panel keeps the shot you were on in place."
|
|
15021
15272
|
}
|
|
15022
15273
|
]
|
|
15023
15274
|
},
|
|
15024
15275
|
{
|
|
15025
15276
|
version: "0.16.0",
|
|
15026
15277
|
date: "2026-09-24",
|
|
15027
|
-
title: "Open Scenri on your phone or tablet
|
|
15278
|
+
title: "Open Scenri on your phone or tablet",
|
|
15279
|
+
announce: true,
|
|
15028
15280
|
sections: [
|
|
15029
15281
|
{
|
|
15030
15282
|
heading: "Local access",
|
|
15031
|
-
body: "Scenri
|
|
15283
|
+
body: "Open Scenri on a phone, tablet or another computer on the same Wi-Fi: scan the QR code in Settings > Local access. If your computer's firewall stops it, Local access says so and offers Allow Scenri.",
|
|
15284
|
+
image: {
|
|
15285
|
+
file: "0.16.0-local-access.webp",
|
|
15286
|
+
alt: "The Local access card: a QR code to scan, the address and six-digit code to type, and an iPhone just connected."
|
|
15287
|
+
}
|
|
15032
15288
|
},
|
|
15033
15289
|
{
|
|
15034
15290
|
heading: "Security",
|
|
15035
|
-
body: "Only devices with the code get in, and wrong guesses
|
|
15291
|
+
body: "Only devices with the six-digit code get in, and wrong guesses have to wait. Press New code to sign every device out."
|
|
15036
15292
|
},
|
|
15037
15293
|
{
|
|
15038
15294
|
heading: "Settings",
|
|
15039
|
-
body: "Providers, Appearance, Library, Local access, Updates and About
|
|
15295
|
+
body: "One page for each thing: Providers, Appearance, Library, Local access, Updates and About, with every delete under Danger zone."
|
|
15040
15296
|
}
|
|
15041
|
-
]
|
|
15297
|
+
],
|
|
15298
|
+
newFeatures: ["local-access"]
|
|
15042
15299
|
},
|
|
15043
15300
|
{
|
|
15044
15301
|
version: "0.15.1",
|
|
15045
15302
|
date: "2026-09-23",
|
|
15303
|
+
title: "Make a scene by describing it, or by choosing from pictures",
|
|
15304
|
+
announce: true,
|
|
15046
15305
|
sections: [
|
|
15047
15306
|
{
|
|
15048
15307
|
heading: "Scenes",
|
|
15049
|
-
body: "
|
|
15050
|
-
},
|
|
15051
|
-
{
|
|
15052
|
-
heading: "Shots",
|
|
15053
|
-
body: "A small product alone in a scene is drawn at its real size. Glossy products such as phones and laptops are lit and staged like a campaign shoot, with the screen dark unless you ask for something on it. An attached picture now means what the words beside it say, so a screenshot can go on a device's screen, redesigned for that screen when it was made for another shape, and a refine can swap it for a different one."
|
|
15308
|
+
body: "Describe a place, start from your own pictures or a shot you already made, or press Guide me and choose a world, its surface and its light from pictures. Your pictures set the mood and are never copied."
|
|
15054
15309
|
},
|
|
15055
15310
|
{
|
|
15056
15311
|
heading: "Activity",
|
|
15057
|
-
body: "Scene and presenter work
|
|
15312
|
+
body: "Scene and presenter work keeps going when you leave the page, and shows in the bell and in Activity. A scene still being made stays on the Scenes wall."
|
|
15058
15313
|
},
|
|
15059
15314
|
{
|
|
15060
|
-
heading: "
|
|
15061
|
-
body: "
|
|
15315
|
+
heading: "Shots",
|
|
15316
|
+
body: "A small product alone in a scene is drawn at its real size. Phones, laptops and other glossy products are lit like a campaign shoot, and an attached picture can go on a device's screen when your words say so."
|
|
15062
15317
|
}
|
|
15063
15318
|
]
|
|
15064
15319
|
},
|
|
@@ -15073,10 +15328,11 @@ var RELEASES = [
|
|
|
15073
15328
|
{
|
|
15074
15329
|
version: "0.14.0",
|
|
15075
15330
|
date: "2026-09-22",
|
|
15331
|
+
title: "Settings is redesigned with six pages",
|
|
15076
15332
|
sections: [
|
|
15077
15333
|
{
|
|
15078
15334
|
heading: "Settings",
|
|
15079
|
-
body: "
|
|
15335
|
+
body: "Six pages instead of eight: Brand kit and Usage sit under the brand, Providers now includes the monthly caps, and General holds the theme and the library. On a phone it opens as a sheet, with the list of pages first and Back from each one."
|
|
15080
15336
|
},
|
|
15081
15337
|
{
|
|
15082
15338
|
heading: "Providers",
|
|
@@ -15091,16 +15347,18 @@ var RELEASES = [
|
|
|
15091
15347
|
{
|
|
15092
15348
|
version: "0.13.2",
|
|
15093
15349
|
date: "2026-09-22",
|
|
15350
|
+
title: "Presenters show an expression that fits the shot",
|
|
15094
15351
|
sections: [
|
|
15095
15352
|
{
|
|
15096
15353
|
heading: "Presenters",
|
|
15097
|
-
body: "
|
|
15354
|
+
body: "In a shot, a presenter now shows an expression that fits the moment and looks where the shot directs, instead of a blank stare into the camera. When a presenter has a detail on one side, such as a tattoo or a prosthetic, approving their face and full body now says which side to check."
|
|
15098
15355
|
}
|
|
15099
15356
|
]
|
|
15100
15357
|
},
|
|
15101
15358
|
{
|
|
15102
15359
|
version: "0.13.1",
|
|
15103
15360
|
date: "2026-09-22",
|
|
15361
|
+
title: "Alerts work without closing the dialog beneath them",
|
|
15104
15362
|
sections: [
|
|
15105
15363
|
{
|
|
15106
15364
|
heading: "Alerts",
|
|
@@ -15119,15 +15377,16 @@ var RELEASES = [
|
|
|
15119
15377
|
{
|
|
15120
15378
|
version: "0.13.0",
|
|
15121
15379
|
date: "2026-09-21",
|
|
15122
|
-
title: "Learn teaches Scenri one lesson at a time, alerts are quieter, and the Create feed holds still
|
|
15380
|
+
title: "Learn teaches Scenri one lesson at a time, alerts are quieter, and the Create feed holds still",
|
|
15381
|
+
announce: true,
|
|
15123
15382
|
sections: [
|
|
15124
15383
|
{
|
|
15125
15384
|
heading: "Learn",
|
|
15126
|
-
body: "
|
|
15385
|
+
body: "Six short lessons, in the top bar or under Help on a smaller screen, teach Scenri: your first shot, adding a product, a presenter, a scene, using a product again, and refining a shot. Each lesson keeps its own place, so you can set one down, take up another and come back to it. A new install is offered a guided first shot, and the guide can step back when you change your mind."
|
|
15127
15386
|
},
|
|
15128
15387
|
{
|
|
15129
15388
|
heading: "Alerts",
|
|
15130
|
-
body: "
|
|
15389
|
+
body: "Each alert is smaller, and they stack at the bottom of the screen, above the composer rather than over it. The same event twice shares one card, an error stays until you close it, and an alert holds still while you point at it or tab into it."
|
|
15131
15390
|
},
|
|
15132
15391
|
{
|
|
15133
15392
|
heading: "Create",
|
|
@@ -15138,7 +15397,8 @@ var RELEASES = [
|
|
|
15138
15397
|
{
|
|
15139
15398
|
version: "0.12.0",
|
|
15140
15399
|
date: "2026-09-20",
|
|
15141
|
-
title: "Saved work shows up everywhere at once, and a selection has its own toolbar
|
|
15400
|
+
title: "Saved work shows up everywhere at once, and a selection has its own toolbar",
|
|
15401
|
+
announce: true,
|
|
15142
15402
|
sections: [
|
|
15143
15403
|
{
|
|
15144
15404
|
heading: "Create",
|
|
@@ -15157,6 +15417,7 @@ var RELEASES = [
|
|
|
15157
15417
|
{
|
|
15158
15418
|
version: "0.11.1",
|
|
15159
15419
|
date: "2026-09-18",
|
|
15420
|
+
title: "Learn and the guided first shot are paused",
|
|
15160
15421
|
sections: [
|
|
15161
15422
|
{
|
|
15162
15423
|
heading: "Learn",
|
|
@@ -15171,7 +15432,8 @@ var RELEASES = [
|
|
|
15171
15432
|
{
|
|
15172
15433
|
version: "0.11.0",
|
|
15173
15434
|
date: "2026-09-18",
|
|
15174
|
-
title: "A new top bar, and a tutor that makes your first shot with you
|
|
15435
|
+
title: "A new top bar, and a tutor that makes your first shot with you",
|
|
15436
|
+
announce: true,
|
|
15175
15437
|
sections: [
|
|
15176
15438
|
{
|
|
15177
15439
|
heading: "Top bar",
|
|
@@ -15194,6 +15456,7 @@ var RELEASES = [
|
|
|
15194
15456
|
{
|
|
15195
15457
|
version: "0.10.3",
|
|
15196
15458
|
date: "2026-09-18",
|
|
15459
|
+
title: "Scenri no longer pauses right after the studio opens",
|
|
15197
15460
|
sections: [
|
|
15198
15461
|
{
|
|
15199
15462
|
heading: "Fixes",
|
|
@@ -15204,6 +15467,7 @@ var RELEASES = [
|
|
|
15204
15467
|
{
|
|
15205
15468
|
version: "0.10.2",
|
|
15206
15469
|
date: "2026-09-17",
|
|
15470
|
+
title: "A presenter draft opens where the conversation left off",
|
|
15207
15471
|
sections: [
|
|
15208
15472
|
{
|
|
15209
15473
|
heading: "Presenters",
|
|
@@ -15214,7 +15478,8 @@ var RELEASES = [
|
|
|
15214
15478
|
{
|
|
15215
15479
|
version: "0.10.1",
|
|
15216
15480
|
date: "2026-09-16",
|
|
15217
|
-
title: "A saved presenter can be duplicated or deleted straight from its card
|
|
15481
|
+
title: "A saved presenter can be duplicated or deleted straight from its card",
|
|
15482
|
+
announce: true,
|
|
15218
15483
|
sections: [
|
|
15219
15484
|
{
|
|
15220
15485
|
heading: "Presenters",
|
|
@@ -15237,7 +15502,8 @@ var RELEASES = [
|
|
|
15237
15502
|
{
|
|
15238
15503
|
version: "0.10.0",
|
|
15239
15504
|
date: "2026-09-16",
|
|
15240
|
-
title: "Presenters are cast by talking to Scenri, one question at a time
|
|
15505
|
+
title: "Presenters are cast by talking to Scenri, one question at a time",
|
|
15506
|
+
announce: true,
|
|
15241
15507
|
sections: [
|
|
15242
15508
|
{
|
|
15243
15509
|
heading: "Presenters",
|
|
@@ -15252,7 +15518,8 @@ var RELEASES = [
|
|
|
15252
15518
|
{
|
|
15253
15519
|
version: "0.9.4",
|
|
15254
15520
|
date: "2026-09-14",
|
|
15255
|
-
title: "A store that asks Scenri to slow down no longer looks like an empty one
|
|
15521
|
+
title: "A store that asks Scenri to slow down no longer looks like an empty one",
|
|
15522
|
+
announce: true,
|
|
15256
15523
|
sections: [
|
|
15257
15524
|
{
|
|
15258
15525
|
heading: "Products",
|
|
@@ -15267,7 +15534,8 @@ var RELEASES = [
|
|
|
15267
15534
|
{
|
|
15268
15535
|
version: "0.9.3",
|
|
15269
15536
|
date: "2026-09-14",
|
|
15270
|
-
title: "Paste a website, get the brand and its products
|
|
15537
|
+
title: "Paste a website, get the brand and its products",
|
|
15538
|
+
announce: true,
|
|
15271
15539
|
sections: [
|
|
15272
15540
|
{
|
|
15273
15541
|
heading: "Brand",
|
|
@@ -15290,6 +15558,7 @@ var RELEASES = [
|
|
|
15290
15558
|
{
|
|
15291
15559
|
version: "0.9.2",
|
|
15292
15560
|
date: "2026-09-07",
|
|
15561
|
+
title: "A Codex CLI too old for its model now says so",
|
|
15293
15562
|
sections: [
|
|
15294
15563
|
{
|
|
15295
15564
|
heading: "Fixes",
|
|
@@ -15300,6 +15569,7 @@ var RELEASES = [
|
|
|
15300
15569
|
{
|
|
15301
15570
|
version: "0.9.1",
|
|
15302
15571
|
date: "2026-09-07",
|
|
15572
|
+
title: "The Windows desktop icon never fails in silence",
|
|
15303
15573
|
sections: [
|
|
15304
15574
|
{
|
|
15305
15575
|
heading: "Desktop",
|
|
@@ -15311,6 +15581,7 @@ var RELEASES = [
|
|
|
15311
15581
|
version: "0.9.0",
|
|
15312
15582
|
date: "2026-09-06",
|
|
15313
15583
|
title: "Scenri on your desktop",
|
|
15584
|
+
announce: true,
|
|
15314
15585
|
sections: [
|
|
15315
15586
|
{
|
|
15316
15587
|
heading: "Desktop",
|
|
@@ -15329,6 +15600,7 @@ var RELEASES = [
|
|
|
15329
15600
|
{
|
|
15330
15601
|
version: "0.8.3",
|
|
15331
15602
|
date: "2026-09-06",
|
|
15603
|
+
title: "One picker for everything you add to a shot",
|
|
15332
15604
|
sections: [
|
|
15333
15605
|
{
|
|
15334
15606
|
heading: "Create",
|
|
@@ -15351,6 +15623,7 @@ var RELEASES = [
|
|
|
15351
15623
|
{
|
|
15352
15624
|
version: "0.8.2",
|
|
15353
15625
|
date: "2026-09-03",
|
|
15626
|
+
title: "Security updates to the server and its URL parser",
|
|
15354
15627
|
sections: [
|
|
15355
15628
|
{
|
|
15356
15629
|
heading: "Security",
|
|
@@ -15361,7 +15634,8 @@ var RELEASES = [
|
|
|
15361
15634
|
{
|
|
15362
15635
|
version: "0.8.1",
|
|
15363
15636
|
date: "2026-09-03",
|
|
15364
|
-
title: "The same speed at any size
|
|
15637
|
+
title: "The same speed at any size",
|
|
15638
|
+
announce: true,
|
|
15365
15639
|
sections: [
|
|
15366
15640
|
{
|
|
15367
15641
|
heading: "Create",
|
|
@@ -15380,7 +15654,8 @@ var RELEASES = [
|
|
|
15380
15654
|
{
|
|
15381
15655
|
version: "0.8.0",
|
|
15382
15656
|
date: "2026-09-02",
|
|
15383
|
-
title: "The composer, rebuilt around what a shot is made of
|
|
15657
|
+
title: "The composer, rebuilt around what a shot is made of",
|
|
15658
|
+
announce: true,
|
|
15384
15659
|
sections: [
|
|
15385
15660
|
{
|
|
15386
15661
|
heading: "Create",
|
|
@@ -15403,7 +15678,8 @@ var RELEASES = [
|
|
|
15403
15678
|
{
|
|
15404
15679
|
version: "0.7.5",
|
|
15405
15680
|
date: "2026-09-01",
|
|
15406
|
-
title: "Every shot is one card
|
|
15681
|
+
title: "Every shot is one card",
|
|
15682
|
+
announce: true,
|
|
15407
15683
|
sections: [
|
|
15408
15684
|
{
|
|
15409
15685
|
heading: "Create",
|
|
@@ -15422,6 +15698,7 @@ var RELEASES = [
|
|
|
15422
15698
|
{
|
|
15423
15699
|
version: "0.7.4",
|
|
15424
15700
|
date: "2026-08-31",
|
|
15701
|
+
title: "A failed Codex shot says why it failed",
|
|
15425
15702
|
sections: [
|
|
15426
15703
|
{
|
|
15427
15704
|
heading: "Fixes",
|
|
@@ -15432,6 +15709,7 @@ var RELEASES = [
|
|
|
15432
15709
|
{
|
|
15433
15710
|
version: "0.7.3",
|
|
15434
15711
|
date: "2026-08-31",
|
|
15712
|
+
title: "A finished tile no longer guesses its shape",
|
|
15435
15713
|
sections: [
|
|
15436
15714
|
{
|
|
15437
15715
|
heading: "Fixes",
|
|
@@ -15442,7 +15720,8 @@ var RELEASES = [
|
|
|
15442
15720
|
{
|
|
15443
15721
|
version: "0.7.2",
|
|
15444
15722
|
date: "2026-08-30",
|
|
15445
|
-
title: "Changing a shot to a new shape keeps the photograph
|
|
15723
|
+
title: "Changing a shot to a new shape keeps the photograph",
|
|
15724
|
+
announce: true,
|
|
15446
15725
|
sections: [
|
|
15447
15726
|
{
|
|
15448
15727
|
heading: "Create",
|
|
@@ -15457,7 +15736,8 @@ var RELEASES = [
|
|
|
15457
15736
|
{
|
|
15458
15737
|
version: "0.7.1",
|
|
15459
15738
|
date: "2026-08-30",
|
|
15460
|
-
title: "Four images from one
|
|
15739
|
+
title: "Four images from one prompt are one set",
|
|
15740
|
+
announce: true,
|
|
15461
15741
|
sections: [
|
|
15462
15742
|
{
|
|
15463
15743
|
heading: "Create",
|
|
@@ -15476,7 +15756,8 @@ var RELEASES = [
|
|
|
15476
15756
|
{
|
|
15477
15757
|
version: "0.7.0",
|
|
15478
15758
|
date: "2026-08-30",
|
|
15479
|
-
title: "Your presenter stays your presenter
|
|
15759
|
+
title: "Your presenter stays your presenter",
|
|
15760
|
+
announce: true,
|
|
15480
15761
|
sections: [
|
|
15481
15762
|
{
|
|
15482
15763
|
heading: "Scenes",
|
|
@@ -15495,6 +15776,7 @@ var RELEASES = [
|
|
|
15495
15776
|
{
|
|
15496
15777
|
version: "0.6.13",
|
|
15497
15778
|
date: "2026-08-30",
|
|
15779
|
+
title: "Products keep their colour, and your logo joins the shot",
|
|
15498
15780
|
sections: [
|
|
15499
15781
|
{
|
|
15500
15782
|
heading: "Products",
|
|
@@ -15513,6 +15795,7 @@ var RELEASES = [
|
|
|
15513
15795
|
{
|
|
15514
15796
|
version: "0.6.12",
|
|
15515
15797
|
date: "2026-08-29",
|
|
15798
|
+
title: "Reference images show what they are",
|
|
15516
15799
|
sections: [
|
|
15517
15800
|
{
|
|
15518
15801
|
heading: "Create",
|
|
@@ -15523,6 +15806,7 @@ var RELEASES = [
|
|
|
15523
15806
|
{
|
|
15524
15807
|
version: "0.6.11",
|
|
15525
15808
|
date: "2026-08-29",
|
|
15809
|
+
title: "Hitting your Codex usage limit says when it comes back",
|
|
15526
15810
|
sections: [
|
|
15527
15811
|
{
|
|
15528
15812
|
heading: "Create",
|
|
@@ -15533,7 +15817,8 @@ var RELEASES = [
|
|
|
15533
15817
|
{
|
|
15534
15818
|
version: "0.6.10",
|
|
15535
15819
|
date: "2026-08-29",
|
|
15536
|
-
title: "Pictures keep their shape
|
|
15820
|
+
title: "Pictures keep their shape",
|
|
15821
|
+
announce: true,
|
|
15537
15822
|
sections: [
|
|
15538
15823
|
{
|
|
15539
15824
|
heading: "Create",
|
|
@@ -15548,7 +15833,8 @@ var RELEASES = [
|
|
|
15548
15833
|
{
|
|
15549
15834
|
version: "0.6.9",
|
|
15550
15835
|
date: "2026-08-29",
|
|
15551
|
-
title: "Refinements keep the shot
|
|
15836
|
+
title: "Refinements keep the shot",
|
|
15837
|
+
announce: true,
|
|
15552
15838
|
sections: [
|
|
15553
15839
|
{
|
|
15554
15840
|
heading: "Refine",
|
|
@@ -15567,6 +15853,7 @@ var RELEASES = [
|
|
|
15567
15853
|
{
|
|
15568
15854
|
version: "0.6.8",
|
|
15569
15855
|
date: "2026-08-29",
|
|
15856
|
+
title: "Closing a new form ends it, with Undo if you did not mean it",
|
|
15570
15857
|
sections: [
|
|
15571
15858
|
{
|
|
15572
15859
|
heading: "Create dialogs",
|
|
@@ -15581,6 +15868,7 @@ var RELEASES = [
|
|
|
15581
15868
|
{
|
|
15582
15869
|
version: "0.6.7",
|
|
15583
15870
|
date: "2026-08-29",
|
|
15871
|
+
title: "The prompt starts empty, with no scene you did not pick",
|
|
15584
15872
|
sections: [
|
|
15585
15873
|
{
|
|
15586
15874
|
heading: "Create",
|
|
@@ -15591,6 +15879,7 @@ var RELEASES = [
|
|
|
15591
15879
|
{
|
|
15592
15880
|
version: "0.6.6",
|
|
15593
15881
|
date: "2026-08-29",
|
|
15882
|
+
title: "Removing a chip works on the first click",
|
|
15594
15883
|
sections: [
|
|
15595
15884
|
{
|
|
15596
15885
|
heading: "Create",
|
|
@@ -15605,6 +15894,7 @@ var RELEASES = [
|
|
|
15605
15894
|
{
|
|
15606
15895
|
version: "0.6.5",
|
|
15607
15896
|
date: "2026-08-29",
|
|
15897
|
+
title: "A scene no longer brings its own demo object",
|
|
15608
15898
|
sections: [
|
|
15609
15899
|
{
|
|
15610
15900
|
heading: "Scenes",
|
|
@@ -15615,6 +15905,7 @@ var RELEASES = [
|
|
|
15615
15905
|
{
|
|
15616
15906
|
version: "0.6.4",
|
|
15617
15907
|
date: "2026-08-29",
|
|
15908
|
+
title: "New forms no longer carry photos from something else",
|
|
15618
15909
|
sections: [
|
|
15619
15910
|
{
|
|
15620
15911
|
heading: "Create",
|
|
@@ -15625,7 +15916,8 @@ var RELEASES = [
|
|
|
15625
15916
|
{
|
|
15626
15917
|
version: "0.6.3",
|
|
15627
15918
|
date: "2026-08-28",
|
|
15628
|
-
title: "Your logo stays your logo, and every page opens like a page
|
|
15919
|
+
title: "Your logo stays your logo, and every page opens like a page",
|
|
15920
|
+
announce: true,
|
|
15629
15921
|
sections: [
|
|
15630
15922
|
{
|
|
15631
15923
|
heading: "Brand",
|
|
@@ -15640,7 +15932,8 @@ var RELEASES = [
|
|
|
15640
15932
|
{
|
|
15641
15933
|
version: "0.6.2",
|
|
15642
15934
|
date: "2026-08-28",
|
|
15643
|
-
title: "Scenes keep what makes them distinctive
|
|
15935
|
+
title: "Scenes keep what makes them distinctive",
|
|
15936
|
+
announce: true,
|
|
15644
15937
|
sections: [
|
|
15645
15938
|
{
|
|
15646
15939
|
heading: "Scenes",
|
|
@@ -15663,6 +15956,7 @@ var RELEASES = [
|
|
|
15663
15956
|
{
|
|
15664
15957
|
version: "0.6.1",
|
|
15665
15958
|
date: "2026-08-26",
|
|
15959
|
+
title: "Refining a shot opens at its own shape",
|
|
15666
15960
|
sections: [
|
|
15667
15961
|
{
|
|
15668
15962
|
heading: "Shots",
|
|
@@ -15673,6 +15967,7 @@ var RELEASES = [
|
|
|
15673
15967
|
{
|
|
15674
15968
|
version: "0.6.0",
|
|
15675
15969
|
date: "2026-08-26",
|
|
15970
|
+
title: "A wider or taller shot keeps the original photograph",
|
|
15676
15971
|
sections: [
|
|
15677
15972
|
{
|
|
15678
15973
|
heading: "Shots",
|
|
@@ -15687,6 +15982,7 @@ var RELEASES = [
|
|
|
15687
15982
|
{
|
|
15688
15983
|
version: "0.5.1",
|
|
15689
15984
|
date: "2026-08-26",
|
|
15985
|
+
title: "An extended shot keeps its subject where it was",
|
|
15690
15986
|
sections: [
|
|
15691
15987
|
{
|
|
15692
15988
|
heading: "Shots",
|
|
@@ -15697,7 +15993,8 @@ var RELEASES = [
|
|
|
15697
15993
|
{
|
|
15698
15994
|
version: "0.5.0",
|
|
15699
15995
|
date: "2026-08-26",
|
|
15700
|
-
title: "A finished shot can change shape, and a presenter you attach is a requirement rather than a suggestion
|
|
15996
|
+
title: "A finished shot can change shape, and a presenter you attach is a requirement rather than a suggestion",
|
|
15997
|
+
announce: true,
|
|
15701
15998
|
sections: [
|
|
15702
15999
|
{
|
|
15703
16000
|
heading: "Create",
|
|
@@ -15720,7 +16017,8 @@ var RELEASES = [
|
|
|
15720
16017
|
{
|
|
15721
16018
|
version: "0.4.7",
|
|
15722
16019
|
date: "2026-08-24",
|
|
15723
|
-
title: "The feed shows the work, and the controls wait until you point at it
|
|
16020
|
+
title: "The feed shows the work, and the controls wait until you point at it",
|
|
16021
|
+
announce: true,
|
|
15724
16022
|
sections: [
|
|
15725
16023
|
{
|
|
15726
16024
|
heading: "Create",
|
|
@@ -15739,6 +16037,7 @@ var RELEASES = [
|
|
|
15739
16037
|
{
|
|
15740
16038
|
version: "0.4.6",
|
|
15741
16039
|
date: "2026-08-24",
|
|
16040
|
+
title: "Removing something from a shot leaves no outline",
|
|
15742
16041
|
sections: [
|
|
15743
16042
|
{
|
|
15744
16043
|
heading: "Refining",
|
|
@@ -15753,7 +16052,8 @@ var RELEASES = [
|
|
|
15753
16052
|
{
|
|
15754
16053
|
version: "0.4.5",
|
|
15755
16054
|
date: "2026-08-23",
|
|
15756
|
-
title: "New versions find you while Scenri runs
|
|
16055
|
+
title: "New versions find you while Scenri runs",
|
|
16056
|
+
announce: true,
|
|
15757
16057
|
sections: [
|
|
15758
16058
|
{
|
|
15759
16059
|
heading: "Updates",
|
|
@@ -15768,7 +16068,8 @@ var RELEASES = [
|
|
|
15768
16068
|
{
|
|
15769
16069
|
version: "0.4.4",
|
|
15770
16070
|
date: "2026-08-23",
|
|
15771
|
-
title: "Codex setup tells the truth, and a stuck generation fails instead of running forever
|
|
16071
|
+
title: "Codex setup tells the truth, and a stuck generation fails instead of running forever",
|
|
16072
|
+
announce: true,
|
|
15772
16073
|
sections: [
|
|
15773
16074
|
{
|
|
15774
16075
|
heading: "Codex",
|
|
@@ -15783,7 +16084,8 @@ var RELEASES = [
|
|
|
15783
16084
|
{
|
|
15784
16085
|
version: "0.4.3",
|
|
15785
16086
|
date: "2026-08-23",
|
|
15786
|
-
title: "Controls hold still when you press them
|
|
16087
|
+
title: "Controls hold still when you press them",
|
|
16088
|
+
announce: true,
|
|
15787
16089
|
sections: [
|
|
15788
16090
|
{
|
|
15789
16091
|
heading: "Fixes",
|
|
@@ -15794,6 +16096,7 @@ var RELEASES = [
|
|
|
15794
16096
|
{
|
|
15795
16097
|
version: "0.4.2",
|
|
15796
16098
|
date: "2026-08-23",
|
|
16099
|
+
title: "Settings tells the update story in one row",
|
|
15797
16100
|
sections: [
|
|
15798
16101
|
{
|
|
15799
16102
|
heading: "Updates",
|
|
@@ -15804,7 +16107,8 @@ var RELEASES = [
|
|
|
15804
16107
|
{
|
|
15805
16108
|
version: "0.4.1",
|
|
15806
16109
|
date: "2026-08-23",
|
|
15807
|
-
title: "Updates now arrive by themselves
|
|
16110
|
+
title: "Updates now arrive by themselves",
|
|
16111
|
+
announce: true,
|
|
15808
16112
|
sections: [
|
|
15809
16113
|
{
|
|
15810
16114
|
heading: "Updates",
|
|
@@ -15819,7 +16123,8 @@ var RELEASES = [
|
|
|
15819
16123
|
{
|
|
15820
16124
|
version: "0.4.0",
|
|
15821
16125
|
date: "2026-08-23",
|
|
15822
|
-
title: "Refining a shot keeps the shot
|
|
16126
|
+
title: "Refining a shot keeps the shot",
|
|
16127
|
+
announce: true,
|
|
15823
16128
|
sections: [
|
|
15824
16129
|
{
|
|
15825
16130
|
heading: "Refining",
|
|
@@ -15842,6 +16147,7 @@ var RELEASES = [
|
|
|
15842
16147
|
{
|
|
15843
16148
|
version: "0.3.5",
|
|
15844
16149
|
date: "2026-08-21",
|
|
16150
|
+
title: "Codex setup on Windows sees a successful install",
|
|
15845
16151
|
sections: [
|
|
15846
16152
|
{
|
|
15847
16153
|
heading: "Fixes",
|
|
@@ -15852,6 +16158,7 @@ var RELEASES = [
|
|
|
15852
16158
|
{
|
|
15853
16159
|
version: "0.3.4",
|
|
15854
16160
|
date: "2026-08-21",
|
|
16161
|
+
title: "A presenter's photos define who they are, not what they wear",
|
|
15855
16162
|
sections: [
|
|
15856
16163
|
{
|
|
15857
16164
|
heading: "Presenters",
|
|
@@ -15873,6 +16180,7 @@ var RELEASES = [
|
|
|
15873
16180
|
{
|
|
15874
16181
|
version: "0.3.2",
|
|
15875
16182
|
date: "2026-08-20",
|
|
16183
|
+
title: "Codex CLI setup offers the command that gets past a refusal",
|
|
15876
16184
|
sections: [
|
|
15877
16185
|
{
|
|
15878
16186
|
heading: "Fixes",
|
|
@@ -15883,6 +16191,7 @@ var RELEASES = [
|
|
|
15883
16191
|
{
|
|
15884
16192
|
version: "0.3.1",
|
|
15885
16193
|
date: "2026-08-20",
|
|
16194
|
+
title: "Starting Scenri explains what went wrong in plain words",
|
|
15886
16195
|
sections: [
|
|
15887
16196
|
{
|
|
15888
16197
|
heading: "Fixes",
|
|
@@ -15893,7 +16202,8 @@ var RELEASES = [
|
|
|
15893
16202
|
{
|
|
15894
16203
|
version: "0.3.0",
|
|
15895
16204
|
date: "2026-08-20",
|
|
15896
|
-
title: "The name is Scenri
|
|
16205
|
+
title: "The name is Scenri",
|
|
16206
|
+
announce: true,
|
|
15897
16207
|
sections: [
|
|
15898
16208
|
{
|
|
15899
16209
|
heading: "The name",
|
|
@@ -15908,26 +16218,29 @@ var RELEASES = [
|
|
|
15908
16218
|
{
|
|
15909
16219
|
version: "0.2.3",
|
|
15910
16220
|
date: "2026-08-20",
|
|
16221
|
+
title: "The last advisory against a build dependency is closed",
|
|
15911
16222
|
sections: [
|
|
15912
16223
|
{
|
|
15913
16224
|
heading: "Security",
|
|
15914
|
-
body: "The last advisory against a build dependency is closed. Nothing about how
|
|
16225
|
+
body: "The last advisory against a build dependency is closed. Nothing about how Scenri runs changes, and nothing in the published package moves."
|
|
15915
16226
|
}
|
|
15916
16227
|
]
|
|
15917
16228
|
},
|
|
15918
16229
|
{
|
|
15919
16230
|
version: "0.2.2",
|
|
15920
16231
|
date: "2026-08-20",
|
|
16232
|
+
title: "Two dependencies are on their patched releases",
|
|
15921
16233
|
sections: [
|
|
15922
16234
|
{
|
|
15923
16235
|
heading: "Security",
|
|
15924
|
-
body: "Two dependencies are on their patched releases. The file server that serves the studio is updated for a path traversal advisory, and the image library
|
|
16236
|
+
body: "Two dependencies are on their patched releases. The file server that serves the studio is updated for a path traversal advisory, and the image library Scenri re-encodes every upload through is updated for the libvips advisories. Nothing about how Scenri works changes."
|
|
15925
16237
|
}
|
|
15926
16238
|
]
|
|
15927
16239
|
},
|
|
15928
16240
|
{
|
|
15929
16241
|
version: "0.2.1",
|
|
15930
16242
|
date: "2026-08-20",
|
|
16243
|
+
title: "The top bar and app icons carry the real mark",
|
|
15931
16244
|
sections: [
|
|
15932
16245
|
{
|
|
15933
16246
|
heading: "Fixes",
|
|
@@ -15938,7 +16251,8 @@ var RELEASES = [
|
|
|
15938
16251
|
{
|
|
15939
16252
|
version: "0.2.0",
|
|
15940
16253
|
date: "2026-08-20",
|
|
15941
|
-
title: "
|
|
16254
|
+
title: "Scenri goes public",
|
|
16255
|
+
announce: true,
|
|
15942
16256
|
sections: [
|
|
15943
16257
|
{
|
|
15944
16258
|
heading: "Compose",
|
|
@@ -15954,7 +16268,7 @@ var RELEASES = [
|
|
|
15954
16268
|
},
|
|
15955
16269
|
{
|
|
15956
16270
|
heading: "The studio",
|
|
15957
|
-
body: "The
|
|
16271
|
+
body: "The Scenri mark sits in the top bar, the browser tab names the screen you are on, and About says who builds this and under which license."
|
|
15958
16272
|
}
|
|
15959
16273
|
]
|
|
15960
16274
|
},
|
|
@@ -15972,6 +16286,7 @@ var RELEASES = [
|
|
|
15972
16286
|
version: "0.1.0",
|
|
15973
16287
|
date: "2026-08-16",
|
|
15974
16288
|
title: "The first complete scenri, released internally.",
|
|
16289
|
+
announce: true,
|
|
15975
16290
|
sections: [
|
|
15976
16291
|
{
|
|
15977
16292
|
heading: "Create",
|
|
@@ -15998,6 +16313,35 @@ function releaseFor(version) {
|
|
|
15998
16313
|
function isNewsworthy(entry) {
|
|
15999
16314
|
return (entry?.sections.length ?? 0) > 0;
|
|
16000
16315
|
}
|
|
16316
|
+
function whatsNewWindow(releases, running5, seen = null) {
|
|
16317
|
+
const released = running5 !== "0.0.0";
|
|
16318
|
+
const recent = [];
|
|
16319
|
+
let headlines = 0;
|
|
16320
|
+
for (const r of releases) {
|
|
16321
|
+
if (headlines === HEADLINES_KEPT) break;
|
|
16322
|
+
if (!isNewsworthy(r)) continue;
|
|
16323
|
+
if (released && compareSemver(r.version, running5) > 0) continue;
|
|
16324
|
+
recent.push(r);
|
|
16325
|
+
if (r.announce) headlines++;
|
|
16326
|
+
}
|
|
16327
|
+
const unseen = seen === null || !released ? [] : recent.filter((r) => compareSemver(seen, r.version) < 0 && compareSemver(r.version, running5) <= 0).map((r) => r.version);
|
|
16328
|
+
const lead = recent.find((r) => r.announce && unseen.includes(r.version))?.version ?? null;
|
|
16329
|
+
const history = releases.filter(
|
|
16330
|
+
(r) => isNewsworthy(r) && compareSemver(r.version, FIRST_PUBLIC) >= 0 && (!released || compareSemver(r.version, running5) <= 0)
|
|
16331
|
+
);
|
|
16332
|
+
return { recent, history, unseen, lead };
|
|
16333
|
+
}
|
|
16334
|
+
var NEW_FOR_DAYS = 30;
|
|
16335
|
+
var DAY_MS = 864e5;
|
|
16336
|
+
var dayOf = (entry) => Date.parse(`${entry.date}T00:00:00Z`);
|
|
16337
|
+
function newUntil(entry) {
|
|
16338
|
+
return dayOf(entry) + NEW_FOR_DAYS * DAY_MS;
|
|
16339
|
+
}
|
|
16340
|
+
function newFeaturesFor(releases, install) {
|
|
16341
|
+
const { firstVersion, used, now: now2 } = install;
|
|
16342
|
+
if (!firstVersion) return [];
|
|
16343
|
+
return releases.filter((r) => compareSemver(r.version, firstVersion) > 0 && now2 < newUntil(r)).flatMap((r) => r.newFeatures ?? []).filter((f) => !used.includes(f));
|
|
16344
|
+
}
|
|
16001
16345
|
|
|
16002
16346
|
// src/routes/updates.ts
|
|
16003
16347
|
function registerUpdateRoutes(app, deps) {
|
|
@@ -16156,38 +16500,68 @@ function registerUpdateRoutes(app, deps) {
|
|
|
16156
16500
|
});
|
|
16157
16501
|
}, 50);
|
|
16158
16502
|
});
|
|
16503
|
+
const raiseSeen = (v) => {
|
|
16504
|
+
const was = core.store.getSetting("whatsnew.seen");
|
|
16505
|
+
if (!was || compareSemver(v, was) > 0) core.store.setSetting("whatsnew.seen", v);
|
|
16506
|
+
};
|
|
16159
16507
|
app.get("/api/release/notes", async () => {
|
|
16160
|
-
|
|
16161
|
-
|
|
16162
|
-
|
|
16163
|
-
|
|
16508
|
+
const unmarked = !core.store.getSetting("install.firstVersion");
|
|
16509
|
+
if (unmarked) core.store.setSetting("install.firstVersion", meta.version);
|
|
16510
|
+
if (unmarked || !core.store.getSetting("whatsnew.seen")) raiseSeen(meta.version);
|
|
16511
|
+
const seen = core.store.getSetting("whatsnew.seen") || null;
|
|
16164
16512
|
return {
|
|
16165
16513
|
version: meta.version,
|
|
16166
16514
|
entry: releaseFor(meta.version),
|
|
16167
|
-
seen
|
|
16515
|
+
seen,
|
|
16516
|
+
// The in-app history (`recent`), what in it is new here (`unseen`), and
|
|
16517
|
+
// the one headline update allowed to open by itself (`lead`). All of it
|
|
16518
|
+
// is in the build: nothing here reaches the network.
|
|
16519
|
+
...whatsNewWindow(RELEASES, meta.version, seen),
|
|
16168
16520
|
// 0.0.0 is the placeholder release-please has not bumped yet. No tag has
|
|
16169
16521
|
// ever existed for it, and the releases index of a project that has not
|
|
16170
16522
|
// released is an empty page, so there is nowhere honest to point: null.
|
|
16171
|
-
// Its absence is also what tells the dialog it is looking at a
|
|
16172
|
-
// development build rather than a release nobody wrote notes for.
|
|
16173
16523
|
// Every other version a user can be running was published, and
|
|
16174
16524
|
// publishing is what creates the tag. The exceptions are the internal
|
|
16175
16525
|
// 0.1.x builds: published, unpublished, never tagged. Their numbers are
|
|
16176
16526
|
// burned on npm and a tag link would 404.
|
|
16177
16527
|
changelogUrl: ghSlug && meta.version !== UNRELEASED && !UNTAGGED.has(meta.version) ? `https://github.com/${ghSlug}/releases/tag/v${meta.version}` : null,
|
|
16178
|
-
//
|
|
16179
|
-
// a tag: this one is the archive,
|
|
16180
|
-
// Gated on there being an archive
|
|
16181
|
-
//
|
|
16182
|
-
//
|
|
16183
|
-
releasesUrl: ghSlug && RELEASES.some(isNewsworthy) ? `https://github.com/${ghSlug}/releases` : null
|
|
16528
|
+
// Full release notes: everything older than the in-app window, and every
|
|
16529
|
+
// fix a record left out. The index, not a tag: this one is the archive,
|
|
16530
|
+
// and it outlives the version running. Gated on there being an archive
|
|
16531
|
+
// at all, so a project that has never published does not offer a link
|
|
16532
|
+
// to an empty page.
|
|
16533
|
+
releasesUrl: ghSlug && RELEASES.some(isNewsworthy) ? `https://github.com/${ghSlug}/releases` : null,
|
|
16534
|
+
// The few features saying New on this install, from the same record
|
|
16535
|
+
// (DESIGN.md, "New"). Read after the stamping above, so a new home's
|
|
16536
|
+
// first read has none: everything is new to it.
|
|
16537
|
+
newFeatures: newFeaturesFor(RELEASES, {
|
|
16538
|
+
firstVersion: core.store.getSetting("install.firstVersion"),
|
|
16539
|
+
used: usedFeatures(),
|
|
16540
|
+
now: Date.now()
|
|
16541
|
+
})
|
|
16184
16542
|
};
|
|
16185
16543
|
});
|
|
16186
16544
|
app.post("/api/release/seen", async (req) => {
|
|
16187
16545
|
const v = req.body?.version;
|
|
16188
|
-
|
|
16546
|
+
raiseSeen(typeof v === "string" && isReleaseTriplet(v) ? v : meta.version);
|
|
16547
|
+
return { ok: true };
|
|
16548
|
+
});
|
|
16549
|
+
app.post("/api/release/used", async (req, reply) => {
|
|
16550
|
+
const f = req.body?.feature;
|
|
16551
|
+
if (typeof f !== "string" || !RELEASES.some((r) => r.newFeatures?.includes(f)))
|
|
16552
|
+
return reply.status(400).send({ error: "not a feature any release marked New" });
|
|
16553
|
+
const used = usedFeatures();
|
|
16554
|
+
if (!used.includes(f)) core.store.setSetting("features.used", JSON.stringify([...used, f]));
|
|
16189
16555
|
return { ok: true };
|
|
16190
16556
|
});
|
|
16557
|
+
function usedFeatures() {
|
|
16558
|
+
try {
|
|
16559
|
+
const v = JSON.parse(core.store.getSetting("features.used") ?? "[]");
|
|
16560
|
+
return Array.isArray(v) ? v.filter((x) => typeof x === "string") : [];
|
|
16561
|
+
} catch {
|
|
16562
|
+
return [];
|
|
16563
|
+
}
|
|
16564
|
+
}
|
|
16191
16565
|
}
|
|
16192
16566
|
function registerSystemRoutes(app, deps) {
|
|
16193
16567
|
const { core, thumbs } = deps;
|
|
@@ -16210,10 +16584,19 @@ function registerSystemRoutes(app, deps) {
|
|
|
16210
16584
|
app.post("/api/system/reveal", async (req, reply) => {
|
|
16211
16585
|
if (!fromThisComputer(req)) return reply.status(403).send({ error: "Only on the computer running Scenri." });
|
|
16212
16586
|
const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "explorer" : "xdg-open";
|
|
16213
|
-
|
|
16214
|
-
|
|
16215
|
-
|
|
16216
|
-
|
|
16587
|
+
const opened = await new Promise((resolve) => {
|
|
16588
|
+
try {
|
|
16589
|
+
const child = spawn(cmd, [core.home], { detached: true, stdio: "ignore" });
|
|
16590
|
+
child.on("error", () => resolve(false));
|
|
16591
|
+
child.once("spawn", () => {
|
|
16592
|
+
child.unref();
|
|
16593
|
+
resolve(true);
|
|
16594
|
+
});
|
|
16595
|
+
} catch {
|
|
16596
|
+
resolve(false);
|
|
16597
|
+
}
|
|
16598
|
+
});
|
|
16599
|
+
if (!opened) return reply.status(501).send({ error: "No file manager could be opened on this computer." });
|
|
16217
16600
|
return reply.send({ ok: true });
|
|
16218
16601
|
});
|
|
16219
16602
|
app.get("/api/export/all", async (_req, reply) => {
|
|
@@ -16243,6 +16626,7 @@ function registerSystemRoutes(app, deps) {
|
|
|
16243
16626
|
const scope = String(req.query?.scope ?? "");
|
|
16244
16627
|
if (scope !== "shots" && scope !== "all") return reply.status(400).send({ error: "scope must be shots or all" });
|
|
16245
16628
|
if (scope === "shots") {
|
|
16629
|
+
deps.stopAllRuns();
|
|
16246
16630
|
let removed = 0;
|
|
16247
16631
|
for (const brand of core.store.listBrands()) {
|
|
16248
16632
|
for (const set of core.store.listSets(brand.id)) core.store.deleteSet(set.id);
|
|
@@ -16513,7 +16897,7 @@ function registerDesktopRoutes(app, deps) {
|
|
|
16513
16897
|
record: null
|
|
16514
16898
|
};
|
|
16515
16899
|
}
|
|
16516
|
-
const { installDeps } = await import('./cli-
|
|
16900
|
+
const { installDeps } = await import('./cli-WRNWT6P7.js');
|
|
16517
16901
|
const { desktopStatus } = await import('./install-VXJPLYWM.js');
|
|
16518
16902
|
return desktopStatus(installDeps(runtime.entry));
|
|
16519
16903
|
});
|
|
@@ -16521,7 +16905,7 @@ function registerDesktopRoutes(app, deps) {
|
|
|
16521
16905
|
if (!runtime.entry) {
|
|
16522
16906
|
return { ok: false, reason: "unsupported", message: "Desktop shortcuts are not available on this system yet." };
|
|
16523
16907
|
}
|
|
16524
|
-
const { addToDesktop } = await import('./cli-
|
|
16908
|
+
const { addToDesktop } = await import('./cli-WRNWT6P7.js');
|
|
16525
16909
|
return addToDesktop(runtime.entry);
|
|
16526
16910
|
});
|
|
16527
16911
|
app.get("/api/desktop", async (req) => {
|
|
@@ -16632,6 +17016,16 @@ function buildServer(opts) {
|
|
|
16632
17016
|
});
|
|
16633
17017
|
const reserved = /* @__PURE__ */ new Map();
|
|
16634
17018
|
const runningGenerations = /* @__PURE__ */ new Map();
|
|
17019
|
+
const stopRuns = (belongs) => {
|
|
17020
|
+
const stopped = /* @__PURE__ */ new Set();
|
|
17021
|
+
for (const [id, ctrl] of runningGenerations) {
|
|
17022
|
+
if (stopped.has(ctrl)) continue;
|
|
17023
|
+
const node = core.store.getNode(id);
|
|
17024
|
+
if (!node || !belongs(node)) continue;
|
|
17025
|
+
ctrl.abort();
|
|
17026
|
+
stopped.add(ctrl);
|
|
17027
|
+
}
|
|
17028
|
+
};
|
|
16635
17029
|
core.catalog.reconcileInterruptedJobs();
|
|
16636
17030
|
const thumbs = createThumbStore(core);
|
|
16637
17031
|
const { scenes } = loadScenes(opts.templatesDir);
|
|
@@ -16698,6 +17092,8 @@ function buildServer(opts) {
|
|
|
16698
17092
|
const quiet = {
|
|
16699
17093
|
core};
|
|
16700
17094
|
for (const d of listPresenterDrafts(core, id)) await discardPresenterDraft(quiet, d.id, hooks);
|
|
17095
|
+
stopRuns((node) => core.store.getProject(node.projectId)?.brandId === id);
|
|
17096
|
+
for (const job of core.catalog.listJobs(id)) cancelCatalogImport(job.id);
|
|
16701
17097
|
}
|
|
16702
17098
|
core.store.deleteBrand(id);
|
|
16703
17099
|
if (brand) {
|
|
@@ -16742,7 +17138,9 @@ function buildServer(opts) {
|
|
|
16742
17138
|
).slice(0, 80).join("").trim() || spec.fallback;
|
|
16743
17139
|
}
|
|
16744
17140
|
const id = `${spec.prefix}-${randomUUID().slice(0, 8)}`;
|
|
16745
|
-
const
|
|
17141
|
+
const current = core.store.getBrand(brand.id);
|
|
17142
|
+
if (!current) return reply.status(404).send({ error: "brand not found" });
|
|
17143
|
+
const json = { ...current.json };
|
|
16746
17144
|
json[spec.key] = [
|
|
16747
17145
|
...json[spec.key] ?? [],
|
|
16748
17146
|
{
|
|
@@ -16780,7 +17178,9 @@ function buildServer(opts) {
|
|
|
16780
17178
|
inspectMark: (buf) => inspectMark(buf, toMarkPng),
|
|
16781
17179
|
createdWith: `${meta.name}/${meta.version}`
|
|
16782
17180
|
});
|
|
16783
|
-
const
|
|
17181
|
+
const current = core.store.getBrand(brand.id);
|
|
17182
|
+
if (!current) return reply.status(404).send({ error: "brand not found" });
|
|
17183
|
+
const { brand: merged, suggestions } = mergeScrape(current.json, scraped);
|
|
16784
17184
|
const v = validateBrand(merged);
|
|
16785
17185
|
if (!v.valid) return reply.status(400).send({ error: "brand became invalid", details: v.errors });
|
|
16786
17186
|
const row = core.store.updateBrand(brand.id, merged);
|
|
@@ -16809,11 +17209,9 @@ function buildServer(opts) {
|
|
|
16809
17209
|
if (!brand) return reply.status(404).send({ error: "brand not found" });
|
|
16810
17210
|
const productId = String(req.params.productId);
|
|
16811
17211
|
const catalogId2 = productId.startsWith("cat-") ? productId.slice(4) : null;
|
|
16812
|
-
const
|
|
16813
|
-
const products = json.products ?? [];
|
|
16814
|
-
const idx = catalogId2 ? -1 : products.findIndex((p) => p.id === productId);
|
|
17212
|
+
const owned = (json2) => (json2.products ?? []).findIndex((p) => p.id === productId);
|
|
16815
17213
|
const catalogRow = catalogId2 ? core.catalog.getProduct(catalogId2) : null;
|
|
16816
|
-
if (
|
|
17214
|
+
if ((catalogId2 ? -1 : owned(brand.json)) === -1 && (!catalogRow || catalogRow.brandId !== brand.id)) {
|
|
16817
17215
|
return reply.status(404).send({ error: "product not found" });
|
|
16818
17216
|
}
|
|
16819
17217
|
const part = await readImagePart(core, req, toPng);
|
|
@@ -16824,6 +17222,11 @@ function buildServer(opts) {
|
|
|
16824
17222
|
core.catalog.addLocalImage(catalogId2, `asset:${hash}`, angle ?? null);
|
|
16825
17223
|
return core.store.getBrand(brand.id);
|
|
16826
17224
|
}
|
|
17225
|
+
const current = core.store.getBrand(brand.id);
|
|
17226
|
+
const json = { ...current?.json };
|
|
17227
|
+
const products = json.products ?? [];
|
|
17228
|
+
const idx = current ? owned(json) : -1;
|
|
17229
|
+
if (idx === -1) return reply.status(404).send({ error: "product not found" });
|
|
16827
17230
|
const shot = { file: `asset:${hash}`, locked: true };
|
|
16828
17231
|
if (angle) shot.angle = angle;
|
|
16829
17232
|
json.products = products.map((p, i) => i === idx ? { ...p, shots: [...p.shots ?? [], shot] } : p);
|
|
@@ -17075,6 +17478,8 @@ function buildServer(opts) {
|
|
|
17075
17478
|
}
|
|
17076
17479
|
}
|
|
17077
17480
|
if (inheritedPerson) inheritedDirectives.push(personSkinDirective());
|
|
17481
|
+
const parentImages = core.store.getNode(parentId)?.images ?? [];
|
|
17482
|
+
const source = opts2?.source ?? (parentImages.length === 1 ? parentImages[0] : void 0);
|
|
17078
17483
|
const compileCtx = {
|
|
17079
17484
|
brand: brandJson,
|
|
17080
17485
|
images: core.images,
|
|
@@ -17083,6 +17488,9 @@ function buildServer(opts) {
|
|
|
17083
17488
|
template: brief.templateId ? sceneById(String(brief.templateId)) : void 0,
|
|
17084
17489
|
templateById: sceneById,
|
|
17085
17490
|
mode: "edit",
|
|
17491
|
+
// The frame rides on its own, so a picture this refine attaches that is
|
|
17492
|
+
// the same bytes is never sent a second time beside it.
|
|
17493
|
+
...source ? { sourceHash: String(source) } : {},
|
|
17086
17494
|
editScope: verdict.scope,
|
|
17087
17495
|
editRemoval: verdict.removal ?? false,
|
|
17088
17496
|
// Kinds, not a count: the identity claim speaks only about the kinds
|
|
@@ -17146,7 +17554,8 @@ function buildServer(opts) {
|
|
|
17146
17554
|
if (parentId && core.store.getNode(String(parentId))) {
|
|
17147
17555
|
const previewReshape = req.body.reshape;
|
|
17148
17556
|
const edit = await compileEditBrief(brand.id, String(parentId), brief, engine.capabilities(), {
|
|
17149
|
-
reshape: previewReshape === "extend" ? "extend" : previewReshape === "crop" ? "crop" : void 0
|
|
17557
|
+
reshape: previewReshape === "extend" ? "extend" : previewReshape === "crop" ? "crop" : void 0,
|
|
17558
|
+
source: req.body.sourceImage
|
|
17150
17559
|
});
|
|
17151
17560
|
const { referenceImages: referenceImages2, ...rest2 } = edit.compiled;
|
|
17152
17561
|
return {
|
|
@@ -17446,6 +17855,8 @@ function buildServer(opts) {
|
|
|
17446
17855
|
let { width = 1024, height = 1024 } = req.body;
|
|
17447
17856
|
const project = core.store.getProject(String(projectId));
|
|
17448
17857
|
if (!project) return reply.status(404).send({ error: "project not found" });
|
|
17858
|
+
if (parentId && core.store.getNode(String(parentId))?.projectId !== project.id)
|
|
17859
|
+
return reply.status(404).send({ error: "That shot is no longer there." });
|
|
17449
17860
|
const rawReshape = req.body.reshape ?? req.body.brief?.reshape;
|
|
17450
17861
|
const reshape = rawReshape === "crop" ? "crop" : rawReshape === "extend" ? "extend" : void 0;
|
|
17451
17862
|
const runCropNode = async (args) => {
|
|
@@ -17476,7 +17887,8 @@ function buildServer(opts) {
|
|
|
17476
17887
|
void runNode([node2.id], null, 0, work2, { width: plan2.width, height: plan2.height }).catch(
|
|
17477
17888
|
(err) => app.log.error({ err }, "crop run failed")
|
|
17478
17889
|
);
|
|
17479
|
-
|
|
17890
|
+
const sent2 = core.store.getNode(node2.id) ?? node2;
|
|
17891
|
+
return reply.status(202).send(args.note ? { ...sent2, warnings: [args.note] } : sent2);
|
|
17480
17892
|
};
|
|
17481
17893
|
if (kind === "edit" && reshape === "crop") {
|
|
17482
17894
|
const rootForCrop = core.store.rootFor(project.id);
|
|
@@ -17533,7 +17945,8 @@ function buildServer(opts) {
|
|
|
17533
17945
|
if (briefErrors.length) return reply.status(400).send({ error: `invalid brief: ${briefErrors.join("; ")}` });
|
|
17534
17946
|
if (kind === "edit") {
|
|
17535
17947
|
const edit = await compileEditBrief(project.brandId, resolvedParentId, brief, engine.capabilities(), {
|
|
17536
|
-
reshape
|
|
17948
|
+
reshape,
|
|
17949
|
+
source: req.body.sourceImage
|
|
17537
17950
|
});
|
|
17538
17951
|
compiled2 = edit.compiled;
|
|
17539
17952
|
inheritedTokens = edit.inheritedTokens;
|
|
@@ -17659,13 +18072,13 @@ function buildServer(opts) {
|
|
|
17659
18072
|
const sentRefs = keptRefs && maxEdge ? await Promise.all(keptRefs.map((p) => capReferenceEdge(core, p, maxEdge))) : keptRefs;
|
|
17660
18073
|
const sentRoles = referenceRoles && cap2 > 0 ? referenceRoles.slice(0, cap2) : referenceRoles ?? [];
|
|
17661
18074
|
if (process.env.SCENRI_DEBUG) {
|
|
17662
|
-
const
|
|
17663
|
-
for (const r of sentRoles)
|
|
18075
|
+
const sent2 = {};
|
|
18076
|
+
for (const r of sentRoles) sent2[r] = (sent2[r] ?? 0) + 1;
|
|
17664
18077
|
app.log.info(
|
|
17665
18078
|
{
|
|
17666
18079
|
engine: engine.capabilities().id,
|
|
17667
18080
|
cap: cap2,
|
|
17668
|
-
sent,
|
|
18081
|
+
sent: sent2,
|
|
17669
18082
|
dropped: (compiled2?.dropped ?? []).map((d) => `${d.role}:${d.label} (${d.reason ?? "budget"})`)
|
|
17670
18083
|
},
|
|
17671
18084
|
"reference transport"
|
|
@@ -17738,14 +18151,14 @@ function buildServer(opts) {
|
|
|
17738
18151
|
const editEdge = engine.capabilities().maxReferenceEdge;
|
|
17739
18152
|
if (editEdge) for (const r of editRefs) r.path = await capReferenceEdge(core, r.path, editEdge);
|
|
17740
18153
|
if (process.env.SCENRI_DEBUG) {
|
|
17741
|
-
const
|
|
17742
|
-
for (const r of editRefs)
|
|
18154
|
+
const sent2 = {};
|
|
18155
|
+
for (const r of editRefs) sent2[String(r.role ?? "reference")] = (sent2[String(r.role ?? "reference")] ?? 0) + 1;
|
|
17743
18156
|
app.log.info(
|
|
17744
18157
|
{
|
|
17745
18158
|
engine: engine.capabilities().id,
|
|
17746
18159
|
cap: Math.max(0, engine.capabilities().maxReferenceImages - 1),
|
|
17747
18160
|
sourceFrame: true,
|
|
17748
|
-
sent,
|
|
18161
|
+
sent: sent2,
|
|
17749
18162
|
dropped: (mergedEdit?.dropped ?? []).map((d) => `${d.role}:${d.label} (${d.reason ?? "budget"})`)
|
|
17750
18163
|
},
|
|
17751
18164
|
"reference transport"
|
|
@@ -18054,10 +18467,10 @@ function buildServer(opts) {
|
|
|
18054
18467
|
} : kind === "edit" && original ? async (images) => {
|
|
18055
18468
|
let staged = images;
|
|
18056
18469
|
if (gradeOnlyAsk && !expandPlan) {
|
|
18057
|
-
const
|
|
18470
|
+
const sent2 = budgetSourceHash ? core.images.read(budgetSourceHash) : original;
|
|
18058
18471
|
const out = [];
|
|
18059
18472
|
for (const h of staged) {
|
|
18060
|
-
const g = await gradeComposite(original,
|
|
18473
|
+
const g = await gradeComposite(original, sent2, core.images.read(h));
|
|
18061
18474
|
if (g) {
|
|
18062
18475
|
app.log.info(
|
|
18063
18476
|
{ nodeId: node.id, residual: Number(g.residual.toFixed(2)) },
|
|
@@ -18102,14 +18515,16 @@ function buildServer(opts) {
|
|
|
18102
18515
|
nodeBudgetMs
|
|
18103
18516
|
).catch((err) => app.log.error({ err }, "node run failed"));
|
|
18104
18517
|
const allWarnings = [...compiled2?.warnings ?? [], ...extraWarnings];
|
|
18105
|
-
|
|
18518
|
+
const sent = nodes.map((n) => core.store.getNode(n.id) ?? n);
|
|
18519
|
+
return reply.status(202).send({ ...sent[0], siblings: sent, ...allWarnings.length ? { warnings: allWarnings } : {} });
|
|
18106
18520
|
};
|
|
18107
18521
|
app.post("/api/nodes", async (req, reply) => startNodeRun(req, reply));
|
|
18108
18522
|
app.post("/api/nodes/:id/retry", async (req, reply) => {
|
|
18109
18523
|
const id = req.params.id;
|
|
18110
18524
|
const n = core.store.getNode(id);
|
|
18111
18525
|
if (!n) return reply.status(404).send({ error: "node not found" });
|
|
18112
|
-
if (n.status === "running"
|
|
18526
|
+
if (n.status === "running" || runningGenerations.has(id))
|
|
18527
|
+
return reply.status(409).send({ error: "already running" });
|
|
18113
18528
|
if (n.status === "done") return reply.status(400).send({ error: "finished shots start a new take" });
|
|
18114
18529
|
if (n.status !== "error" && n.status !== "cancelled") {
|
|
18115
18530
|
return reply.status(400).send({ error: "cannot retry this shot" });
|
|
@@ -18231,7 +18646,7 @@ function buildServer(opts) {
|
|
|
18231
18646
|
})();
|
|
18232
18647
|
return drained;
|
|
18233
18648
|
});
|
|
18234
|
-
registerSystemRoutes(app, { core, thumbs });
|
|
18649
|
+
registerSystemRoutes(app, { core, thumbs, stopAllRuns: () => stopRuns(() => true) });
|
|
18235
18650
|
registerPhoneRoutes(app, { phone });
|
|
18236
18651
|
registerGuideRoutes(app, { core, version: meta.version });
|
|
18237
18652
|
registerDesktopRoutes(app, {
|
|
@@ -18331,6 +18746,12 @@ async function serve() {
|
|
|
18331
18746
|
}
|
|
18332
18747
|
}
|
|
18333
18748
|
async function run3() {
|
|
18749
|
+
const supervised = process.env.SCENRI_SUPERVISED === "1";
|
|
18750
|
+
const installKind = detectInstallKind(fileURLToPath(import.meta.url), defaultHome());
|
|
18751
|
+
const theirs = await runningScenri();
|
|
18752
|
+
if (theirs && shouldAdoptRunning({ installKind, ourHome: defaultHome(), theirHome: theirs.home })) {
|
|
18753
|
+
await adoptRunning();
|
|
18754
|
+
}
|
|
18334
18755
|
const core = createCore();
|
|
18335
18756
|
const stubs = process.env.SCENRI_DEMO_ENGINE === "1" ? [createDemoEngine((b) => core.images.save(b), demoOptionsFromEnv(process.env))] : [];
|
|
18336
18757
|
const engines = createEngineRegistry(core, stubs);
|
|
@@ -18343,8 +18764,6 @@ async function run3() {
|
|
|
18343
18764
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
18344
18765
|
const candidates = [join(here, "..", "..", "..", "apps", "studio", "dist"), join(here, "..", "studio-dist")];
|
|
18345
18766
|
const studioDist = candidates.find((p) => existsSync(p));
|
|
18346
|
-
const supervised = process.env.SCENRI_SUPERVISED === "1";
|
|
18347
|
-
const installKind = detectInstallKind(fileURLToPath(import.meta.url), core.home);
|
|
18348
18767
|
const named = isLoopbackName(HOST) || isWildcardHost(HOST) || isIPv4Literal(HOST) ? [] : [HOST];
|
|
18349
18768
|
const app = buildServer({
|
|
18350
18769
|
core,
|
|
@@ -18371,34 +18790,17 @@ async function run3() {
|
|
|
18371
18790
|
await new Promise((r) => setTimeout(r, 300));
|
|
18372
18791
|
continue;
|
|
18373
18792
|
}
|
|
18374
|
-
|
|
18375
|
-
|
|
18376
|
-
|
|
18377
|
-
|
|
18378
|
-
|
|
18379
|
-
|
|
18380
|
-
if (!shouldAdoptRunning({ installKind, ourHome: core.home, theirHome: info.home })) {
|
|
18381
|
-
console.error("");
|
|
18382
|
-
for (const line of anotherScenriLines(PORT, info.home ?? "another library", core.home)) {
|
|
18383
|
-
console.error(` ${line}`);
|
|
18384
|
-
}
|
|
18385
|
-
console.error("");
|
|
18386
|
-
process.exit(1);
|
|
18793
|
+
const theirs2 = await runningScenri();
|
|
18794
|
+
if (theirs2) {
|
|
18795
|
+
if (!shouldAdoptRunning({ installKind, ourHome: core.home, theirHome: theirs2.home })) {
|
|
18796
|
+
console.error("");
|
|
18797
|
+
for (const line of anotherScenriLines(PORT, theirs2.home ?? "another library", core.home)) {
|
|
18798
|
+
console.error(` ${line}`);
|
|
18387
18799
|
}
|
|
18388
|
-
|
|
18389
|
-
|
|
18390
|
-
Scenri is already running \u2192 ${url}
|
|
18391
|
-
`);
|
|
18392
|
-
if (process.env.SCENRI_NO_OPEN !== "1") {
|
|
18393
|
-
try {
|
|
18394
|
-
const { default: open } = await import('open');
|
|
18395
|
-
await open(url);
|
|
18396
|
-
} catch {
|
|
18397
|
-
}
|
|
18398
|
-
}
|
|
18399
|
-
process.exit(0);
|
|
18800
|
+
console.error("");
|
|
18801
|
+
process.exit(1);
|
|
18400
18802
|
}
|
|
18401
|
-
|
|
18803
|
+
await adoptRunning();
|
|
18402
18804
|
}
|
|
18403
18805
|
console.error("");
|
|
18404
18806
|
for (const line of portBusyLines(PORT)) console.error(` ${line}`);
|
|
@@ -18412,13 +18814,14 @@ async function run3() {
|
|
|
18412
18814
|
const shutdown = (signal) => {
|
|
18413
18815
|
if (closing2) return;
|
|
18414
18816
|
closing2 = true;
|
|
18415
|
-
console.log(`
|
|
18817
|
+
if (signal !== "SIGHUP") console.log(`
|
|
18416
18818
|
${signal}: closing Scenri\u2026`);
|
|
18417
18819
|
setTimeout(() => process.exit(1), 5e3).unref();
|
|
18418
18820
|
app.drain().then(() => process.exit(0)).catch(() => process.exit(1));
|
|
18419
18821
|
};
|
|
18420
18822
|
process.on("SIGINT", shutdown);
|
|
18421
18823
|
process.on("SIGTERM", shutdown);
|
|
18824
|
+
process.on("SIGHUP", shutdown);
|
|
18422
18825
|
void repairPresenterCrops(core, (line) => console.log(line)).catch(() => void 0);
|
|
18423
18826
|
app.updates.schedule();
|
|
18424
18827
|
app.content.schedule();
|
|
@@ -18452,7 +18855,7 @@ async function run3() {
|
|
|
18452
18855
|
}
|
|
18453
18856
|
}
|
|
18454
18857
|
const ownEntry = fileURLToPath(import.meta.url);
|
|
18455
|
-
const { addToDesktop, installDeps } = await import('./cli-
|
|
18858
|
+
const { addToDesktop, installDeps } = await import('./cli-WRNWT6P7.js');
|
|
18456
18859
|
const { refreshLauncher } = await import('./refresh-5HBDLLLB.js');
|
|
18457
18860
|
const { askOnTerminal, offerDesktop, shouldOfferDesktop } = await import('./offer-W6NJXMWT.js');
|
|
18458
18861
|
const { launcherInstalled } = await import('./paths-6ZXRZUHY.js');
|
|
@@ -18494,6 +18897,29 @@ async function verify() {
|
|
|
18494
18897
|
process.exit(1);
|
|
18495
18898
|
}
|
|
18496
18899
|
}
|
|
18900
|
+
async function runningScenri() {
|
|
18901
|
+
try {
|
|
18902
|
+
const res = await fetch(`http://127.0.0.1:${PORT}/api/version`, { signal: AbortSignal.timeout(2e3) });
|
|
18903
|
+
const info = await res.json();
|
|
18904
|
+
return info.name === readMeta().name ? info : null;
|
|
18905
|
+
} catch {
|
|
18906
|
+
return null;
|
|
18907
|
+
}
|
|
18908
|
+
}
|
|
18909
|
+
async function adoptRunning() {
|
|
18910
|
+
const url = `http://127.0.0.1:${PORT}`;
|
|
18911
|
+
console.log(`
|
|
18912
|
+
Scenri is already running \u2192 ${url}
|
|
18913
|
+
`);
|
|
18914
|
+
if (process.env.SCENRI_NO_OPEN !== "1") {
|
|
18915
|
+
try {
|
|
18916
|
+
const { default: open } = await import('open');
|
|
18917
|
+
await open(url);
|
|
18918
|
+
} catch {
|
|
18919
|
+
}
|
|
18920
|
+
}
|
|
18921
|
+
process.exit(0);
|
|
18922
|
+
}
|
|
18497
18923
|
|
|
18498
18924
|
export { serve, verify };
|
|
18499
18925
|
//# sourceMappingURL=serve.js.map
|