scenri 0.19.0 → 0.20.0
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 +81 -0
- package/README.md +3 -3
- 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 +736 -258
- 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-Bf4EHKis.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-BPTXm681.js +0 -108
package/dist/serve.js
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
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
|
-
import { readFile, stat, copyFile, lstat, readdir, mkdtemp, rm, access, rename, unlink, writeFile } from 'fs/promises';
|
|
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';
|
|
15
15
|
import sharp22 from 'sharp';
|
|
16
16
|
import Fastify from 'fastify';
|
|
@@ -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 };
|
|
@@ -829,12 +910,15 @@ function execArgs(dir, effort = "low") {
|
|
|
829
910
|
CODEX_MODEL,
|
|
830
911
|
"-c",
|
|
831
912
|
`model_reasoning_effort="${effort}"`,
|
|
913
|
+
// No session files: codex would otherwise keep every run's transcript, the
|
|
914
|
+
// pictures in it as base64, long after Scenri has let the work go.
|
|
915
|
+
"--ephemeral",
|
|
832
916
|
"-C",
|
|
833
917
|
dir,
|
|
834
918
|
"-"
|
|
835
919
|
];
|
|
836
920
|
}
|
|
837
|
-
function
|
|
921
|
+
function extensionsOffArgs(env) {
|
|
838
922
|
const user = env.HOME || env.USERPROFILE;
|
|
839
923
|
const home = env.CODEX_HOME || (user ? join(user, ".codex") : "");
|
|
840
924
|
if (!home) return [];
|
|
@@ -844,12 +928,12 @@ function mcpOffArgs(env) {
|
|
|
844
928
|
} catch {
|
|
845
929
|
return [];
|
|
846
930
|
}
|
|
847
|
-
const
|
|
931
|
+
const keys = /* @__PURE__ */ new Set();
|
|
848
932
|
for (const line of text.split(/\r?\n/)) {
|
|
849
|
-
const m = /^\s*\[\s*mcp_servers\.(?:"([A-Za-z0-9_
|
|
850
|
-
if (m)
|
|
933
|
+
const m = /^\s*\[\s*(mcp_servers|plugins)\.(?:"([A-Za-z0-9_@-]+)"|([A-Za-z0-9_@-]+))\s*\]\s*(?:#.*)?$/.exec(line);
|
|
934
|
+
if (m) keys.add(`${m[1]}.${m[2] ?? m[3]}`);
|
|
851
935
|
}
|
|
852
|
-
return [...
|
|
936
|
+
return [...keys].flatMap((key) => ["-c", `${key}.enabled=false`]);
|
|
853
937
|
}
|
|
854
938
|
var TAIL_BYTES = 4096;
|
|
855
939
|
var DETAIL_CHARS = 800;
|
|
@@ -939,12 +1023,13 @@ function createRunner(opts = {}) {
|
|
|
939
1023
|
const spawnCodex = (exe, args, stdinOpen) => {
|
|
940
1024
|
const stdio = [stdinOpen ? "pipe" : "ignore", "pipe", "pipe"];
|
|
941
1025
|
const env = buildChildEnv(parentEnv, [...ignoreEnvKeys(), ...SIBLING_PROVIDER_KEYS]);
|
|
942
|
-
if (args[0] === "exec") args = ["exec", ...
|
|
1026
|
+
if (args[0] === "exec") args = ["exec", ...extensionsOffArgs(parentEnv), ...args.slice(1)];
|
|
943
1027
|
return exe.direct ? spawnImpl(exe.command, args, { stdio, env, ...platform !== "win32" ? { detached: true } : {} }) : spawnImpl([exe.command, ...args.map(winArg)].join(" "), [], { stdio, env, shell: true });
|
|
944
1028
|
};
|
|
945
1029
|
async function run4(args, signal, io) {
|
|
946
1030
|
const exe = await resolution();
|
|
947
1031
|
const timeoutMs = io?.timeoutMs ?? defaultTimeoutMs;
|
|
1032
|
+
const launchedAs = args[0] === "exec" ? fingerprintFor(exe, knownVersion) : null;
|
|
948
1033
|
return new Promise((resolve, reject) => {
|
|
949
1034
|
let child;
|
|
950
1035
|
try {
|
|
@@ -1028,7 +1113,11 @@ function createRunner(opts = {}) {
|
|
|
1028
1113
|
});
|
|
1029
1114
|
child.on("exit", (code) => {
|
|
1030
1115
|
if (code === 0) {
|
|
1031
|
-
finish("ok",
|
|
1116
|
+
finish("ok", () => {
|
|
1117
|
+
if (launchedAs && resolved && launchedAs === fingerprintFor(resolved, knownVersion))
|
|
1118
|
+
noteConnection("proven");
|
|
1119
|
+
resolve();
|
|
1120
|
+
});
|
|
1032
1121
|
return;
|
|
1033
1122
|
}
|
|
1034
1123
|
const limit = /You've hit your usage limit\.(?:[^\n]*?\btry again at ([^.\n]+)\.)?/.exec(stderr);
|
|
@@ -1263,8 +1352,9 @@ function createRunner(opts = {}) {
|
|
|
1263
1352
|
if (outcome === "unproven") return;
|
|
1264
1353
|
const exe = resolved;
|
|
1265
1354
|
if (!exe) return;
|
|
1355
|
+
const wasRefused = connCache?.outcome === "refused";
|
|
1266
1356
|
connCache = { outcome, failure, at: Date.now(), fingerprint: fingerprintFor(exe, knownVersion) };
|
|
1267
|
-
cached = null;
|
|
1357
|
+
if (outcome === "refused" || wasRefused) cached = null;
|
|
1268
1358
|
}
|
|
1269
1359
|
function invalidateProbe() {
|
|
1270
1360
|
cached = null;
|
|
@@ -1354,7 +1444,7 @@ function createCodexAnalyzer(opts = {}) {
|
|
|
1354
1444
|
if (parsed.ok) return parsed.size;
|
|
1355
1445
|
problems = parsed.problems;
|
|
1356
1446
|
}
|
|
1357
|
-
|
|
1447
|
+
return null;
|
|
1358
1448
|
});
|
|
1359
1449
|
}
|
|
1360
1450
|
};
|
|
@@ -1716,9 +1806,43 @@ function createCodexEngine(opts) {
|
|
|
1716
1806
|
const buf = await readLeftFile(join(dir, name), name);
|
|
1717
1807
|
if (buf.length === 0) throw new Error(`codex: ${name} is empty`);
|
|
1718
1808
|
hashes.push(saveImage(buf));
|
|
1809
|
+
await forgetGenerated(buf);
|
|
1719
1810
|
}
|
|
1720
1811
|
return hashes;
|
|
1721
1812
|
}
|
|
1813
|
+
async function forgetGenerated(buf) {
|
|
1814
|
+
const home = generatedImagesDir();
|
|
1815
|
+
const recent = Date.now() - 30 * 6e4;
|
|
1816
|
+
let names;
|
|
1817
|
+
try {
|
|
1818
|
+
names = await readdir(home);
|
|
1819
|
+
} catch {
|
|
1820
|
+
return;
|
|
1821
|
+
}
|
|
1822
|
+
const dropIfSame = async (path) => {
|
|
1823
|
+
const st = await lstat(path);
|
|
1824
|
+
if (!st.isFile() || st.size !== buf.length) return false;
|
|
1825
|
+
if (!(await readFile(path)).equals(buf)) return false;
|
|
1826
|
+
await unlink(path);
|
|
1827
|
+
return true;
|
|
1828
|
+
};
|
|
1829
|
+
for (const name of names) {
|
|
1830
|
+
const at = join(home, name);
|
|
1831
|
+
try {
|
|
1832
|
+
const st = await lstat(at);
|
|
1833
|
+
if (st.mtimeMs < recent) continue;
|
|
1834
|
+
if (st.isFile()) {
|
|
1835
|
+
await dropIfSame(at);
|
|
1836
|
+
continue;
|
|
1837
|
+
}
|
|
1838
|
+
if (!st.isDirectory()) continue;
|
|
1839
|
+
let dropped = false;
|
|
1840
|
+
for (const file of await readdir(at)) dropped = await dropIfSame(join(at, file)) || dropped;
|
|
1841
|
+
if (dropped && (await readdir(at)).length === 0) await rmdir(at);
|
|
1842
|
+
} catch {
|
|
1843
|
+
}
|
|
1844
|
+
}
|
|
1845
|
+
}
|
|
1722
1846
|
async function recoverFromGenerated(before, claimed) {
|
|
1723
1847
|
const home = generatedImagesDir();
|
|
1724
1848
|
let names;
|
|
@@ -2003,6 +2127,7 @@ function demoOptionsFromEnv(env) {
|
|
|
2003
2127
|
if (env.SCENRI_DEMO_FAIL_EDIT === "1") out.failEdit = true;
|
|
2004
2128
|
const refs = Number(env.SCENRI_DEMO_REFS);
|
|
2005
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;
|
|
2006
2131
|
return out;
|
|
2007
2132
|
}
|
|
2008
2133
|
function sleep2(ms, signal) {
|
|
@@ -2028,7 +2153,21 @@ function createDemoEngine(saveImage, opts = {}) {
|
|
|
2028
2153
|
return hexes.length ? hexes : ["#4B5563", "#9CA3AF"];
|
|
2029
2154
|
};
|
|
2030
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
|
+
})();
|
|
2031
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
|
+
}
|
|
2032
2171
|
const c0 = colors[seed % colors.length];
|
|
2033
2172
|
const c1 = colors[(seed + 1) % colors.length] ?? "#111111";
|
|
2034
2173
|
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${h}">
|
|
@@ -4442,8 +4581,10 @@ function mergeScrape(existing, scraped) {
|
|
|
4442
4581
|
}
|
|
4443
4582
|
|
|
4444
4583
|
// src/assetRecords.ts
|
|
4584
|
+
var TYPED_TEXT_MAX = 4e3;
|
|
4585
|
+
var COMPOSED_TEXT_MAX = 8e3;
|
|
4445
4586
|
var IDENTITY_EDITS_MAX = 8;
|
|
4446
|
-
var IDENTITY_EDIT_CHARS =
|
|
4587
|
+
var IDENTITY_EDIT_CHARS = TYPED_TEXT_MAX;
|
|
4447
4588
|
var PRESENTER_SOURCES = ["synthetic", "photos"];
|
|
4448
4589
|
var LIKENESS_VERSION = "v1";
|
|
4449
4590
|
var PRESENTER_ID_PREFIX = "up";
|
|
@@ -4513,7 +4654,7 @@ function identityEditsOf(v) {
|
|
|
4513
4654
|
return out;
|
|
4514
4655
|
}
|
|
4515
4656
|
var SUBJECTS2 = /* @__PURE__ */ new Set(["product", "person", "either"]);
|
|
4516
|
-
var SCENE_INSTRUCTION_MAX =
|
|
4657
|
+
var SCENE_INSTRUCTION_MAX = COMPOSED_TEXT_MAX;
|
|
4517
4658
|
function sceneRecordFrom(input, base) {
|
|
4518
4659
|
const has = (k) => input[k] !== void 0;
|
|
4519
4660
|
const prompt = has("prompt") ? phrase(input.prompt, 2e3) : base?.prompt ?? "";
|
|
@@ -4966,21 +5107,31 @@ var toMarkPng = async (buf) => {
|
|
|
4966
5107
|
}
|
|
4967
5108
|
return out;
|
|
4968
5109
|
};
|
|
5110
|
+
var SENDABLE_FORMATS = /* @__PURE__ */ new Set(["png", "jpeg", "webp", "gif"]);
|
|
4969
5111
|
var cappedRefs = /* @__PURE__ */ new Map();
|
|
4970
5112
|
async function capReferenceEdge(core, path, maxEdge) {
|
|
4971
5113
|
const key = `${path}#${maxEdge}`;
|
|
4972
5114
|
const hit = cappedRefs.get(key);
|
|
4973
5115
|
if (hit && existsSync(hit)) return hit;
|
|
4974
|
-
let
|
|
5116
|
+
let format;
|
|
5117
|
+
let buf = null;
|
|
4975
5118
|
try {
|
|
4976
5119
|
const meta = await sharp22(path).metadata();
|
|
4977
|
-
|
|
4978
|
-
|
|
4979
|
-
|
|
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();
|
|
4980
5124
|
}
|
|
4981
5125
|
} catch {
|
|
4982
|
-
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
|
+
);
|
|
4983
5133
|
}
|
|
5134
|
+
const out = buf ? core.images.pathFor(core.images.save(buf)) : path;
|
|
4984
5135
|
cappedRefs.set(key, out);
|
|
4985
5136
|
return out;
|
|
4986
5137
|
}
|
|
@@ -5253,8 +5404,11 @@ function productEditFidelityDirective(name) {
|
|
|
5253
5404
|
function characterEditIdentityDirective(name) {
|
|
5254
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.`;
|
|
5255
5406
|
}
|
|
5256
|
-
function referenceIdentityGuard() {
|
|
5257
|
-
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.";
|
|
5258
5412
|
}
|
|
5259
5413
|
function referenceProductGuard() {
|
|
5260
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.";
|
|
@@ -5832,6 +5986,22 @@ function compileBrief(input, ctx) {
|
|
|
5832
5986
|
);
|
|
5833
5987
|
}
|
|
5834
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);
|
|
5835
6005
|
const max = ctx.engineCaps.maxReferenceImages;
|
|
5836
6006
|
const {
|
|
5837
6007
|
kept,
|
|
@@ -5924,7 +6094,7 @@ function compileBrief(input, ctx) {
|
|
|
5924
6094
|
const rec = (ctx.brand?.products ?? []).find((x) => x?.id === a.id);
|
|
5925
6095
|
return String(rec?.category ?? "").toLowerCase() === "apparel";
|
|
5926
6096
|
}) ? [garmentDisplayDirective()] : [];
|
|
5927
|
-
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)] : [];
|
|
5928
6098
|
const framed = ctx.mode !== "edit" && kept.some((a) => a.role === "reference" && frameRefs.has(a.hash));
|
|
5929
6099
|
const refProductGuard = ctx.mode !== "edit" && productId && kept.some((a) => a.role === "reference") ? [referenceProductGuard()] : [];
|
|
5930
6100
|
const allDirectives = [
|
|
@@ -6034,6 +6204,10 @@ function shotWordsFor(core, brandId) {
|
|
|
6034
6204
|
}
|
|
6035
6205
|
var CONTENT_VERSION = 3;
|
|
6036
6206
|
var CONTENT_TAG = "content-v3";
|
|
6207
|
+
var CONTENT_SHA256 = "c736f1f9142bc428e19bb80bc139c2d9e60a9dce7449b322760ffe8df333b734";
|
|
6208
|
+
function archiveMatches(bytes, expected = CONTENT_SHA256) {
|
|
6209
|
+
return createHash("sha256").update(bytes).digest("hex") === expected;
|
|
6210
|
+
}
|
|
6037
6211
|
var DEFAULT_CONTENT_URL = `https://github.com/tonygorb/scenri/releases/download/${CONTENT_TAG}/scenri-content.zip`;
|
|
6038
6212
|
var TIMEOUT_MS = 30 * 60 * 1e3;
|
|
6039
6213
|
function resolveContentUrl(env = process.env, override) {
|
|
@@ -6085,8 +6259,12 @@ function createContentFetcher(deps) {
|
|
|
6085
6259
|
const doFetch = deps.fetchImpl ?? fetch;
|
|
6086
6260
|
const url = resolveContentUrl(env, deps.url);
|
|
6087
6261
|
const custom = Boolean(deps.url ?? env.SCENRI_CONTENT_URL);
|
|
6262
|
+
const expected = deps.sha256 !== void 0 ? deps.sha256 : custom ? null : CONTENT_SHA256;
|
|
6088
6263
|
const enabled = () => env.SCENRI_NO_CONTENT_FETCH !== "1" && deps.store.getSetting("content.enabled") !== "false";
|
|
6089
6264
|
let inflight = null;
|
|
6265
|
+
let gaveUp = false;
|
|
6266
|
+
let installs = 0;
|
|
6267
|
+
let stale = null;
|
|
6090
6268
|
async function download() {
|
|
6091
6269
|
const root = contentCacheRoot(env);
|
|
6092
6270
|
if (!deps.store.getSetting("content.disclosed")) {
|
|
@@ -6105,6 +6283,13 @@ function createContentFetcher(deps) {
|
|
|
6105
6283
|
} finally {
|
|
6106
6284
|
clearTimeout(timer);
|
|
6107
6285
|
}
|
|
6286
|
+
if (expected && !archiveMatches(bytes, expected)) {
|
|
6287
|
+
return {
|
|
6288
|
+
ok: false,
|
|
6289
|
+
updated: false,
|
|
6290
|
+
error: "the library download does not match the archive this version expects"
|
|
6291
|
+
};
|
|
6292
|
+
}
|
|
6108
6293
|
const refused = await installContentArchive(bytes, root);
|
|
6109
6294
|
if (refused) return { ok: false, updated: false, error: refused };
|
|
6110
6295
|
try {
|
|
@@ -6121,15 +6306,25 @@ function createContentFetcher(deps) {
|
|
|
6121
6306
|
async function ensure() {
|
|
6122
6307
|
if (!enabled()) return { ok: false, updated: false, error: null };
|
|
6123
6308
|
if (!contentCacheStale(env, custom)) return { ok: true, updated: false, error: null };
|
|
6124
|
-
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(() => {
|
|
6125
6314
|
inflight = null;
|
|
6315
|
+
stale = null;
|
|
6126
6316
|
});
|
|
6127
6317
|
return inflight;
|
|
6128
6318
|
}
|
|
6319
|
+
function state() {
|
|
6320
|
+
if (!enabled() || gaveUp) return { arriving: false, installs };
|
|
6321
|
+
stale ??= contentCacheStale(env, custom);
|
|
6322
|
+
return { arriving: stale, installs };
|
|
6323
|
+
}
|
|
6129
6324
|
function schedule() {
|
|
6130
6325
|
setTimeout(() => void ensure(), 3e3).unref();
|
|
6131
6326
|
}
|
|
6132
|
-
return { enabled, ensure, schedule };
|
|
6327
|
+
return { enabled, ensure, schedule, state };
|
|
6133
6328
|
}
|
|
6134
6329
|
async function inspectMark(buf, toPng2) {
|
|
6135
6330
|
const png = await toPng2(buf);
|
|
@@ -7621,7 +7816,8 @@ async function discoverCatalog(opts) {
|
|
|
7621
7816
|
const estimatedTotal = discovered.estimatedTotal ?? discovered.productUrls.length;
|
|
7622
7817
|
let empty = null;
|
|
7623
7818
|
if (!estimatedTotal) {
|
|
7624
|
-
|
|
7819
|
+
const unreachable = await neverAnswered(baseUrl, fetchImpl, opts.signal);
|
|
7820
|
+
empty = unreachable ? { code: "unreachable", message: unreachable, retryable: true } : {
|
|
7625
7821
|
code: "empty_catalog",
|
|
7626
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.`
|
|
7627
7823
|
};
|
|
@@ -7653,6 +7849,17 @@ async function discoverCatalog(opts) {
|
|
|
7653
7849
|
}
|
|
7654
7850
|
};
|
|
7655
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
|
+
}
|
|
7656
7863
|
|
|
7657
7864
|
// ../catalog/src/robots.ts
|
|
7658
7865
|
var ALLOW_ALL = { rules: [], crawlDelayMs: 0 };
|
|
@@ -7951,6 +8158,16 @@ async function runJob(deps, jobId, brandId, url, signal, only) {
|
|
|
7951
8158
|
platform = found.detection.platform;
|
|
7952
8159
|
discovered = found.estimatedTotal;
|
|
7953
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
|
+
}
|
|
7954
8171
|
if (found.empty || !found.estimatedTotal) {
|
|
7955
8172
|
const source = core.catalog.upsertSource(brandId, found.baseUrl, platform);
|
|
7956
8173
|
patch3({ sourceId: source.id, platform });
|
|
@@ -9781,13 +9998,17 @@ function resolveSize(dimensions, stored2) {
|
|
|
9781
9998
|
}
|
|
9782
9999
|
async function drawAtScale(o) {
|
|
9783
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
|
+
);
|
|
9784
10005
|
const plates = {
|
|
9785
10006
|
prompt: platePrompt({ span, light: o.plan.light, shot: o.plan.shot }),
|
|
9786
10007
|
brand: o.brand,
|
|
9787
10008
|
width: o.width,
|
|
9788
10009
|
height: o.height,
|
|
9789
10010
|
count: o.count,
|
|
9790
|
-
referenceImages: [
|
|
10011
|
+
referenceImages: [scenePath],
|
|
9791
10012
|
referenceRoles: ["scene"]
|
|
9792
10013
|
};
|
|
9793
10014
|
const instruction = placeInstruction({
|
|
@@ -9827,7 +10048,7 @@ async function drawAtScale(o) {
|
|
|
9827
10048
|
instruction,
|
|
9828
10049
|
sourceImage: o.images.pathFor(plate),
|
|
9829
10050
|
brand: o.brand,
|
|
9830
|
-
referenceImages: [
|
|
10051
|
+
referenceImages: [productPath],
|
|
9831
10052
|
referenceRoles: ["product"],
|
|
9832
10053
|
width: o.width,
|
|
9833
10054
|
height: o.height
|
|
@@ -9869,17 +10090,28 @@ async function drawAtScale(o) {
|
|
|
9869
10090
|
raw: { requested: o.count, variantIndexes: slots, partialFailures: reasons }
|
|
9870
10091
|
};
|
|
9871
10092
|
}
|
|
9872
|
-
|
|
9873
|
-
|
|
9874
|
-
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) {
|
|
9875
10096
|
const reading = /* @__PURE__ */ new Map();
|
|
9876
10097
|
const stored2 = (brandId, id) => readSize(core.store.getSetting(sizeKey(brandId, id)));
|
|
9877
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
|
+
};
|
|
9878
10108
|
return {
|
|
9879
10109
|
known,
|
|
9880
10110
|
async ensure(brandId, p, signal) {
|
|
9881
10111
|
const now2 = known(brandId, p);
|
|
9882
10112
|
if (now2 || !p.photo) return now2;
|
|
10113
|
+
const name = basename(p.photo);
|
|
10114
|
+
if (missed(brandId, p.id, name)) return null;
|
|
9883
10115
|
const key = sizeKey(brandId, p.id);
|
|
9884
10116
|
let read2 = reading.get(key);
|
|
9885
10117
|
if (!read2) {
|
|
@@ -9887,13 +10119,21 @@ function createProductSizes(core, reader) {
|
|
|
9887
10119
|
read2 = (async () => {
|
|
9888
10120
|
const analyzer = await reader();
|
|
9889
10121
|
if (!analyzer?.measure) return null;
|
|
9890
|
-
|
|
9891
|
-
|
|
9892
|
-
|
|
9893
|
-
|
|
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
|
+
}
|
|
9894
10131
|
const n = Number(got?.largestCm);
|
|
9895
10132
|
const text = String(got?.text ?? "").trim();
|
|
9896
|
-
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
|
+
}
|
|
9897
10137
|
const size = { text, largestCm: n, by: "estimate" };
|
|
9898
10138
|
core.store.setSetting(key, JSON.stringify(size));
|
|
9899
10139
|
return size;
|
|
@@ -10886,12 +11126,13 @@ function registerLogoRoutes(app, deps) {
|
|
|
10886
11126
|
return allowed.includes(v) ? v : null;
|
|
10887
11127
|
};
|
|
10888
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 });
|
|
10889
11132
|
const brand = core.store.getBrand(req.params.id);
|
|
10890
11133
|
if (!brand) return reply.status(404).send({ error: "brand not found" });
|
|
10891
11134
|
const json = { ...brand.json };
|
|
10892
11135
|
const logos = [...readLogos(json)];
|
|
10893
|
-
const part = await readImagePart(core, req, toMarkPng);
|
|
10894
|
-
if ("error" in part) return reply.status(400).send({ error: part.error });
|
|
10895
11136
|
const existing = logos.findIndex((l) => assetHash(l?.file) === part.hash);
|
|
10896
11137
|
const role = enumField(
|
|
10897
11138
|
part.fields?.role?.value,
|
|
@@ -11594,8 +11835,8 @@ function sweepPresenterDrafts(core) {
|
|
|
11594
11835
|
}
|
|
11595
11836
|
return swept;
|
|
11596
11837
|
}
|
|
11597
|
-
var KEEP_ITEM_CHARS =
|
|
11598
|
-
var SETUP_CHARS =
|
|
11838
|
+
var KEEP_ITEM_CHARS = TYPED_TEXT_MAX;
|
|
11839
|
+
var SETUP_CHARS = 64 * TYPED_TEXT_MAX;
|
|
11599
11840
|
var KEEP_ITEMS_MAX = 12;
|
|
11600
11841
|
var SOURCES_MAX = 8;
|
|
11601
11842
|
function keepItemsOf(raw, has) {
|
|
@@ -11637,18 +11878,18 @@ async function createPresenterDraft(deps, input) {
|
|
|
11637
11878
|
const brand = core.store.getBrand(input.brandId);
|
|
11638
11879
|
if (!brand) throw fail("brand not found", 404);
|
|
11639
11880
|
const source = input.source === "synthetic" ? "synthetic" : "photos";
|
|
11640
|
-
const direction = str4(input.direction,
|
|
11881
|
+
const direction = str4(input.direction, COMPOSED_TEXT_MAX);
|
|
11641
11882
|
const keep = str4(input.keep, 240);
|
|
11642
11883
|
const keepItems = keepItemsOf(input.keepItems, (h) => core.images.has(h));
|
|
11643
11884
|
const sources = [
|
|
11644
11885
|
...new Set((input.imageHashes ?? []).map(String).filter((h) => HASH.test(h) && core.images.has(h)))
|
|
11645
11886
|
].slice(0, SOURCES_MAX);
|
|
11646
11887
|
if (source === "synthetic" && !direction) throw fail("describe who they are in a sentence", 400);
|
|
11647
|
-
if (source === "synthetic" && !deps.engine) throw fail("no engine here can draw a person", 400);
|
|
11648
11888
|
if (source === "photos" && !sources.length) throw fail("add at least one photo of this person", 400);
|
|
11649
11889
|
if (source === "photos" && !input.attestation) {
|
|
11650
11890
|
throw fail("confirm you have permission to use this person's likeness", 400);
|
|
11651
11891
|
}
|
|
11892
|
+
if (!deps.engine) throw fail("no engine here can draw a person", 400);
|
|
11652
11893
|
if (source === "photos" && closing) throw restarting();
|
|
11653
11894
|
const clientKey = str4(input.clientKey, 80);
|
|
11654
11895
|
const made = clientKey ? listPresenterDrafts(core, brand.id).find(
|
|
@@ -11916,7 +12157,7 @@ async function generateView(deps, id, view, opts = {}) {
|
|
|
11916
12157
|
if (closing) throw restarting();
|
|
11917
12158
|
if (isExtra(view) && !rec.extras) throw fail("extra views are built on request", 400);
|
|
11918
12159
|
if (rec.source === "synthetic" && !rec.direction?.trim()) {
|
|
11919
|
-
const said = str4(opts.adjustment,
|
|
12160
|
+
const said = str4(opts.adjustment, TYPED_TEXT_MAX);
|
|
11920
12161
|
if (!said) throw fail("describe who they are in a sentence", 400);
|
|
11921
12162
|
rec = mutate(core, id, (r) => {
|
|
11922
12163
|
r.direction = said;
|
|
@@ -11928,7 +12169,7 @@ async function generateView(deps, id, view, opts = {}) {
|
|
|
11928
12169
|
if (rec.views[dep].status !== "approved") throw fail(`approve the ${VIEW_LABEL[dep]} first`, 400);
|
|
11929
12170
|
}
|
|
11930
12171
|
if (!engine || !engine.capabilities().maxReferenceImages) throw fail("no engine here can draw a person", 400);
|
|
11931
|
-
const adjustment = str4(opts.adjustment,
|
|
12172
|
+
const adjustment = str4(opts.adjustment, TYPED_TEXT_MAX) || void 0;
|
|
11932
12173
|
const decide = opts.decide === "auto" ? "auto" : void 0;
|
|
11933
12174
|
const before = rec.views[view].status;
|
|
11934
12175
|
const saved = mutate(core, id, (r) => {
|
|
@@ -12266,35 +12507,17 @@ async function redoView(deps, id, view) {
|
|
|
12266
12507
|
r.decisions = [...r.decisions, { view, what: "again", at: (/* @__PURE__ */ new Date()).toISOString() }].slice(-RESULTS_MAX);
|
|
12267
12508
|
});
|
|
12268
12509
|
}
|
|
12269
|
-
async function usePhotoForView(deps, id, view, hash) {
|
|
12270
|
-
if (!isView(view)) throw fail("no such view", 400);
|
|
12271
|
-
if (running3.has(id)) throw fail("a view is still being drawn", 409);
|
|
12272
|
-
const rec = getPresenterDraft(deps.core, id);
|
|
12273
|
-
if (!rec) throw fail("draft not found", 404);
|
|
12274
|
-
if (rec.source !== "photos" || !rec.sources.includes(hash)) throw fail("that is not one of their photos", 400);
|
|
12275
|
-
return mutate(deps.core, id, (r) => {
|
|
12276
|
-
const slot = r.views[view];
|
|
12277
|
-
if (slot.hash === hash && slot.origin === "photo") return;
|
|
12278
|
-
const gone = [slot.hash, slot.prior].filter((h) => !!h && !r.sources.includes(h));
|
|
12279
|
-
r.views[view] = {
|
|
12280
|
-
...emptySlot(),
|
|
12281
|
-
attempts: slot.attempts,
|
|
12282
|
-
rejected: [...slot.rejected, ...gone],
|
|
12283
|
-
status: "approved",
|
|
12284
|
-
hash,
|
|
12285
|
-
origin: "photo"
|
|
12286
|
-
};
|
|
12287
|
-
staleDependents(r, view);
|
|
12288
|
-
});
|
|
12289
|
-
}
|
|
12290
12510
|
async function updatePresenterDraft(core, id, patch3) {
|
|
12291
12511
|
return mutate(core, id, (r) => {
|
|
12292
12512
|
if (patch3.extras !== void 0) r.extras = patch3.extras === true;
|
|
12293
12513
|
if (patch3.name !== void 0) r.name = str4(patch3.name, 60);
|
|
12294
12514
|
if (Array.isArray(patch3.facets))
|
|
12295
12515
|
r.facets = patch3.facets.map((f) => str4(f, 40)).filter(Boolean).slice(0, 8);
|
|
12296
|
-
if (patch3.direction !== void 0) r.direction = str4(patch3.direction,
|
|
12297
|
-
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
|
+
}
|
|
12298
12521
|
const items = keepItemsOf(patch3.keepItems, (h) => core.images.has(h));
|
|
12299
12522
|
if (items) writeItems(r, items);
|
|
12300
12523
|
else {
|
|
@@ -12327,19 +12550,15 @@ async function saveNew(deps, rec, hooks) {
|
|
|
12327
12550
|
const { core } = deps;
|
|
12328
12551
|
const id = rec.id;
|
|
12329
12552
|
if (!rec.name.trim()) throw fail("give them a name", 400);
|
|
12330
|
-
const
|
|
12331
|
-
(v) => v !== "portrait" && rec.views[v].origin === "generated" && !!rec.views[v].hash
|
|
12332
|
-
);
|
|
12333
|
-
const blind = !deps.engine && !drawnSet;
|
|
12334
|
-
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")];
|
|
12335
12554
|
for (const v of required) {
|
|
12336
12555
|
const s = rec.views[v];
|
|
12337
12556
|
if (s.status === "stale") throw fail(`redo the ${VIEW_LABEL[v]}: it was built on a view you changed`, 400);
|
|
12338
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);
|
|
12339
12559
|
}
|
|
12340
|
-
const
|
|
12341
|
-
const
|
|
12342
|
-
const angles = blind ? ["portrait"] : [...required];
|
|
12560
|
+
const shots = required.map((v) => rec.views[v].hash);
|
|
12561
|
+
const angles = [...required];
|
|
12343
12562
|
const portraitFile = `asset:${shots[0]}`;
|
|
12344
12563
|
const sourceFiles = rec.sources.map((h) => `asset:${h}`);
|
|
12345
12564
|
const mode = presenterCropMode(portraitFile, sourceFiles, "portrait");
|
|
@@ -12728,12 +12947,16 @@ function createSceneExamples(deps) {
|
|
|
12728
12947
|
const engineId = engine.capabilities().id;
|
|
12729
12948
|
const brand = deps.brandContext(brandId);
|
|
12730
12949
|
const path = (h) => deps.core.images.pathFor(h);
|
|
12950
|
+
const edge = engine.capabilities().maxReferenceEdge;
|
|
12731
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
|
+
);
|
|
12732
12955
|
const req = {
|
|
12733
12956
|
instruction,
|
|
12734
12957
|
sourceImage: path(source),
|
|
12735
12958
|
brand,
|
|
12736
|
-
referenceImages
|
|
12959
|
+
referenceImages,
|
|
12737
12960
|
referenceRoles: refs.map((_, i) => Array.isArray(role) ? role[i] : role),
|
|
12738
12961
|
width: scene.width,
|
|
12739
12962
|
height: scene.height
|
|
@@ -13112,7 +13335,7 @@ function createSceneExamples(deps) {
|
|
|
13112
13335
|
// src/sceneStudio.ts
|
|
13113
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`;
|
|
13114
13337
|
var STUDIO_PICTURES_MAX = 4;
|
|
13115
|
-
var ASK_MAX =
|
|
13338
|
+
var ASK_MAX = TYPED_TEXT_MAX;
|
|
13116
13339
|
var KEEP_PER_BRAND2 = 24;
|
|
13117
13340
|
var KEEP_MS2 = 24 * 60 * 60 * 1e3;
|
|
13118
13341
|
var jobs = /* @__PURE__ */ new Map();
|
|
@@ -13120,6 +13343,17 @@ var running4 = /* @__PURE__ */ new Map();
|
|
|
13120
13343
|
var tasks2 = /* @__PURE__ */ new Map();
|
|
13121
13344
|
var readHolds = /* @__PURE__ */ new Map();
|
|
13122
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
|
+
}
|
|
13123
13357
|
var fail2 = (message, statusCode = 400) => Object.assign(new Error(message), { statusCode });
|
|
13124
13358
|
var oneLine2 = (v, max) => String(v ?? "").replace(/\s+/g, " ").trim().slice(0, max);
|
|
13125
13359
|
function nameFromWords(words) {
|
|
@@ -13306,7 +13540,9 @@ function startSceneStudioJob(deps, input) {
|
|
|
13306
13540
|
const checked = readingFrom(input.reading);
|
|
13307
13541
|
if (!checked.ok) throw fail2(checked.error);
|
|
13308
13542
|
reading = checked.reading;
|
|
13309
|
-
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 = [];
|
|
13310
13546
|
}
|
|
13311
13547
|
const ask = oneLine2(input.ask, ASK_MAX);
|
|
13312
13548
|
if (kind === "make") {
|
|
@@ -13382,7 +13618,13 @@ async function read(deps, job, input, signal) {
|
|
|
13382
13618
|
const read2 = readingOfDraft(draft, fallback);
|
|
13383
13619
|
const holds = imagePaths.length ? holdsFrom(draft.holds) : prior?.holds;
|
|
13384
13620
|
if (imagePaths.length && holds) {
|
|
13385
|
-
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
|
+
}
|
|
13386
13628
|
while (readHolds.size > READ_HOLDS_KEPT) readHolds.delete(readHolds.keys().next().value);
|
|
13387
13629
|
}
|
|
13388
13630
|
const hero = read2.hero ?? prior?.hero;
|
|
@@ -14051,15 +14293,6 @@ function registerPresenterDraftRoutes(app, deps) {
|
|
|
14051
14293
|
const hash = String(req.body?.hash ?? "");
|
|
14052
14294
|
return answer(reply, async () => restoreView(await buildDeps(), draft.id, viewOf(req), hash));
|
|
14053
14295
|
});
|
|
14054
|
-
app.post("/api/brands/:id/presenter-drafts/:draftId/views/:view/use-photo", async (req, reply) => {
|
|
14055
|
-
const draft = draftOr404(req, reply);
|
|
14056
|
-
if (!draft) return;
|
|
14057
|
-
const body = req.body ?? {};
|
|
14058
|
-
return answer(
|
|
14059
|
-
reply,
|
|
14060
|
-
async () => usePhotoForView(await buildDeps(), draft.id, viewOf(req), String(body.hash ?? ""))
|
|
14061
|
-
);
|
|
14062
|
-
});
|
|
14063
14296
|
app.post("/api/brands/:id/presenter-drafts/:draftId/save", async (req, reply) => {
|
|
14064
14297
|
const draft = draftOr404(req, reply);
|
|
14065
14298
|
if (!draft) return;
|
|
@@ -14438,7 +14671,11 @@ function registerProjectRoutes(app, deps) {
|
|
|
14438
14671
|
nodes: core.store.recentActivity(brand.id, limit),
|
|
14439
14672
|
jobs: core.catalog.listRecentJobs(brand.id),
|
|
14440
14673
|
studio: listStudioWork(core, brand.id, deps.sceneExampleJobs?.(brand.id) ?? []),
|
|
14441
|
-
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()
|
|
14442
14679
|
};
|
|
14443
14680
|
});
|
|
14444
14681
|
app.get("/api/brands/:id/workspace", async (req, reply) => {
|
|
@@ -14842,33 +15079,80 @@ function registerImageRoutes(app, deps) {
|
|
|
14842
15079
|
}
|
|
14843
15080
|
|
|
14844
15081
|
// src/release/notes.data.ts
|
|
15082
|
+
var HEADLINES_KEPT = 5;
|
|
15083
|
+
var FIRST_PUBLIC = "0.2.0";
|
|
14845
15084
|
var RELEASES = [
|
|
15085
|
+
{
|
|
15086
|
+
version: "0.20.0",
|
|
15087
|
+
date: "2026-09-27",
|
|
15088
|
+
title: "Pictures develop in place, and What's new has its own page",
|
|
15089
|
+
announce: true,
|
|
15090
|
+
sections: [
|
|
15091
|
+
{
|
|
15092
|
+
heading: "Create",
|
|
15093
|
+
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.",
|
|
15094
|
+
image: {
|
|
15095
|
+
file: "0.20.0-made-in-place.webp",
|
|
15096
|
+
alt: "Two shots from one prompt in Create: the first has landed, and the second is still being made, a swirl with its clock."
|
|
15097
|
+
}
|
|
15098
|
+
},
|
|
15099
|
+
{
|
|
15100
|
+
heading: "What's new",
|
|
15101
|
+
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."
|
|
15102
|
+
},
|
|
15103
|
+
{
|
|
15104
|
+
heading: "Fixes",
|
|
15105
|
+
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."
|
|
15106
|
+
}
|
|
15107
|
+
]
|
|
15108
|
+
},
|
|
15109
|
+
{
|
|
15110
|
+
version: "0.19.1",
|
|
15111
|
+
date: "2026-09-26",
|
|
15112
|
+
title: "Codex keeps no copy of your pictures",
|
|
15113
|
+
sections: [
|
|
15114
|
+
{
|
|
15115
|
+
heading: "Privacy",
|
|
15116
|
+
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."
|
|
15117
|
+
},
|
|
15118
|
+
{
|
|
15119
|
+
heading: "Library",
|
|
15120
|
+
body: "The library download is checked before anything is installed, and one that does not match what this version expects is never used."
|
|
15121
|
+
},
|
|
15122
|
+
{
|
|
15123
|
+
heading: "Fixes",
|
|
15124
|
+
body: "A scene draw shows in the bell as soon as it starts, and the Scenes wall no longer calls it not drawn yet while it is drawing."
|
|
15125
|
+
}
|
|
15126
|
+
]
|
|
15127
|
+
},
|
|
14846
15128
|
{
|
|
14847
15129
|
version: "0.19.0",
|
|
14848
15130
|
date: "2026-09-26",
|
|
14849
|
-
title: "A new
|
|
15131
|
+
title: "A new library of products, presenters and scenes",
|
|
15132
|
+
announce: true,
|
|
14850
15133
|
sections: [
|
|
14851
15134
|
{
|
|
14852
15135
|
heading: "Library",
|
|
14853
|
-
body: "
|
|
15136
|
+
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.",
|
|
15137
|
+
image: {
|
|
15138
|
+
file: "0.19.0-home-examples.webp",
|
|
15139
|
+
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."
|
|
15140
|
+
}
|
|
14854
15141
|
},
|
|
14855
15142
|
{
|
|
14856
15143
|
heading: "Scenes",
|
|
14857
|
-
body: "
|
|
15144
|
+
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."
|
|
14858
15145
|
},
|
|
14859
15146
|
{
|
|
14860
15147
|
heading: "Create",
|
|
14861
|
-
body: "
|
|
14862
|
-
},
|
|
14863
|
-
{
|
|
14864
|
-
heading: "Fixes",
|
|
14865
|
-
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."
|
|
15148
|
+
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."
|
|
14866
15149
|
}
|
|
14867
15150
|
]
|
|
14868
15151
|
},
|
|
14869
15152
|
{
|
|
14870
15153
|
version: "0.18.2",
|
|
14871
15154
|
date: "2026-09-26",
|
|
15155
|
+
title: "The first library download finishes on a slow connection",
|
|
14872
15156
|
sections: [
|
|
14873
15157
|
{
|
|
14874
15158
|
heading: "Fixes",
|
|
@@ -14879,119 +15163,142 @@ var RELEASES = [
|
|
|
14879
15163
|
{
|
|
14880
15164
|
version: "0.18.1",
|
|
14881
15165
|
date: "2026-09-26",
|
|
15166
|
+
title: "Pictures made with Codex use far less of your ChatGPT plan",
|
|
15167
|
+
announce: true,
|
|
14882
15168
|
sections: [
|
|
14883
15169
|
{
|
|
14884
15170
|
heading: "Codex",
|
|
14885
|
-
body: "
|
|
15171
|
+
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."
|
|
14886
15172
|
},
|
|
14887
15173
|
{
|
|
14888
|
-
heading: "
|
|
14889
|
-
body: "A phone with the
|
|
15174
|
+
heading: "Local access",
|
|
15175
|
+
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."
|
|
14890
15176
|
},
|
|
14891
15177
|
{
|
|
14892
15178
|
heading: "Fixes",
|
|
14893
|
-
body: "A library download that stalls now
|
|
15179
|
+
body: "A library download that stalls now stops, and the library you already have keeps working."
|
|
14894
15180
|
}
|
|
14895
15181
|
]
|
|
14896
15182
|
},
|
|
14897
15183
|
{
|
|
14898
15184
|
version: "0.18.0",
|
|
14899
15185
|
date: "2026-09-25",
|
|
15186
|
+
title: "New shots start in portrait at Standard quality",
|
|
14900
15187
|
sections: [
|
|
14901
15188
|
{
|
|
14902
15189
|
heading: "Create",
|
|
14903
|
-
body: "A new shot
|
|
15190
|
+
body: "A new shot starts in portrait, with one picture at Standard quality. A setting you already saved still wins.",
|
|
15191
|
+
image: {
|
|
15192
|
+
file: "0.18.0-portrait-standard.webp",
|
|
15193
|
+
alt: "The settings a new shot starts with, Portrait, one picture and Standard, with the shape list open on Portrait."
|
|
15194
|
+
}
|
|
14904
15195
|
},
|
|
14905
15196
|
{
|
|
14906
15197
|
heading: "Library",
|
|
14907
|
-
body: "
|
|
15198
|
+
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."
|
|
14908
15199
|
},
|
|
14909
15200
|
{
|
|
14910
15201
|
heading: "Shots",
|
|
14911
|
-
body: "
|
|
14912
|
-
},
|
|
14913
|
-
{
|
|
14914
|
-
heading: "Activity",
|
|
14915
|
-
body: "An empty Activity panel is the heading and one sentence."
|
|
15202
|
+
body: "On a shot, Download comes first, then Archive or Delete, then the Keepers star."
|
|
14916
15203
|
}
|
|
14917
15204
|
]
|
|
14918
15205
|
},
|
|
14919
15206
|
{
|
|
14920
15207
|
version: "0.17.2",
|
|
14921
15208
|
date: "2026-09-24",
|
|
15209
|
+
title: "A waiting prompt uses your presenters as they are now",
|
|
14922
15210
|
sections: [
|
|
14923
15211
|
{
|
|
14924
15212
|
heading: "Presenters",
|
|
14925
|
-
body: "A
|
|
15213
|
+
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.",
|
|
15214
|
+
image: {
|
|
15215
|
+
file: "0.17.2-presenter-prompt.webp",
|
|
15216
|
+
alt: "A waiting prompt, Amara and Kwame by a window, naming two of your presenters as chips with their faces."
|
|
15217
|
+
}
|
|
14926
15218
|
}
|
|
14927
15219
|
]
|
|
14928
15220
|
},
|
|
14929
15221
|
{
|
|
14930
15222
|
version: "0.17.1",
|
|
14931
15223
|
date: "2026-09-24",
|
|
15224
|
+
title: "Presenters keep their own face, hair and build",
|
|
14932
15225
|
sections: [
|
|
14933
15226
|
{
|
|
14934
15227
|
heading: "Presenters",
|
|
14935
|
-
body: "A presenter keeps their own face, hair, build, glasses and tattoos when a scene or
|
|
15228
|
+
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.",
|
|
15229
|
+
image: {
|
|
15230
|
+
file: "0.17.1-presenter-identity.webp",
|
|
15231
|
+
alt: "Kwame, one of your presenters, as his page opens: his face, what he is filed under, and his hair and build."
|
|
15232
|
+
}
|
|
14936
15233
|
}
|
|
14937
15234
|
]
|
|
14938
15235
|
},
|
|
14939
15236
|
{
|
|
14940
15237
|
version: "0.17.0",
|
|
14941
15238
|
date: "2026-09-24",
|
|
15239
|
+
title: "Select several of your own products, presenters or scenes",
|
|
15240
|
+
announce: true,
|
|
14942
15241
|
sections: [
|
|
14943
15242
|
{
|
|
14944
15243
|
heading: "Library",
|
|
14945
|
-
body: "
|
|
15244
|
+
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.",
|
|
15245
|
+
image: {
|
|
15246
|
+
file: "0.17.0-select-several.webp",
|
|
15247
|
+
alt: "Five of your scenes in a row, three of them picked, and the bar that keeps or deletes them together."
|
|
15248
|
+
}
|
|
14946
15249
|
},
|
|
14947
15250
|
{
|
|
14948
|
-
heading: "
|
|
14949
|
-
body: "Home, Create and
|
|
15251
|
+
heading: "Tabs",
|
|
15252
|
+
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."
|
|
14950
15253
|
},
|
|
14951
15254
|
{
|
|
14952
15255
|
heading: "Fixes",
|
|
14953
|
-
body: "Tab counts on Create no longer jump while the
|
|
15256
|
+
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."
|
|
14954
15257
|
}
|
|
14955
15258
|
]
|
|
14956
15259
|
},
|
|
14957
15260
|
{
|
|
14958
15261
|
version: "0.16.0",
|
|
14959
15262
|
date: "2026-09-24",
|
|
14960
|
-
title: "Open Scenri on your phone or tablet
|
|
15263
|
+
title: "Open Scenri on your phone or tablet",
|
|
15264
|
+
announce: true,
|
|
14961
15265
|
sections: [
|
|
14962
15266
|
{
|
|
14963
15267
|
heading: "Local access",
|
|
14964
|
-
body: "Scenri
|
|
15268
|
+
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.",
|
|
15269
|
+
image: {
|
|
15270
|
+
file: "0.16.0-local-access.webp",
|
|
15271
|
+
alt: "The Local access card: a QR code to scan, the address and six-digit code to type, and an iPhone just connected."
|
|
15272
|
+
}
|
|
14965
15273
|
},
|
|
14966
15274
|
{
|
|
14967
15275
|
heading: "Security",
|
|
14968
|
-
body: "Only devices with the code get in, and wrong guesses
|
|
15276
|
+
body: "Only devices with the six-digit code get in, and wrong guesses have to wait. Press New code to sign every device out."
|
|
14969
15277
|
},
|
|
14970
15278
|
{
|
|
14971
15279
|
heading: "Settings",
|
|
14972
|
-
body: "Providers, Appearance, Library, Local access, Updates and About
|
|
15280
|
+
body: "One page for each thing: Providers, Appearance, Library, Local access, Updates and About, with every delete under Danger zone."
|
|
14973
15281
|
}
|
|
14974
|
-
]
|
|
15282
|
+
],
|
|
15283
|
+
newFeatures: ["local-access"]
|
|
14975
15284
|
},
|
|
14976
15285
|
{
|
|
14977
15286
|
version: "0.15.1",
|
|
14978
15287
|
date: "2026-09-23",
|
|
15288
|
+
title: "Make a scene by describing it, or by choosing from pictures",
|
|
15289
|
+
announce: true,
|
|
14979
15290
|
sections: [
|
|
14980
15291
|
{
|
|
14981
15292
|
heading: "Scenes",
|
|
14982
|
-
body: "
|
|
14983
|
-
},
|
|
14984
|
-
{
|
|
14985
|
-
heading: "Shots",
|
|
14986
|
-
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."
|
|
15293
|
+
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."
|
|
14987
15294
|
},
|
|
14988
15295
|
{
|
|
14989
15296
|
heading: "Activity",
|
|
14990
|
-
body: "Scene and presenter work
|
|
15297
|
+
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."
|
|
14991
15298
|
},
|
|
14992
15299
|
{
|
|
14993
|
-
heading: "
|
|
14994
|
-
body: "
|
|
15300
|
+
heading: "Shots",
|
|
15301
|
+
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."
|
|
14995
15302
|
}
|
|
14996
15303
|
]
|
|
14997
15304
|
},
|
|
@@ -15006,10 +15313,11 @@ var RELEASES = [
|
|
|
15006
15313
|
{
|
|
15007
15314
|
version: "0.14.0",
|
|
15008
15315
|
date: "2026-09-22",
|
|
15316
|
+
title: "Settings is redesigned with six pages",
|
|
15009
15317
|
sections: [
|
|
15010
15318
|
{
|
|
15011
15319
|
heading: "Settings",
|
|
15012
|
-
body: "
|
|
15320
|
+
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."
|
|
15013
15321
|
},
|
|
15014
15322
|
{
|
|
15015
15323
|
heading: "Providers",
|
|
@@ -15024,16 +15332,18 @@ var RELEASES = [
|
|
|
15024
15332
|
{
|
|
15025
15333
|
version: "0.13.2",
|
|
15026
15334
|
date: "2026-09-22",
|
|
15335
|
+
title: "Presenters show an expression that fits the shot",
|
|
15027
15336
|
sections: [
|
|
15028
15337
|
{
|
|
15029
15338
|
heading: "Presenters",
|
|
15030
|
-
body: "
|
|
15339
|
+
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."
|
|
15031
15340
|
}
|
|
15032
15341
|
]
|
|
15033
15342
|
},
|
|
15034
15343
|
{
|
|
15035
15344
|
version: "0.13.1",
|
|
15036
15345
|
date: "2026-09-22",
|
|
15346
|
+
title: "Alerts work without closing the dialog beneath them",
|
|
15037
15347
|
sections: [
|
|
15038
15348
|
{
|
|
15039
15349
|
heading: "Alerts",
|
|
@@ -15052,15 +15362,16 @@ var RELEASES = [
|
|
|
15052
15362
|
{
|
|
15053
15363
|
version: "0.13.0",
|
|
15054
15364
|
date: "2026-09-21",
|
|
15055
|
-
title: "Learn teaches Scenri one lesson at a time, alerts are quieter, and the Create feed holds still
|
|
15365
|
+
title: "Learn teaches Scenri one lesson at a time, alerts are quieter, and the Create feed holds still",
|
|
15366
|
+
announce: true,
|
|
15056
15367
|
sections: [
|
|
15057
15368
|
{
|
|
15058
15369
|
heading: "Learn",
|
|
15059
|
-
body: "
|
|
15370
|
+
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."
|
|
15060
15371
|
},
|
|
15061
15372
|
{
|
|
15062
15373
|
heading: "Alerts",
|
|
15063
|
-
body: "
|
|
15374
|
+
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."
|
|
15064
15375
|
},
|
|
15065
15376
|
{
|
|
15066
15377
|
heading: "Create",
|
|
@@ -15071,7 +15382,8 @@ var RELEASES = [
|
|
|
15071
15382
|
{
|
|
15072
15383
|
version: "0.12.0",
|
|
15073
15384
|
date: "2026-09-20",
|
|
15074
|
-
title: "Saved work shows up everywhere at once, and a selection has its own toolbar
|
|
15385
|
+
title: "Saved work shows up everywhere at once, and a selection has its own toolbar",
|
|
15386
|
+
announce: true,
|
|
15075
15387
|
sections: [
|
|
15076
15388
|
{
|
|
15077
15389
|
heading: "Create",
|
|
@@ -15090,6 +15402,7 @@ var RELEASES = [
|
|
|
15090
15402
|
{
|
|
15091
15403
|
version: "0.11.1",
|
|
15092
15404
|
date: "2026-09-18",
|
|
15405
|
+
title: "Learn and the guided first shot are paused",
|
|
15093
15406
|
sections: [
|
|
15094
15407
|
{
|
|
15095
15408
|
heading: "Learn",
|
|
@@ -15104,7 +15417,8 @@ var RELEASES = [
|
|
|
15104
15417
|
{
|
|
15105
15418
|
version: "0.11.0",
|
|
15106
15419
|
date: "2026-09-18",
|
|
15107
|
-
title: "A new top bar, and a tutor that makes your first shot with you
|
|
15420
|
+
title: "A new top bar, and a tutor that makes your first shot with you",
|
|
15421
|
+
announce: true,
|
|
15108
15422
|
sections: [
|
|
15109
15423
|
{
|
|
15110
15424
|
heading: "Top bar",
|
|
@@ -15127,6 +15441,7 @@ var RELEASES = [
|
|
|
15127
15441
|
{
|
|
15128
15442
|
version: "0.10.3",
|
|
15129
15443
|
date: "2026-09-18",
|
|
15444
|
+
title: "Scenri no longer pauses right after the studio opens",
|
|
15130
15445
|
sections: [
|
|
15131
15446
|
{
|
|
15132
15447
|
heading: "Fixes",
|
|
@@ -15137,6 +15452,7 @@ var RELEASES = [
|
|
|
15137
15452
|
{
|
|
15138
15453
|
version: "0.10.2",
|
|
15139
15454
|
date: "2026-09-17",
|
|
15455
|
+
title: "A presenter draft opens where the conversation left off",
|
|
15140
15456
|
sections: [
|
|
15141
15457
|
{
|
|
15142
15458
|
heading: "Presenters",
|
|
@@ -15147,7 +15463,8 @@ var RELEASES = [
|
|
|
15147
15463
|
{
|
|
15148
15464
|
version: "0.10.1",
|
|
15149
15465
|
date: "2026-09-16",
|
|
15150
|
-
title: "A saved presenter can be duplicated or deleted straight from its card
|
|
15466
|
+
title: "A saved presenter can be duplicated or deleted straight from its card",
|
|
15467
|
+
announce: true,
|
|
15151
15468
|
sections: [
|
|
15152
15469
|
{
|
|
15153
15470
|
heading: "Presenters",
|
|
@@ -15170,7 +15487,8 @@ var RELEASES = [
|
|
|
15170
15487
|
{
|
|
15171
15488
|
version: "0.10.0",
|
|
15172
15489
|
date: "2026-09-16",
|
|
15173
|
-
title: "Presenters are cast by talking to Scenri, one question at a time
|
|
15490
|
+
title: "Presenters are cast by talking to Scenri, one question at a time",
|
|
15491
|
+
announce: true,
|
|
15174
15492
|
sections: [
|
|
15175
15493
|
{
|
|
15176
15494
|
heading: "Presenters",
|
|
@@ -15185,7 +15503,8 @@ var RELEASES = [
|
|
|
15185
15503
|
{
|
|
15186
15504
|
version: "0.9.4",
|
|
15187
15505
|
date: "2026-09-14",
|
|
15188
|
-
title: "A store that asks Scenri to slow down no longer looks like an empty one
|
|
15506
|
+
title: "A store that asks Scenri to slow down no longer looks like an empty one",
|
|
15507
|
+
announce: true,
|
|
15189
15508
|
sections: [
|
|
15190
15509
|
{
|
|
15191
15510
|
heading: "Products",
|
|
@@ -15200,7 +15519,8 @@ var RELEASES = [
|
|
|
15200
15519
|
{
|
|
15201
15520
|
version: "0.9.3",
|
|
15202
15521
|
date: "2026-09-14",
|
|
15203
|
-
title: "Paste a website, get the brand and its products
|
|
15522
|
+
title: "Paste a website, get the brand and its products",
|
|
15523
|
+
announce: true,
|
|
15204
15524
|
sections: [
|
|
15205
15525
|
{
|
|
15206
15526
|
heading: "Brand",
|
|
@@ -15223,6 +15543,7 @@ var RELEASES = [
|
|
|
15223
15543
|
{
|
|
15224
15544
|
version: "0.9.2",
|
|
15225
15545
|
date: "2026-09-07",
|
|
15546
|
+
title: "A Codex CLI too old for its model now says so",
|
|
15226
15547
|
sections: [
|
|
15227
15548
|
{
|
|
15228
15549
|
heading: "Fixes",
|
|
@@ -15233,6 +15554,7 @@ var RELEASES = [
|
|
|
15233
15554
|
{
|
|
15234
15555
|
version: "0.9.1",
|
|
15235
15556
|
date: "2026-09-07",
|
|
15557
|
+
title: "The Windows desktop icon never fails in silence",
|
|
15236
15558
|
sections: [
|
|
15237
15559
|
{
|
|
15238
15560
|
heading: "Desktop",
|
|
@@ -15244,6 +15566,7 @@ var RELEASES = [
|
|
|
15244
15566
|
version: "0.9.0",
|
|
15245
15567
|
date: "2026-09-06",
|
|
15246
15568
|
title: "Scenri on your desktop",
|
|
15569
|
+
announce: true,
|
|
15247
15570
|
sections: [
|
|
15248
15571
|
{
|
|
15249
15572
|
heading: "Desktop",
|
|
@@ -15262,6 +15585,7 @@ var RELEASES = [
|
|
|
15262
15585
|
{
|
|
15263
15586
|
version: "0.8.3",
|
|
15264
15587
|
date: "2026-09-06",
|
|
15588
|
+
title: "One picker for everything you add to a shot",
|
|
15265
15589
|
sections: [
|
|
15266
15590
|
{
|
|
15267
15591
|
heading: "Create",
|
|
@@ -15284,6 +15608,7 @@ var RELEASES = [
|
|
|
15284
15608
|
{
|
|
15285
15609
|
version: "0.8.2",
|
|
15286
15610
|
date: "2026-09-03",
|
|
15611
|
+
title: "Security updates to the server and its URL parser",
|
|
15287
15612
|
sections: [
|
|
15288
15613
|
{
|
|
15289
15614
|
heading: "Security",
|
|
@@ -15294,7 +15619,8 @@ var RELEASES = [
|
|
|
15294
15619
|
{
|
|
15295
15620
|
version: "0.8.1",
|
|
15296
15621
|
date: "2026-09-03",
|
|
15297
|
-
title: "The same speed at any size
|
|
15622
|
+
title: "The same speed at any size",
|
|
15623
|
+
announce: true,
|
|
15298
15624
|
sections: [
|
|
15299
15625
|
{
|
|
15300
15626
|
heading: "Create",
|
|
@@ -15313,7 +15639,8 @@ var RELEASES = [
|
|
|
15313
15639
|
{
|
|
15314
15640
|
version: "0.8.0",
|
|
15315
15641
|
date: "2026-09-02",
|
|
15316
|
-
title: "The composer, rebuilt around what a shot is made of
|
|
15642
|
+
title: "The composer, rebuilt around what a shot is made of",
|
|
15643
|
+
announce: true,
|
|
15317
15644
|
sections: [
|
|
15318
15645
|
{
|
|
15319
15646
|
heading: "Create",
|
|
@@ -15336,7 +15663,8 @@ var RELEASES = [
|
|
|
15336
15663
|
{
|
|
15337
15664
|
version: "0.7.5",
|
|
15338
15665
|
date: "2026-09-01",
|
|
15339
|
-
title: "Every shot is one card
|
|
15666
|
+
title: "Every shot is one card",
|
|
15667
|
+
announce: true,
|
|
15340
15668
|
sections: [
|
|
15341
15669
|
{
|
|
15342
15670
|
heading: "Create",
|
|
@@ -15355,6 +15683,7 @@ var RELEASES = [
|
|
|
15355
15683
|
{
|
|
15356
15684
|
version: "0.7.4",
|
|
15357
15685
|
date: "2026-08-31",
|
|
15686
|
+
title: "A failed Codex shot says why it failed",
|
|
15358
15687
|
sections: [
|
|
15359
15688
|
{
|
|
15360
15689
|
heading: "Fixes",
|
|
@@ -15365,6 +15694,7 @@ var RELEASES = [
|
|
|
15365
15694
|
{
|
|
15366
15695
|
version: "0.7.3",
|
|
15367
15696
|
date: "2026-08-31",
|
|
15697
|
+
title: "A finished tile no longer guesses its shape",
|
|
15368
15698
|
sections: [
|
|
15369
15699
|
{
|
|
15370
15700
|
heading: "Fixes",
|
|
@@ -15375,7 +15705,8 @@ var RELEASES = [
|
|
|
15375
15705
|
{
|
|
15376
15706
|
version: "0.7.2",
|
|
15377
15707
|
date: "2026-08-30",
|
|
15378
|
-
title: "Changing a shot to a new shape keeps the photograph
|
|
15708
|
+
title: "Changing a shot to a new shape keeps the photograph",
|
|
15709
|
+
announce: true,
|
|
15379
15710
|
sections: [
|
|
15380
15711
|
{
|
|
15381
15712
|
heading: "Create",
|
|
@@ -15390,7 +15721,8 @@ var RELEASES = [
|
|
|
15390
15721
|
{
|
|
15391
15722
|
version: "0.7.1",
|
|
15392
15723
|
date: "2026-08-30",
|
|
15393
|
-
title: "Four images from one
|
|
15724
|
+
title: "Four images from one prompt are one set",
|
|
15725
|
+
announce: true,
|
|
15394
15726
|
sections: [
|
|
15395
15727
|
{
|
|
15396
15728
|
heading: "Create",
|
|
@@ -15409,7 +15741,8 @@ var RELEASES = [
|
|
|
15409
15741
|
{
|
|
15410
15742
|
version: "0.7.0",
|
|
15411
15743
|
date: "2026-08-30",
|
|
15412
|
-
title: "Your presenter stays your presenter
|
|
15744
|
+
title: "Your presenter stays your presenter",
|
|
15745
|
+
announce: true,
|
|
15413
15746
|
sections: [
|
|
15414
15747
|
{
|
|
15415
15748
|
heading: "Scenes",
|
|
@@ -15428,6 +15761,7 @@ var RELEASES = [
|
|
|
15428
15761
|
{
|
|
15429
15762
|
version: "0.6.13",
|
|
15430
15763
|
date: "2026-08-30",
|
|
15764
|
+
title: "Products keep their colour, and your logo joins the shot",
|
|
15431
15765
|
sections: [
|
|
15432
15766
|
{
|
|
15433
15767
|
heading: "Products",
|
|
@@ -15446,6 +15780,7 @@ var RELEASES = [
|
|
|
15446
15780
|
{
|
|
15447
15781
|
version: "0.6.12",
|
|
15448
15782
|
date: "2026-08-29",
|
|
15783
|
+
title: "Reference images show what they are",
|
|
15449
15784
|
sections: [
|
|
15450
15785
|
{
|
|
15451
15786
|
heading: "Create",
|
|
@@ -15456,6 +15791,7 @@ var RELEASES = [
|
|
|
15456
15791
|
{
|
|
15457
15792
|
version: "0.6.11",
|
|
15458
15793
|
date: "2026-08-29",
|
|
15794
|
+
title: "Hitting your Codex usage limit says when it comes back",
|
|
15459
15795
|
sections: [
|
|
15460
15796
|
{
|
|
15461
15797
|
heading: "Create",
|
|
@@ -15466,7 +15802,8 @@ var RELEASES = [
|
|
|
15466
15802
|
{
|
|
15467
15803
|
version: "0.6.10",
|
|
15468
15804
|
date: "2026-08-29",
|
|
15469
|
-
title: "Pictures keep their shape
|
|
15805
|
+
title: "Pictures keep their shape",
|
|
15806
|
+
announce: true,
|
|
15470
15807
|
sections: [
|
|
15471
15808
|
{
|
|
15472
15809
|
heading: "Create",
|
|
@@ -15481,7 +15818,8 @@ var RELEASES = [
|
|
|
15481
15818
|
{
|
|
15482
15819
|
version: "0.6.9",
|
|
15483
15820
|
date: "2026-08-29",
|
|
15484
|
-
title: "Refinements keep the shot
|
|
15821
|
+
title: "Refinements keep the shot",
|
|
15822
|
+
announce: true,
|
|
15485
15823
|
sections: [
|
|
15486
15824
|
{
|
|
15487
15825
|
heading: "Refine",
|
|
@@ -15500,6 +15838,7 @@ var RELEASES = [
|
|
|
15500
15838
|
{
|
|
15501
15839
|
version: "0.6.8",
|
|
15502
15840
|
date: "2026-08-29",
|
|
15841
|
+
title: "Closing a new form ends it, with Undo if you did not mean it",
|
|
15503
15842
|
sections: [
|
|
15504
15843
|
{
|
|
15505
15844
|
heading: "Create dialogs",
|
|
@@ -15514,6 +15853,7 @@ var RELEASES = [
|
|
|
15514
15853
|
{
|
|
15515
15854
|
version: "0.6.7",
|
|
15516
15855
|
date: "2026-08-29",
|
|
15856
|
+
title: "The prompt starts empty, with no scene you did not pick",
|
|
15517
15857
|
sections: [
|
|
15518
15858
|
{
|
|
15519
15859
|
heading: "Create",
|
|
@@ -15524,6 +15864,7 @@ var RELEASES = [
|
|
|
15524
15864
|
{
|
|
15525
15865
|
version: "0.6.6",
|
|
15526
15866
|
date: "2026-08-29",
|
|
15867
|
+
title: "Removing a chip works on the first click",
|
|
15527
15868
|
sections: [
|
|
15528
15869
|
{
|
|
15529
15870
|
heading: "Create",
|
|
@@ -15538,6 +15879,7 @@ var RELEASES = [
|
|
|
15538
15879
|
{
|
|
15539
15880
|
version: "0.6.5",
|
|
15540
15881
|
date: "2026-08-29",
|
|
15882
|
+
title: "A scene no longer brings its own demo object",
|
|
15541
15883
|
sections: [
|
|
15542
15884
|
{
|
|
15543
15885
|
heading: "Scenes",
|
|
@@ -15548,6 +15890,7 @@ var RELEASES = [
|
|
|
15548
15890
|
{
|
|
15549
15891
|
version: "0.6.4",
|
|
15550
15892
|
date: "2026-08-29",
|
|
15893
|
+
title: "New forms no longer carry photos from something else",
|
|
15551
15894
|
sections: [
|
|
15552
15895
|
{
|
|
15553
15896
|
heading: "Create",
|
|
@@ -15558,7 +15901,8 @@ var RELEASES = [
|
|
|
15558
15901
|
{
|
|
15559
15902
|
version: "0.6.3",
|
|
15560
15903
|
date: "2026-08-28",
|
|
15561
|
-
title: "Your logo stays your logo, and every page opens like a page
|
|
15904
|
+
title: "Your logo stays your logo, and every page opens like a page",
|
|
15905
|
+
announce: true,
|
|
15562
15906
|
sections: [
|
|
15563
15907
|
{
|
|
15564
15908
|
heading: "Brand",
|
|
@@ -15573,7 +15917,8 @@ var RELEASES = [
|
|
|
15573
15917
|
{
|
|
15574
15918
|
version: "0.6.2",
|
|
15575
15919
|
date: "2026-08-28",
|
|
15576
|
-
title: "Scenes keep what makes them distinctive
|
|
15920
|
+
title: "Scenes keep what makes them distinctive",
|
|
15921
|
+
announce: true,
|
|
15577
15922
|
sections: [
|
|
15578
15923
|
{
|
|
15579
15924
|
heading: "Scenes",
|
|
@@ -15596,6 +15941,7 @@ var RELEASES = [
|
|
|
15596
15941
|
{
|
|
15597
15942
|
version: "0.6.1",
|
|
15598
15943
|
date: "2026-08-26",
|
|
15944
|
+
title: "Refining a shot opens at its own shape",
|
|
15599
15945
|
sections: [
|
|
15600
15946
|
{
|
|
15601
15947
|
heading: "Shots",
|
|
@@ -15606,6 +15952,7 @@ var RELEASES = [
|
|
|
15606
15952
|
{
|
|
15607
15953
|
version: "0.6.0",
|
|
15608
15954
|
date: "2026-08-26",
|
|
15955
|
+
title: "A wider or taller shot keeps the original photograph",
|
|
15609
15956
|
sections: [
|
|
15610
15957
|
{
|
|
15611
15958
|
heading: "Shots",
|
|
@@ -15620,6 +15967,7 @@ var RELEASES = [
|
|
|
15620
15967
|
{
|
|
15621
15968
|
version: "0.5.1",
|
|
15622
15969
|
date: "2026-08-26",
|
|
15970
|
+
title: "An extended shot keeps its subject where it was",
|
|
15623
15971
|
sections: [
|
|
15624
15972
|
{
|
|
15625
15973
|
heading: "Shots",
|
|
@@ -15630,7 +15978,8 @@ var RELEASES = [
|
|
|
15630
15978
|
{
|
|
15631
15979
|
version: "0.5.0",
|
|
15632
15980
|
date: "2026-08-26",
|
|
15633
|
-
title: "A finished shot can change shape, and a presenter you attach is a requirement rather than a suggestion
|
|
15981
|
+
title: "A finished shot can change shape, and a presenter you attach is a requirement rather than a suggestion",
|
|
15982
|
+
announce: true,
|
|
15634
15983
|
sections: [
|
|
15635
15984
|
{
|
|
15636
15985
|
heading: "Create",
|
|
@@ -15653,7 +16002,8 @@ var RELEASES = [
|
|
|
15653
16002
|
{
|
|
15654
16003
|
version: "0.4.7",
|
|
15655
16004
|
date: "2026-08-24",
|
|
15656
|
-
title: "The feed shows the work, and the controls wait until you point at it
|
|
16005
|
+
title: "The feed shows the work, and the controls wait until you point at it",
|
|
16006
|
+
announce: true,
|
|
15657
16007
|
sections: [
|
|
15658
16008
|
{
|
|
15659
16009
|
heading: "Create",
|
|
@@ -15672,6 +16022,7 @@ var RELEASES = [
|
|
|
15672
16022
|
{
|
|
15673
16023
|
version: "0.4.6",
|
|
15674
16024
|
date: "2026-08-24",
|
|
16025
|
+
title: "Removing something from a shot leaves no outline",
|
|
15675
16026
|
sections: [
|
|
15676
16027
|
{
|
|
15677
16028
|
heading: "Refining",
|
|
@@ -15686,7 +16037,8 @@ var RELEASES = [
|
|
|
15686
16037
|
{
|
|
15687
16038
|
version: "0.4.5",
|
|
15688
16039
|
date: "2026-08-23",
|
|
15689
|
-
title: "New versions find you while Scenri runs
|
|
16040
|
+
title: "New versions find you while Scenri runs",
|
|
16041
|
+
announce: true,
|
|
15690
16042
|
sections: [
|
|
15691
16043
|
{
|
|
15692
16044
|
heading: "Updates",
|
|
@@ -15701,7 +16053,8 @@ var RELEASES = [
|
|
|
15701
16053
|
{
|
|
15702
16054
|
version: "0.4.4",
|
|
15703
16055
|
date: "2026-08-23",
|
|
15704
|
-
title: "Codex setup tells the truth, and a stuck generation fails instead of running forever
|
|
16056
|
+
title: "Codex setup tells the truth, and a stuck generation fails instead of running forever",
|
|
16057
|
+
announce: true,
|
|
15705
16058
|
sections: [
|
|
15706
16059
|
{
|
|
15707
16060
|
heading: "Codex",
|
|
@@ -15716,7 +16069,8 @@ var RELEASES = [
|
|
|
15716
16069
|
{
|
|
15717
16070
|
version: "0.4.3",
|
|
15718
16071
|
date: "2026-08-23",
|
|
15719
|
-
title: "Controls hold still when you press them
|
|
16072
|
+
title: "Controls hold still when you press them",
|
|
16073
|
+
announce: true,
|
|
15720
16074
|
sections: [
|
|
15721
16075
|
{
|
|
15722
16076
|
heading: "Fixes",
|
|
@@ -15727,6 +16081,7 @@ var RELEASES = [
|
|
|
15727
16081
|
{
|
|
15728
16082
|
version: "0.4.2",
|
|
15729
16083
|
date: "2026-08-23",
|
|
16084
|
+
title: "Settings tells the update story in one row",
|
|
15730
16085
|
sections: [
|
|
15731
16086
|
{
|
|
15732
16087
|
heading: "Updates",
|
|
@@ -15737,7 +16092,8 @@ var RELEASES = [
|
|
|
15737
16092
|
{
|
|
15738
16093
|
version: "0.4.1",
|
|
15739
16094
|
date: "2026-08-23",
|
|
15740
|
-
title: "Updates now arrive by themselves
|
|
16095
|
+
title: "Updates now arrive by themselves",
|
|
16096
|
+
announce: true,
|
|
15741
16097
|
sections: [
|
|
15742
16098
|
{
|
|
15743
16099
|
heading: "Updates",
|
|
@@ -15752,7 +16108,8 @@ var RELEASES = [
|
|
|
15752
16108
|
{
|
|
15753
16109
|
version: "0.4.0",
|
|
15754
16110
|
date: "2026-08-23",
|
|
15755
|
-
title: "Refining a shot keeps the shot
|
|
16111
|
+
title: "Refining a shot keeps the shot",
|
|
16112
|
+
announce: true,
|
|
15756
16113
|
sections: [
|
|
15757
16114
|
{
|
|
15758
16115
|
heading: "Refining",
|
|
@@ -15775,6 +16132,7 @@ var RELEASES = [
|
|
|
15775
16132
|
{
|
|
15776
16133
|
version: "0.3.5",
|
|
15777
16134
|
date: "2026-08-21",
|
|
16135
|
+
title: "Codex setup on Windows sees a successful install",
|
|
15778
16136
|
sections: [
|
|
15779
16137
|
{
|
|
15780
16138
|
heading: "Fixes",
|
|
@@ -15785,6 +16143,7 @@ var RELEASES = [
|
|
|
15785
16143
|
{
|
|
15786
16144
|
version: "0.3.4",
|
|
15787
16145
|
date: "2026-08-21",
|
|
16146
|
+
title: "A presenter's photos define who they are, not what they wear",
|
|
15788
16147
|
sections: [
|
|
15789
16148
|
{
|
|
15790
16149
|
heading: "Presenters",
|
|
@@ -15806,6 +16165,7 @@ var RELEASES = [
|
|
|
15806
16165
|
{
|
|
15807
16166
|
version: "0.3.2",
|
|
15808
16167
|
date: "2026-08-20",
|
|
16168
|
+
title: "Codex CLI setup offers the command that gets past a refusal",
|
|
15809
16169
|
sections: [
|
|
15810
16170
|
{
|
|
15811
16171
|
heading: "Fixes",
|
|
@@ -15816,6 +16176,7 @@ var RELEASES = [
|
|
|
15816
16176
|
{
|
|
15817
16177
|
version: "0.3.1",
|
|
15818
16178
|
date: "2026-08-20",
|
|
16179
|
+
title: "Starting Scenri explains what went wrong in plain words",
|
|
15819
16180
|
sections: [
|
|
15820
16181
|
{
|
|
15821
16182
|
heading: "Fixes",
|
|
@@ -15826,7 +16187,8 @@ var RELEASES = [
|
|
|
15826
16187
|
{
|
|
15827
16188
|
version: "0.3.0",
|
|
15828
16189
|
date: "2026-08-20",
|
|
15829
|
-
title: "The name is Scenri
|
|
16190
|
+
title: "The name is Scenri",
|
|
16191
|
+
announce: true,
|
|
15830
16192
|
sections: [
|
|
15831
16193
|
{
|
|
15832
16194
|
heading: "The name",
|
|
@@ -15841,26 +16203,29 @@ var RELEASES = [
|
|
|
15841
16203
|
{
|
|
15842
16204
|
version: "0.2.3",
|
|
15843
16205
|
date: "2026-08-20",
|
|
16206
|
+
title: "The last advisory against a build dependency is closed",
|
|
15844
16207
|
sections: [
|
|
15845
16208
|
{
|
|
15846
16209
|
heading: "Security",
|
|
15847
|
-
body: "The last advisory against a build dependency is closed. Nothing about how
|
|
16210
|
+
body: "The last advisory against a build dependency is closed. Nothing about how Scenri runs changes, and nothing in the published package moves."
|
|
15848
16211
|
}
|
|
15849
16212
|
]
|
|
15850
16213
|
},
|
|
15851
16214
|
{
|
|
15852
16215
|
version: "0.2.2",
|
|
15853
16216
|
date: "2026-08-20",
|
|
16217
|
+
title: "Two dependencies are on their patched releases",
|
|
15854
16218
|
sections: [
|
|
15855
16219
|
{
|
|
15856
16220
|
heading: "Security",
|
|
15857
|
-
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
|
|
16221
|
+
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."
|
|
15858
16222
|
}
|
|
15859
16223
|
]
|
|
15860
16224
|
},
|
|
15861
16225
|
{
|
|
15862
16226
|
version: "0.2.1",
|
|
15863
16227
|
date: "2026-08-20",
|
|
16228
|
+
title: "The top bar and app icons carry the real mark",
|
|
15864
16229
|
sections: [
|
|
15865
16230
|
{
|
|
15866
16231
|
heading: "Fixes",
|
|
@@ -15871,7 +16236,8 @@ var RELEASES = [
|
|
|
15871
16236
|
{
|
|
15872
16237
|
version: "0.2.0",
|
|
15873
16238
|
date: "2026-08-20",
|
|
15874
|
-
title: "
|
|
16239
|
+
title: "Scenri goes public",
|
|
16240
|
+
announce: true,
|
|
15875
16241
|
sections: [
|
|
15876
16242
|
{
|
|
15877
16243
|
heading: "Compose",
|
|
@@ -15887,7 +16253,7 @@ var RELEASES = [
|
|
|
15887
16253
|
},
|
|
15888
16254
|
{
|
|
15889
16255
|
heading: "The studio",
|
|
15890
|
-
body: "The
|
|
16256
|
+
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."
|
|
15891
16257
|
}
|
|
15892
16258
|
]
|
|
15893
16259
|
},
|
|
@@ -15905,6 +16271,7 @@ var RELEASES = [
|
|
|
15905
16271
|
version: "0.1.0",
|
|
15906
16272
|
date: "2026-08-16",
|
|
15907
16273
|
title: "The first complete scenri, released internally.",
|
|
16274
|
+
announce: true,
|
|
15908
16275
|
sections: [
|
|
15909
16276
|
{
|
|
15910
16277
|
heading: "Create",
|
|
@@ -15931,6 +16298,35 @@ function releaseFor(version) {
|
|
|
15931
16298
|
function isNewsworthy(entry) {
|
|
15932
16299
|
return (entry?.sections.length ?? 0) > 0;
|
|
15933
16300
|
}
|
|
16301
|
+
function whatsNewWindow(releases, running5, seen = null) {
|
|
16302
|
+
const released = running5 !== "0.0.0";
|
|
16303
|
+
const recent = [];
|
|
16304
|
+
let headlines = 0;
|
|
16305
|
+
for (const r of releases) {
|
|
16306
|
+
if (headlines === HEADLINES_KEPT) break;
|
|
16307
|
+
if (!isNewsworthy(r)) continue;
|
|
16308
|
+
if (released && compareSemver(r.version, running5) > 0) continue;
|
|
16309
|
+
recent.push(r);
|
|
16310
|
+
if (r.announce) headlines++;
|
|
16311
|
+
}
|
|
16312
|
+
const unseen = seen === null || !released ? [] : recent.filter((r) => compareSemver(seen, r.version) < 0 && compareSemver(r.version, running5) <= 0).map((r) => r.version);
|
|
16313
|
+
const lead = recent.find((r) => r.announce && unseen.includes(r.version))?.version ?? null;
|
|
16314
|
+
const history = releases.filter(
|
|
16315
|
+
(r) => isNewsworthy(r) && compareSemver(r.version, FIRST_PUBLIC) >= 0 && (!released || compareSemver(r.version, running5) <= 0)
|
|
16316
|
+
);
|
|
16317
|
+
return { recent, history, unseen, lead };
|
|
16318
|
+
}
|
|
16319
|
+
var NEW_FOR_DAYS = 30;
|
|
16320
|
+
var DAY_MS = 864e5;
|
|
16321
|
+
var dayOf = (entry) => Date.parse(`${entry.date}T00:00:00Z`);
|
|
16322
|
+
function newUntil(entry) {
|
|
16323
|
+
return dayOf(entry) + NEW_FOR_DAYS * DAY_MS;
|
|
16324
|
+
}
|
|
16325
|
+
function newFeaturesFor(releases, install) {
|
|
16326
|
+
const { firstVersion, used, now: now2 } = install;
|
|
16327
|
+
if (!firstVersion) return [];
|
|
16328
|
+
return releases.filter((r) => compareSemver(r.version, firstVersion) > 0 && now2 < newUntil(r)).flatMap((r) => r.newFeatures ?? []).filter((f) => !used.includes(f));
|
|
16329
|
+
}
|
|
15934
16330
|
|
|
15935
16331
|
// src/routes/updates.ts
|
|
15936
16332
|
function registerUpdateRoutes(app, deps) {
|
|
@@ -16089,38 +16485,68 @@ function registerUpdateRoutes(app, deps) {
|
|
|
16089
16485
|
});
|
|
16090
16486
|
}, 50);
|
|
16091
16487
|
});
|
|
16488
|
+
const raiseSeen = (v) => {
|
|
16489
|
+
const was = core.store.getSetting("whatsnew.seen");
|
|
16490
|
+
if (!was || compareSemver(v, was) > 0) core.store.setSetting("whatsnew.seen", v);
|
|
16491
|
+
};
|
|
16092
16492
|
app.get("/api/release/notes", async () => {
|
|
16093
|
-
|
|
16094
|
-
|
|
16095
|
-
|
|
16096
|
-
|
|
16493
|
+
const unmarked = !core.store.getSetting("install.firstVersion");
|
|
16494
|
+
if (unmarked) core.store.setSetting("install.firstVersion", meta.version);
|
|
16495
|
+
if (unmarked || !core.store.getSetting("whatsnew.seen")) raiseSeen(meta.version);
|
|
16496
|
+
const seen = core.store.getSetting("whatsnew.seen") || null;
|
|
16097
16497
|
return {
|
|
16098
16498
|
version: meta.version,
|
|
16099
16499
|
entry: releaseFor(meta.version),
|
|
16100
|
-
seen
|
|
16500
|
+
seen,
|
|
16501
|
+
// The in-app history (`recent`), what in it is new here (`unseen`), and
|
|
16502
|
+
// the one headline update allowed to open by itself (`lead`). All of it
|
|
16503
|
+
// is in the build: nothing here reaches the network.
|
|
16504
|
+
...whatsNewWindow(RELEASES, meta.version, seen),
|
|
16101
16505
|
// 0.0.0 is the placeholder release-please has not bumped yet. No tag has
|
|
16102
16506
|
// ever existed for it, and the releases index of a project that has not
|
|
16103
16507
|
// released is an empty page, so there is nowhere honest to point: null.
|
|
16104
|
-
// Its absence is also what tells the dialog it is looking at a
|
|
16105
|
-
// development build rather than a release nobody wrote notes for.
|
|
16106
16508
|
// Every other version a user can be running was published, and
|
|
16107
16509
|
// publishing is what creates the tag. The exceptions are the internal
|
|
16108
16510
|
// 0.1.x builds: published, unpublished, never tagged. Their numbers are
|
|
16109
16511
|
// burned on npm and a tag link would 404.
|
|
16110
16512
|
changelogUrl: ghSlug && meta.version !== UNRELEASED && !UNTAGGED.has(meta.version) ? `https://github.com/${ghSlug}/releases/tag/v${meta.version}` : null,
|
|
16111
|
-
//
|
|
16112
|
-
// a tag: this one is the archive,
|
|
16113
|
-
// Gated on there being an archive
|
|
16114
|
-
//
|
|
16115
|
-
//
|
|
16116
|
-
releasesUrl: ghSlug && RELEASES.some(isNewsworthy) ? `https://github.com/${ghSlug}/releases` : null
|
|
16513
|
+
// Full release notes: everything older than the in-app window, and every
|
|
16514
|
+
// fix a record left out. The index, not a tag: this one is the archive,
|
|
16515
|
+
// and it outlives the version running. Gated on there being an archive
|
|
16516
|
+
// at all, so a project that has never published does not offer a link
|
|
16517
|
+
// to an empty page.
|
|
16518
|
+
releasesUrl: ghSlug && RELEASES.some(isNewsworthy) ? `https://github.com/${ghSlug}/releases` : null,
|
|
16519
|
+
// The few features saying New on this install, from the same record
|
|
16520
|
+
// (DESIGN.md, "New"). Read after the stamping above, so a new home's
|
|
16521
|
+
// first read has none: everything is new to it.
|
|
16522
|
+
newFeatures: newFeaturesFor(RELEASES, {
|
|
16523
|
+
firstVersion: core.store.getSetting("install.firstVersion"),
|
|
16524
|
+
used: usedFeatures(),
|
|
16525
|
+
now: Date.now()
|
|
16526
|
+
})
|
|
16117
16527
|
};
|
|
16118
16528
|
});
|
|
16119
16529
|
app.post("/api/release/seen", async (req) => {
|
|
16120
16530
|
const v = req.body?.version;
|
|
16121
|
-
|
|
16531
|
+
raiseSeen(typeof v === "string" && isReleaseTriplet(v) ? v : meta.version);
|
|
16122
16532
|
return { ok: true };
|
|
16123
16533
|
});
|
|
16534
|
+
app.post("/api/release/used", async (req, reply) => {
|
|
16535
|
+
const f = req.body?.feature;
|
|
16536
|
+
if (typeof f !== "string" || !RELEASES.some((r) => r.newFeatures?.includes(f)))
|
|
16537
|
+
return reply.status(400).send({ error: "not a feature any release marked New" });
|
|
16538
|
+
const used = usedFeatures();
|
|
16539
|
+
if (!used.includes(f)) core.store.setSetting("features.used", JSON.stringify([...used, f]));
|
|
16540
|
+
return { ok: true };
|
|
16541
|
+
});
|
|
16542
|
+
function usedFeatures() {
|
|
16543
|
+
try {
|
|
16544
|
+
const v = JSON.parse(core.store.getSetting("features.used") ?? "[]");
|
|
16545
|
+
return Array.isArray(v) ? v.filter((x) => typeof x === "string") : [];
|
|
16546
|
+
} catch {
|
|
16547
|
+
return [];
|
|
16548
|
+
}
|
|
16549
|
+
}
|
|
16124
16550
|
}
|
|
16125
16551
|
function registerSystemRoutes(app, deps) {
|
|
16126
16552
|
const { core, thumbs } = deps;
|
|
@@ -16143,10 +16569,19 @@ function registerSystemRoutes(app, deps) {
|
|
|
16143
16569
|
app.post("/api/system/reveal", async (req, reply) => {
|
|
16144
16570
|
if (!fromThisComputer(req)) return reply.status(403).send({ error: "Only on the computer running Scenri." });
|
|
16145
16571
|
const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "explorer" : "xdg-open";
|
|
16146
|
-
|
|
16147
|
-
|
|
16148
|
-
|
|
16149
|
-
|
|
16572
|
+
const opened = await new Promise((resolve) => {
|
|
16573
|
+
try {
|
|
16574
|
+
const child = spawn(cmd, [core.home], { detached: true, stdio: "ignore" });
|
|
16575
|
+
child.on("error", () => resolve(false));
|
|
16576
|
+
child.once("spawn", () => {
|
|
16577
|
+
child.unref();
|
|
16578
|
+
resolve(true);
|
|
16579
|
+
});
|
|
16580
|
+
} catch {
|
|
16581
|
+
resolve(false);
|
|
16582
|
+
}
|
|
16583
|
+
});
|
|
16584
|
+
if (!opened) return reply.status(501).send({ error: "No file manager could be opened on this computer." });
|
|
16150
16585
|
return reply.send({ ok: true });
|
|
16151
16586
|
});
|
|
16152
16587
|
app.get("/api/export/all", async (_req, reply) => {
|
|
@@ -16176,6 +16611,7 @@ function registerSystemRoutes(app, deps) {
|
|
|
16176
16611
|
const scope = String(req.query?.scope ?? "");
|
|
16177
16612
|
if (scope !== "shots" && scope !== "all") return reply.status(400).send({ error: "scope must be shots or all" });
|
|
16178
16613
|
if (scope === "shots") {
|
|
16614
|
+
deps.stopAllRuns();
|
|
16179
16615
|
let removed = 0;
|
|
16180
16616
|
for (const brand of core.store.listBrands()) {
|
|
16181
16617
|
for (const set of core.store.listSets(brand.id)) core.store.deleteSet(set.id);
|
|
@@ -16446,7 +16882,7 @@ function registerDesktopRoutes(app, deps) {
|
|
|
16446
16882
|
record: null
|
|
16447
16883
|
};
|
|
16448
16884
|
}
|
|
16449
|
-
const { installDeps } = await import('./cli-
|
|
16885
|
+
const { installDeps } = await import('./cli-WRNWT6P7.js');
|
|
16450
16886
|
const { desktopStatus } = await import('./install-VXJPLYWM.js');
|
|
16451
16887
|
return desktopStatus(installDeps(runtime.entry));
|
|
16452
16888
|
});
|
|
@@ -16454,7 +16890,7 @@ function registerDesktopRoutes(app, deps) {
|
|
|
16454
16890
|
if (!runtime.entry) {
|
|
16455
16891
|
return { ok: false, reason: "unsupported", message: "Desktop shortcuts are not available on this system yet." };
|
|
16456
16892
|
}
|
|
16457
|
-
const { addToDesktop } = await import('./cli-
|
|
16893
|
+
const { addToDesktop } = await import('./cli-WRNWT6P7.js');
|
|
16458
16894
|
return addToDesktop(runtime.entry);
|
|
16459
16895
|
});
|
|
16460
16896
|
app.get("/api/desktop", async (req) => {
|
|
@@ -16565,6 +17001,16 @@ function buildServer(opts) {
|
|
|
16565
17001
|
});
|
|
16566
17002
|
const reserved = /* @__PURE__ */ new Map();
|
|
16567
17003
|
const runningGenerations = /* @__PURE__ */ new Map();
|
|
17004
|
+
const stopRuns = (belongs) => {
|
|
17005
|
+
const stopped = /* @__PURE__ */ new Set();
|
|
17006
|
+
for (const [id, ctrl] of runningGenerations) {
|
|
17007
|
+
if (stopped.has(ctrl)) continue;
|
|
17008
|
+
const node = core.store.getNode(id);
|
|
17009
|
+
if (!node || !belongs(node)) continue;
|
|
17010
|
+
ctrl.abort();
|
|
17011
|
+
stopped.add(ctrl);
|
|
17012
|
+
}
|
|
17013
|
+
};
|
|
16568
17014
|
core.catalog.reconcileInterruptedJobs();
|
|
16569
17015
|
const thumbs = createThumbStore(core);
|
|
16570
17016
|
const { scenes } = loadScenes(opts.templatesDir);
|
|
@@ -16631,6 +17077,8 @@ function buildServer(opts) {
|
|
|
16631
17077
|
const quiet = {
|
|
16632
17078
|
core};
|
|
16633
17079
|
for (const d of listPresenterDrafts(core, id)) await discardPresenterDraft(quiet, d.id, hooks);
|
|
17080
|
+
stopRuns((node) => core.store.getProject(node.projectId)?.brandId === id);
|
|
17081
|
+
for (const job of core.catalog.listJobs(id)) cancelCatalogImport(job.id);
|
|
16634
17082
|
}
|
|
16635
17083
|
core.store.deleteBrand(id);
|
|
16636
17084
|
if (brand) {
|
|
@@ -16675,7 +17123,9 @@ function buildServer(opts) {
|
|
|
16675
17123
|
).slice(0, 80).join("").trim() || spec.fallback;
|
|
16676
17124
|
}
|
|
16677
17125
|
const id = `${spec.prefix}-${randomUUID().slice(0, 8)}`;
|
|
16678
|
-
const
|
|
17126
|
+
const current = core.store.getBrand(brand.id);
|
|
17127
|
+
if (!current) return reply.status(404).send({ error: "brand not found" });
|
|
17128
|
+
const json = { ...current.json };
|
|
16679
17129
|
json[spec.key] = [
|
|
16680
17130
|
...json[spec.key] ?? [],
|
|
16681
17131
|
{
|
|
@@ -16713,7 +17163,9 @@ function buildServer(opts) {
|
|
|
16713
17163
|
inspectMark: (buf) => inspectMark(buf, toMarkPng),
|
|
16714
17164
|
createdWith: `${meta.name}/${meta.version}`
|
|
16715
17165
|
});
|
|
16716
|
-
const
|
|
17166
|
+
const current = core.store.getBrand(brand.id);
|
|
17167
|
+
if (!current) return reply.status(404).send({ error: "brand not found" });
|
|
17168
|
+
const { brand: merged, suggestions } = mergeScrape(current.json, scraped);
|
|
16717
17169
|
const v = validateBrand(merged);
|
|
16718
17170
|
if (!v.valid) return reply.status(400).send({ error: "brand became invalid", details: v.errors });
|
|
16719
17171
|
const row = core.store.updateBrand(brand.id, merged);
|
|
@@ -16742,11 +17194,9 @@ function buildServer(opts) {
|
|
|
16742
17194
|
if (!brand) return reply.status(404).send({ error: "brand not found" });
|
|
16743
17195
|
const productId = String(req.params.productId);
|
|
16744
17196
|
const catalogId2 = productId.startsWith("cat-") ? productId.slice(4) : null;
|
|
16745
|
-
const
|
|
16746
|
-
const products = json.products ?? [];
|
|
16747
|
-
const idx = catalogId2 ? -1 : products.findIndex((p) => p.id === productId);
|
|
17197
|
+
const owned = (json2) => (json2.products ?? []).findIndex((p) => p.id === productId);
|
|
16748
17198
|
const catalogRow = catalogId2 ? core.catalog.getProduct(catalogId2) : null;
|
|
16749
|
-
if (
|
|
17199
|
+
if ((catalogId2 ? -1 : owned(brand.json)) === -1 && (!catalogRow || catalogRow.brandId !== brand.id)) {
|
|
16750
17200
|
return reply.status(404).send({ error: "product not found" });
|
|
16751
17201
|
}
|
|
16752
17202
|
const part = await readImagePart(core, req, toPng);
|
|
@@ -16757,6 +17207,11 @@ function buildServer(opts) {
|
|
|
16757
17207
|
core.catalog.addLocalImage(catalogId2, `asset:${hash}`, angle ?? null);
|
|
16758
17208
|
return core.store.getBrand(brand.id);
|
|
16759
17209
|
}
|
|
17210
|
+
const current = core.store.getBrand(brand.id);
|
|
17211
|
+
const json = { ...current?.json };
|
|
17212
|
+
const products = json.products ?? [];
|
|
17213
|
+
const idx = current ? owned(json) : -1;
|
|
17214
|
+
if (idx === -1) return reply.status(404).send({ error: "product not found" });
|
|
16760
17215
|
const shot = { file: `asset:${hash}`, locked: true };
|
|
16761
17216
|
if (angle) shot.angle = angle;
|
|
16762
17217
|
json.products = products.map((p, i) => i === idx ? { ...p, shots: [...p.shots ?? [], shot] } : p);
|
|
@@ -17008,6 +17463,8 @@ function buildServer(opts) {
|
|
|
17008
17463
|
}
|
|
17009
17464
|
}
|
|
17010
17465
|
if (inheritedPerson) inheritedDirectives.push(personSkinDirective());
|
|
17466
|
+
const parentImages = core.store.getNode(parentId)?.images ?? [];
|
|
17467
|
+
const source = opts2?.source ?? (parentImages.length === 1 ? parentImages[0] : void 0);
|
|
17011
17468
|
const compileCtx = {
|
|
17012
17469
|
brand: brandJson,
|
|
17013
17470
|
images: core.images,
|
|
@@ -17016,6 +17473,9 @@ function buildServer(opts) {
|
|
|
17016
17473
|
template: brief.templateId ? sceneById(String(brief.templateId)) : void 0,
|
|
17017
17474
|
templateById: sceneById,
|
|
17018
17475
|
mode: "edit",
|
|
17476
|
+
// The frame rides on its own, so a picture this refine attaches that is
|
|
17477
|
+
// the same bytes is never sent a second time beside it.
|
|
17478
|
+
...source ? { sourceHash: String(source) } : {},
|
|
17019
17479
|
editScope: verdict.scope,
|
|
17020
17480
|
editRemoval: verdict.removal ?? false,
|
|
17021
17481
|
// Kinds, not a count: the identity claim speaks only about the kinds
|
|
@@ -17079,7 +17539,8 @@ function buildServer(opts) {
|
|
|
17079
17539
|
if (parentId && core.store.getNode(String(parentId))) {
|
|
17080
17540
|
const previewReshape = req.body.reshape;
|
|
17081
17541
|
const edit = await compileEditBrief(brand.id, String(parentId), brief, engine.capabilities(), {
|
|
17082
|
-
reshape: previewReshape === "extend" ? "extend" : previewReshape === "crop" ? "crop" : void 0
|
|
17542
|
+
reshape: previewReshape === "extend" ? "extend" : previewReshape === "crop" ? "crop" : void 0,
|
|
17543
|
+
source: req.body.sourceImage
|
|
17083
17544
|
});
|
|
17084
17545
|
const { referenceImages: referenceImages2, ...rest2 } = edit.compiled;
|
|
17085
17546
|
return {
|
|
@@ -17379,6 +17840,8 @@ function buildServer(opts) {
|
|
|
17379
17840
|
let { width = 1024, height = 1024 } = req.body;
|
|
17380
17841
|
const project = core.store.getProject(String(projectId));
|
|
17381
17842
|
if (!project) return reply.status(404).send({ error: "project not found" });
|
|
17843
|
+
if (parentId && core.store.getNode(String(parentId))?.projectId !== project.id)
|
|
17844
|
+
return reply.status(404).send({ error: "That shot is no longer there." });
|
|
17382
17845
|
const rawReshape = req.body.reshape ?? req.body.brief?.reshape;
|
|
17383
17846
|
const reshape = rawReshape === "crop" ? "crop" : rawReshape === "extend" ? "extend" : void 0;
|
|
17384
17847
|
const runCropNode = async (args) => {
|
|
@@ -17409,7 +17872,8 @@ function buildServer(opts) {
|
|
|
17409
17872
|
void runNode([node2.id], null, 0, work2, { width: plan2.width, height: plan2.height }).catch(
|
|
17410
17873
|
(err) => app.log.error({ err }, "crop run failed")
|
|
17411
17874
|
);
|
|
17412
|
-
|
|
17875
|
+
const sent2 = core.store.getNode(node2.id) ?? node2;
|
|
17876
|
+
return reply.status(202).send(args.note ? { ...sent2, warnings: [args.note] } : sent2);
|
|
17413
17877
|
};
|
|
17414
17878
|
if (kind === "edit" && reshape === "crop") {
|
|
17415
17879
|
const rootForCrop = core.store.rootFor(project.id);
|
|
@@ -17466,7 +17930,8 @@ function buildServer(opts) {
|
|
|
17466
17930
|
if (briefErrors.length) return reply.status(400).send({ error: `invalid brief: ${briefErrors.join("; ")}` });
|
|
17467
17931
|
if (kind === "edit") {
|
|
17468
17932
|
const edit = await compileEditBrief(project.brandId, resolvedParentId, brief, engine.capabilities(), {
|
|
17469
|
-
reshape
|
|
17933
|
+
reshape,
|
|
17934
|
+
source: req.body.sourceImage
|
|
17470
17935
|
});
|
|
17471
17936
|
compiled2 = edit.compiled;
|
|
17472
17937
|
inheritedTokens = edit.inheritedTokens;
|
|
@@ -17592,13 +18057,13 @@ function buildServer(opts) {
|
|
|
17592
18057
|
const sentRefs = keptRefs && maxEdge ? await Promise.all(keptRefs.map((p) => capReferenceEdge(core, p, maxEdge))) : keptRefs;
|
|
17593
18058
|
const sentRoles = referenceRoles && cap2 > 0 ? referenceRoles.slice(0, cap2) : referenceRoles ?? [];
|
|
17594
18059
|
if (process.env.SCENRI_DEBUG) {
|
|
17595
|
-
const
|
|
17596
|
-
for (const r of sentRoles)
|
|
18060
|
+
const sent2 = {};
|
|
18061
|
+
for (const r of sentRoles) sent2[r] = (sent2[r] ?? 0) + 1;
|
|
17597
18062
|
app.log.info(
|
|
17598
18063
|
{
|
|
17599
18064
|
engine: engine.capabilities().id,
|
|
17600
18065
|
cap: cap2,
|
|
17601
|
-
sent,
|
|
18066
|
+
sent: sent2,
|
|
17602
18067
|
dropped: (compiled2?.dropped ?? []).map((d) => `${d.role}:${d.label} (${d.reason ?? "budget"})`)
|
|
17603
18068
|
},
|
|
17604
18069
|
"reference transport"
|
|
@@ -17671,14 +18136,14 @@ function buildServer(opts) {
|
|
|
17671
18136
|
const editEdge = engine.capabilities().maxReferenceEdge;
|
|
17672
18137
|
if (editEdge) for (const r of editRefs) r.path = await capReferenceEdge(core, r.path, editEdge);
|
|
17673
18138
|
if (process.env.SCENRI_DEBUG) {
|
|
17674
|
-
const
|
|
17675
|
-
for (const r of editRefs)
|
|
18139
|
+
const sent2 = {};
|
|
18140
|
+
for (const r of editRefs) sent2[String(r.role ?? "reference")] = (sent2[String(r.role ?? "reference")] ?? 0) + 1;
|
|
17676
18141
|
app.log.info(
|
|
17677
18142
|
{
|
|
17678
18143
|
engine: engine.capabilities().id,
|
|
17679
18144
|
cap: Math.max(0, engine.capabilities().maxReferenceImages - 1),
|
|
17680
18145
|
sourceFrame: true,
|
|
17681
|
-
sent,
|
|
18146
|
+
sent: sent2,
|
|
17682
18147
|
dropped: (mergedEdit?.dropped ?? []).map((d) => `${d.role}:${d.label} (${d.reason ?? "budget"})`)
|
|
17683
18148
|
},
|
|
17684
18149
|
"reference transport"
|
|
@@ -17987,10 +18452,10 @@ function buildServer(opts) {
|
|
|
17987
18452
|
} : kind === "edit" && original ? async (images) => {
|
|
17988
18453
|
let staged = images;
|
|
17989
18454
|
if (gradeOnlyAsk && !expandPlan) {
|
|
17990
|
-
const
|
|
18455
|
+
const sent2 = budgetSourceHash ? core.images.read(budgetSourceHash) : original;
|
|
17991
18456
|
const out = [];
|
|
17992
18457
|
for (const h of staged) {
|
|
17993
|
-
const g = await gradeComposite(original,
|
|
18458
|
+
const g = await gradeComposite(original, sent2, core.images.read(h));
|
|
17994
18459
|
if (g) {
|
|
17995
18460
|
app.log.info(
|
|
17996
18461
|
{ nodeId: node.id, residual: Number(g.residual.toFixed(2)) },
|
|
@@ -18035,14 +18500,16 @@ function buildServer(opts) {
|
|
|
18035
18500
|
nodeBudgetMs
|
|
18036
18501
|
).catch((err) => app.log.error({ err }, "node run failed"));
|
|
18037
18502
|
const allWarnings = [...compiled2?.warnings ?? [], ...extraWarnings];
|
|
18038
|
-
|
|
18503
|
+
const sent = nodes.map((n) => core.store.getNode(n.id) ?? n);
|
|
18504
|
+
return reply.status(202).send({ ...sent[0], siblings: sent, ...allWarnings.length ? { warnings: allWarnings } : {} });
|
|
18039
18505
|
};
|
|
18040
18506
|
app.post("/api/nodes", async (req, reply) => startNodeRun(req, reply));
|
|
18041
18507
|
app.post("/api/nodes/:id/retry", async (req, reply) => {
|
|
18042
18508
|
const id = req.params.id;
|
|
18043
18509
|
const n = core.store.getNode(id);
|
|
18044
18510
|
if (!n) return reply.status(404).send({ error: "node not found" });
|
|
18045
|
-
if (n.status === "running"
|
|
18511
|
+
if (n.status === "running" || runningGenerations.has(id))
|
|
18512
|
+
return reply.status(409).send({ error: "already running" });
|
|
18046
18513
|
if (n.status === "done") return reply.status(400).send({ error: "finished shots start a new take" });
|
|
18047
18514
|
if (n.status !== "error" && n.status !== "cancelled") {
|
|
18048
18515
|
return reply.status(400).send({ error: "cannot retry this shot" });
|
|
@@ -18164,7 +18631,7 @@ function buildServer(opts) {
|
|
|
18164
18631
|
})();
|
|
18165
18632
|
return drained;
|
|
18166
18633
|
});
|
|
18167
|
-
registerSystemRoutes(app, { core, thumbs });
|
|
18634
|
+
registerSystemRoutes(app, { core, thumbs, stopAllRuns: () => stopRuns(() => true) });
|
|
18168
18635
|
registerPhoneRoutes(app, { phone });
|
|
18169
18636
|
registerGuideRoutes(app, { core, version: meta.version });
|
|
18170
18637
|
registerDesktopRoutes(app, {
|
|
@@ -18264,6 +18731,12 @@ async function serve() {
|
|
|
18264
18731
|
}
|
|
18265
18732
|
}
|
|
18266
18733
|
async function run3() {
|
|
18734
|
+
const supervised = process.env.SCENRI_SUPERVISED === "1";
|
|
18735
|
+
const installKind = detectInstallKind(fileURLToPath(import.meta.url), defaultHome());
|
|
18736
|
+
const theirs = await runningScenri();
|
|
18737
|
+
if (theirs && shouldAdoptRunning({ installKind, ourHome: defaultHome(), theirHome: theirs.home })) {
|
|
18738
|
+
await adoptRunning();
|
|
18739
|
+
}
|
|
18267
18740
|
const core = createCore();
|
|
18268
18741
|
const stubs = process.env.SCENRI_DEMO_ENGINE === "1" ? [createDemoEngine((b) => core.images.save(b), demoOptionsFromEnv(process.env))] : [];
|
|
18269
18742
|
const engines = createEngineRegistry(core, stubs);
|
|
@@ -18276,8 +18749,6 @@ async function run3() {
|
|
|
18276
18749
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
18277
18750
|
const candidates = [join(here, "..", "..", "..", "apps", "studio", "dist"), join(here, "..", "studio-dist")];
|
|
18278
18751
|
const studioDist = candidates.find((p) => existsSync(p));
|
|
18279
|
-
const supervised = process.env.SCENRI_SUPERVISED === "1";
|
|
18280
|
-
const installKind = detectInstallKind(fileURLToPath(import.meta.url), core.home);
|
|
18281
18752
|
const named = isLoopbackName(HOST) || isWildcardHost(HOST) || isIPv4Literal(HOST) ? [] : [HOST];
|
|
18282
18753
|
const app = buildServer({
|
|
18283
18754
|
core,
|
|
@@ -18304,34 +18775,17 @@ async function run3() {
|
|
|
18304
18775
|
await new Promise((r) => setTimeout(r, 300));
|
|
18305
18776
|
continue;
|
|
18306
18777
|
}
|
|
18307
|
-
|
|
18308
|
-
|
|
18309
|
-
|
|
18310
|
-
|
|
18311
|
-
|
|
18312
|
-
|
|
18313
|
-
if (!shouldAdoptRunning({ installKind, ourHome: core.home, theirHome: info.home })) {
|
|
18314
|
-
console.error("");
|
|
18315
|
-
for (const line of anotherScenriLines(PORT, info.home ?? "another library", core.home)) {
|
|
18316
|
-
console.error(` ${line}`);
|
|
18317
|
-
}
|
|
18318
|
-
console.error("");
|
|
18319
|
-
process.exit(1);
|
|
18778
|
+
const theirs2 = await runningScenri();
|
|
18779
|
+
if (theirs2) {
|
|
18780
|
+
if (!shouldAdoptRunning({ installKind, ourHome: core.home, theirHome: theirs2.home })) {
|
|
18781
|
+
console.error("");
|
|
18782
|
+
for (const line of anotherScenriLines(PORT, theirs2.home ?? "another library", core.home)) {
|
|
18783
|
+
console.error(` ${line}`);
|
|
18320
18784
|
}
|
|
18321
|
-
|
|
18322
|
-
|
|
18323
|
-
Scenri is already running \u2192 ${url}
|
|
18324
|
-
`);
|
|
18325
|
-
if (process.env.SCENRI_NO_OPEN !== "1") {
|
|
18326
|
-
try {
|
|
18327
|
-
const { default: open } = await import('open');
|
|
18328
|
-
await open(url);
|
|
18329
|
-
} catch {
|
|
18330
|
-
}
|
|
18331
|
-
}
|
|
18332
|
-
process.exit(0);
|
|
18785
|
+
console.error("");
|
|
18786
|
+
process.exit(1);
|
|
18333
18787
|
}
|
|
18334
|
-
|
|
18788
|
+
await adoptRunning();
|
|
18335
18789
|
}
|
|
18336
18790
|
console.error("");
|
|
18337
18791
|
for (const line of portBusyLines(PORT)) console.error(` ${line}`);
|
|
@@ -18345,13 +18799,14 @@ async function run3() {
|
|
|
18345
18799
|
const shutdown = (signal) => {
|
|
18346
18800
|
if (closing2) return;
|
|
18347
18801
|
closing2 = true;
|
|
18348
|
-
console.log(`
|
|
18802
|
+
if (signal !== "SIGHUP") console.log(`
|
|
18349
18803
|
${signal}: closing Scenri\u2026`);
|
|
18350
18804
|
setTimeout(() => process.exit(1), 5e3).unref();
|
|
18351
18805
|
app.drain().then(() => process.exit(0)).catch(() => process.exit(1));
|
|
18352
18806
|
};
|
|
18353
18807
|
process.on("SIGINT", shutdown);
|
|
18354
18808
|
process.on("SIGTERM", shutdown);
|
|
18809
|
+
process.on("SIGHUP", shutdown);
|
|
18355
18810
|
void repairPresenterCrops(core, (line) => console.log(line)).catch(() => void 0);
|
|
18356
18811
|
app.updates.schedule();
|
|
18357
18812
|
app.content.schedule();
|
|
@@ -18385,7 +18840,7 @@ async function run3() {
|
|
|
18385
18840
|
}
|
|
18386
18841
|
}
|
|
18387
18842
|
const ownEntry = fileURLToPath(import.meta.url);
|
|
18388
|
-
const { addToDesktop, installDeps } = await import('./cli-
|
|
18843
|
+
const { addToDesktop, installDeps } = await import('./cli-WRNWT6P7.js');
|
|
18389
18844
|
const { refreshLauncher } = await import('./refresh-5HBDLLLB.js');
|
|
18390
18845
|
const { askOnTerminal, offerDesktop, shouldOfferDesktop } = await import('./offer-W6NJXMWT.js');
|
|
18391
18846
|
const { launcherInstalled } = await import('./paths-6ZXRZUHY.js');
|
|
@@ -18427,6 +18882,29 @@ async function verify() {
|
|
|
18427
18882
|
process.exit(1);
|
|
18428
18883
|
}
|
|
18429
18884
|
}
|
|
18885
|
+
async function runningScenri() {
|
|
18886
|
+
try {
|
|
18887
|
+
const res = await fetch(`http://127.0.0.1:${PORT}/api/version`, { signal: AbortSignal.timeout(2e3) });
|
|
18888
|
+
const info = await res.json();
|
|
18889
|
+
return info.name === readMeta().name ? info : null;
|
|
18890
|
+
} catch {
|
|
18891
|
+
return null;
|
|
18892
|
+
}
|
|
18893
|
+
}
|
|
18894
|
+
async function adoptRunning() {
|
|
18895
|
+
const url = `http://127.0.0.1:${PORT}`;
|
|
18896
|
+
console.log(`
|
|
18897
|
+
Scenri is already running \u2192 ${url}
|
|
18898
|
+
`);
|
|
18899
|
+
if (process.env.SCENRI_NO_OPEN !== "1") {
|
|
18900
|
+
try {
|
|
18901
|
+
const { default: open } = await import('open');
|
|
18902
|
+
await open(url);
|
|
18903
|
+
} catch {
|
|
18904
|
+
}
|
|
18905
|
+
}
|
|
18906
|
+
process.exit(0);
|
|
18907
|
+
}
|
|
18430
18908
|
|
|
18431
18909
|
export { serve, verify };
|
|
18432
18910
|
//# sourceMappingURL=serve.js.map
|