scenri 0.6.5 → 0.6.7
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 +20 -0
- package/dist/serve.js +160 -51
- package/package.json +1 -1
- package/studio-dist/assets/index-B8Town4N.js +103 -0
- package/studio-dist/assets/{index-DWWdEXLo.css → index-BG-cxlng.css} +1 -1
- package/studio-dist/index.html +2 -2
- package/studio-dist/assets/index-D__G1gHp.js +0 -103
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.6.7](https://github.com/tonygorb/Scenri/compare/v0.6.6...v0.6.7) (2026-08-29)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
* a scene nobody chose never lands in the brief, or comes back ([db00c8f](https://github.com/tonygorb/Scenri/commit/db00c8f73845a3fab2a453c97acec3e79f846b5c))
|
|
9
|
+
* a scene nobody chose never lands in the brief, or comes back ([1fa34d3](https://github.com/tonygorb/Scenri/commit/1fa34d37699addda945ef31543fe643774e6afb6))
|
|
10
|
+
|
|
11
|
+
## [0.6.6](https://github.com/tonygorb/Scenri/compare/v0.6.5...v0.6.6) (2026-08-29)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
### Bug Fixes
|
|
15
|
+
|
|
16
|
+
* composer chip removal is deterministic ([11246fc](https://github.com/tonygorb/Scenri/commit/11246fc64e26be522631f0fd6927210d6b5759b5))
|
|
17
|
+
* composer chip removal is deterministic ([29b9a4b](https://github.com/tonygorb/Scenri/commit/29b9a4bffaee289d5066bf15b52254d030547d97))
|
|
18
|
+
* presenter avatars frame head and shoulders like a real portrait ([3edb693](https://github.com/tonygorb/Scenri/commit/3edb69360755280af822380bd7cf3f65297aa258))
|
|
19
|
+
* presenter avatars frame head and shoulders like a real portrait ([1e4040c](https://github.com/tonygorb/Scenri/commit/1e4040c6c07858994ccde2a862927a6047b02062))
|
|
20
|
+
* stop killing healthy codex runs and classify what actually failed ([0d7888b](https://github.com/tonygorb/Scenri/commit/0d7888bcb3da875775ab125797cdf0b3fc7dec0c))
|
|
21
|
+
* stop killing healthy codex runs and classify what actually failed ([87e4df3](https://github.com/tonygorb/Scenri/commit/87e4df3ee680ad7673fad73351b1f36c07dacba6))
|
|
22
|
+
|
|
3
23
|
## [0.6.5](https://github.com/tonygorb/Scenri/compare/v0.6.4...v0.6.5) (2026-08-29)
|
|
4
24
|
|
|
5
25
|
|
package/dist/serve.js
CHANGED
|
@@ -8,7 +8,7 @@ import Database from 'better-sqlite3';
|
|
|
8
8
|
import { randomBytes, createHash, randomUUID, timingSafeEqual } from 'crypto';
|
|
9
9
|
import fs, { realpathSync, existsSync, readFileSync, mkdirSync, chmodSync, writeFileSync, readdirSync, statSync, rmSync, renameSync } from 'fs';
|
|
10
10
|
import { fileURLToPath } from 'url';
|
|
11
|
-
import { readFile, copyFile, mkdtemp, rm, readdir,
|
|
11
|
+
import { readFile, copyFile, stat, mkdtemp, rm, readdir, writeFile } from 'fs/promises';
|
|
12
12
|
import { spawn } from 'child_process';
|
|
13
13
|
import sharp7 from 'sharp';
|
|
14
14
|
import Fastify from 'fastify';
|
|
@@ -1967,7 +1967,7 @@ var NOT_AUTHENTICATED_REASON = "Codex CLI is installed but not signed in";
|
|
|
1967
1967
|
var UNVERIFIED_REASON = "Could not verify Codex on this computer";
|
|
1968
1968
|
var DEFAULT_TIMEOUT_MS2 = 3e5;
|
|
1969
1969
|
var PROBE_TIMEOUT_MS = 1e4;
|
|
1970
|
-
var
|
|
1970
|
+
var FIRST_OUTPUT_TIMEOUT_MS = 6e4;
|
|
1971
1971
|
var PROBE_TTL_MS = 3e4;
|
|
1972
1972
|
function execArgs(dir, effort = "low") {
|
|
1973
1973
|
return [
|
|
@@ -2002,6 +2002,13 @@ function killTree(child, platform, spawnImpl) {
|
|
|
2002
2002
|
} catch {
|
|
2003
2003
|
}
|
|
2004
2004
|
}
|
|
2005
|
+
if (platform !== "win32" && child.pid && spawnImpl === spawn) {
|
|
2006
|
+
try {
|
|
2007
|
+
process.kill(-child.pid, "SIGTERM");
|
|
2008
|
+
return;
|
|
2009
|
+
} catch {
|
|
2010
|
+
}
|
|
2011
|
+
}
|
|
2005
2012
|
try {
|
|
2006
2013
|
child.kill();
|
|
2007
2014
|
} catch {
|
|
@@ -2009,10 +2016,10 @@ function killTree(child, platform, spawnImpl) {
|
|
|
2009
2016
|
}
|
|
2010
2017
|
function createRunner(opts = {}) {
|
|
2011
2018
|
const spawnImpl = opts.spawnImpl ?? spawn;
|
|
2012
|
-
const
|
|
2019
|
+
const defaultTimeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS2;
|
|
2013
2020
|
const probeTimeoutMs = opts.probeTimeoutMs ?? PROBE_TIMEOUT_MS;
|
|
2014
2021
|
const probeTtlMs = opts.probeTtlMs ?? PROBE_TTL_MS;
|
|
2015
|
-
const
|
|
2022
|
+
const firstOutputMs = opts.firstOutputMs ?? FIRST_OUTPUT_TIMEOUT_MS;
|
|
2016
2023
|
const platform = opts.platform ?? process.platform;
|
|
2017
2024
|
const killCodex = (child) => killTree(child, platform, spawnImpl);
|
|
2018
2025
|
let resolved = null;
|
|
@@ -2023,10 +2030,11 @@ function createRunner(opts = {}) {
|
|
|
2023
2030
|
const winArg = (a) => `"${a.replace(/[\r\n]+/g, " ").replace(/"/g, "'").replace(/%/g, " percent ")}"`;
|
|
2024
2031
|
const spawnCodex = (exe, args, stdinOpen) => {
|
|
2025
2032
|
const stdio = [stdinOpen ? "pipe" : "ignore", "pipe", "pipe"];
|
|
2026
|
-
return exe.direct ? spawnImpl(exe.command, args, { stdio }) : spawnImpl([exe.command, ...args.map(winArg)].join(" "), [], { stdio, shell: true });
|
|
2033
|
+
return exe.direct ? spawnImpl(exe.command, args, { stdio, ...platform !== "win32" ? { detached: true } : {} }) : spawnImpl([exe.command, ...args.map(winArg)].join(" "), [], { stdio, shell: true });
|
|
2027
2034
|
};
|
|
2028
2035
|
async function run2(args, signal, io) {
|
|
2029
2036
|
const exe = await resolution();
|
|
2037
|
+
const timeoutMs = io?.timeoutMs ?? defaultTimeoutMs;
|
|
2030
2038
|
return new Promise((resolve, reject) => {
|
|
2031
2039
|
let child;
|
|
2032
2040
|
try {
|
|
@@ -2037,19 +2045,31 @@ function createRunner(opts = {}) {
|
|
|
2037
2045
|
}
|
|
2038
2046
|
let settled = false;
|
|
2039
2047
|
let stderr = "";
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
}
|
|
2046
|
-
function onSilence() {
|
|
2048
|
+
const spawnedAt = Date.now();
|
|
2049
|
+
let firstByteAt = 0;
|
|
2050
|
+
let lastByteAt = 0;
|
|
2051
|
+
let maxGapMs = 0;
|
|
2052
|
+
const firstOutputTimer = setTimeout(() => {
|
|
2047
2053
|
finish(
|
|
2054
|
+
"first-output-timeout",
|
|
2048
2055
|
() => reject(
|
|
2049
|
-
new Error(
|
|
2056
|
+
new Error(
|
|
2057
|
+
`Codex CLI produced no output for ${Math.round(firstOutputMs / 1e3)}s after launch, treating it as stuck`
|
|
2058
|
+
)
|
|
2050
2059
|
)
|
|
2051
2060
|
);
|
|
2052
2061
|
killCodex(child);
|
|
2062
|
+
}, firstOutputMs);
|
|
2063
|
+
function sawActivity() {
|
|
2064
|
+
if (settled) return;
|
|
2065
|
+
const now = Date.now();
|
|
2066
|
+
if (firstByteAt === 0) {
|
|
2067
|
+
firstByteAt = now;
|
|
2068
|
+
clearTimeout(firstOutputTimer);
|
|
2069
|
+
} else {
|
|
2070
|
+
maxGapMs = Math.max(maxGapMs, now - lastByteAt);
|
|
2071
|
+
}
|
|
2072
|
+
lastByteAt = now;
|
|
2053
2073
|
}
|
|
2054
2074
|
child.stdout?.on("data", sawActivity);
|
|
2055
2075
|
child.stderr?.on("data", (d) => {
|
|
@@ -2063,19 +2083,23 @@ function createRunner(opts = {}) {
|
|
|
2063
2083
|
child.stdin?.end();
|
|
2064
2084
|
}
|
|
2065
2085
|
const timer = setTimeout(() => {
|
|
2066
|
-
finish(() => reject(new Error(`Codex CLI timed out after ${timeoutMs}ms`)));
|
|
2086
|
+
finish("hard-timeout", () => reject(new Error(`Codex CLI timed out after ${timeoutMs}ms`)));
|
|
2067
2087
|
killCodex(child);
|
|
2068
2088
|
}, timeoutMs);
|
|
2069
2089
|
const onAbort = () => {
|
|
2070
|
-
finish(() => reject(new Error("Codex CLI run aborted")));
|
|
2090
|
+
finish("abort", () => reject(new Error("Codex CLI run aborted")));
|
|
2071
2091
|
killCodex(child);
|
|
2072
2092
|
};
|
|
2073
|
-
function finish(fn) {
|
|
2093
|
+
function finish(outcome, fn) {
|
|
2074
2094
|
if (settled) return;
|
|
2075
2095
|
settled = true;
|
|
2076
2096
|
clearTimeout(timer);
|
|
2077
|
-
clearTimeout(
|
|
2097
|
+
clearTimeout(firstOutputTimer);
|
|
2078
2098
|
signal?.removeEventListener("abort", onAbort);
|
|
2099
|
+
const ttfb = firstByteAt ? firstByteAt - spawnedAt : -1;
|
|
2100
|
+
const line = `codex exec${io?.label ? ` [${io.label}]` : ""}: outcome=${outcome} ttfb=${ttfb}ms maxGap=${maxGapMs}ms total=${Date.now() - spawnedAt}ms`;
|
|
2101
|
+
if (outcome !== "ok" && outcome !== "abort") console.warn(line);
|
|
2102
|
+
else if (process.env.SCENRI_DEBUG === "1") console.log(line);
|
|
2079
2103
|
fn();
|
|
2080
2104
|
}
|
|
2081
2105
|
if (signal) {
|
|
@@ -2086,14 +2110,15 @@ function createRunner(opts = {}) {
|
|
|
2086
2110
|
signal.addEventListener("abort", onAbort, { once: true });
|
|
2087
2111
|
}
|
|
2088
2112
|
child.on("error", (err) => {
|
|
2089
|
-
finish(() => reject(new Error(`Failed to spawn codex: ${err.message}`)));
|
|
2113
|
+
finish("spawn-error", () => reject(new Error(`Failed to spawn codex: ${err.message}`)));
|
|
2090
2114
|
});
|
|
2091
2115
|
child.on("exit", (code) => {
|
|
2092
2116
|
if (code === 0) {
|
|
2093
|
-
finish(resolve);
|
|
2117
|
+
finish("ok", resolve);
|
|
2094
2118
|
} else {
|
|
2095
2119
|
const snippet2 = stderr.trim().slice(0, 200);
|
|
2096
2120
|
finish(
|
|
2121
|
+
`exit-${code ?? "unknown"}`,
|
|
2097
2122
|
() => reject(new Error(`codex exited with code ${code ?? "unknown"}${snippet2 ? `: ${snippet2}` : ""}`))
|
|
2098
2123
|
);
|
|
2099
2124
|
}
|
|
@@ -2218,7 +2243,10 @@ function createCodexAnalyzer(opts = {}) {
|
|
|
2218
2243
|
for (const ref of refs) {
|
|
2219
2244
|
args.splice(args.length - 1, 0, `--image=${ref}`);
|
|
2220
2245
|
}
|
|
2221
|
-
await runner.run(args, signal, {
|
|
2246
|
+
await runner.run(args, signal, {
|
|
2247
|
+
stdin: buildPrompt(req, refs.length, problems),
|
|
2248
|
+
label: `analyze refs=${refs.length} attempt=${attempt + 1}`
|
|
2249
|
+
});
|
|
2222
2250
|
let raw;
|
|
2223
2251
|
try {
|
|
2224
2252
|
raw = await readFile(join(dir, OUT_FILE), "utf8");
|
|
@@ -2444,6 +2472,10 @@ function createCodexSetup(opts = {}) {
|
|
|
2444
2472
|
}
|
|
2445
2473
|
|
|
2446
2474
|
// ../engines/codex/src/index.ts
|
|
2475
|
+
var CODEX_POOL = 2;
|
|
2476
|
+
function codexNodeBudgetMs(count) {
|
|
2477
|
+
return Math.ceil(Math.max(1, count) / CODEX_POOL) * DEFAULT_TIMEOUT_MS2 + 6e4;
|
|
2478
|
+
}
|
|
2447
2479
|
function createCodexEngine(opts) {
|
|
2448
2480
|
const { saveImage } = opts;
|
|
2449
2481
|
const platform = opts.platform ?? process.platform;
|
|
@@ -2524,7 +2556,15 @@ function createCodexEngine(opts) {
|
|
|
2524
2556
|
* shot being edited. A refine carrying a full identity payload was
|
|
2525
2557
|
* silently editing nothing at all.
|
|
2526
2558
|
*/
|
|
2527
|
-
maxReferenceImages: 5
|
|
2559
|
+
maxReferenceImages: 5,
|
|
2560
|
+
/*
|
|
2561
|
+
* The longest edge a reference is worth sending at. codex's image_gen
|
|
2562
|
+
* reads references at reduced resolution server-side either way, but
|
|
2563
|
+
* the bytes still ride the user's uplink inside the exec's own time
|
|
2564
|
+
* budget — a full-resolution phone-photo PNG is tens of megabytes that
|
|
2565
|
+
* buy nothing. Same cap as brand marks (MARK_MAX_EDGE).
|
|
2566
|
+
*/
|
|
2567
|
+
maxReferenceEdge: 2048
|
|
2528
2568
|
};
|
|
2529
2569
|
},
|
|
2530
2570
|
isAvailable() {
|
|
@@ -2545,13 +2585,18 @@ function createCodexEngine(opts) {
|
|
|
2545
2585
|
{ length: count },
|
|
2546
2586
|
(_, i) => async () => withWorkDir(async (dir) => {
|
|
2547
2587
|
const args = execArgs(dir);
|
|
2588
|
+
let refBytes = 0;
|
|
2548
2589
|
for (const [idx, ref] of refs.entries()) {
|
|
2549
2590
|
const dest = join(dir, `ref-${idx}.png`);
|
|
2550
2591
|
await copyFile(ref, dest);
|
|
2592
|
+
refBytes += (await stat(dest)).size;
|
|
2551
2593
|
args.splice(args.length - 1, 0, `--image=${dest}`);
|
|
2552
2594
|
}
|
|
2553
2595
|
const before = await snapshotGenerated();
|
|
2554
|
-
await runCodex(args, inner.signal, {
|
|
2596
|
+
await runCodex(args, inner.signal, {
|
|
2597
|
+
stdin: buildPrompt2(req, i, roles),
|
|
2598
|
+
label: `gen v${i + 1}/${count} refs=${refs.length} refKB=${Math.round(refBytes / 1024)}`
|
|
2599
|
+
});
|
|
2555
2600
|
return collectImages(dir, before);
|
|
2556
2601
|
})
|
|
2557
2602
|
);
|
|
@@ -2560,7 +2605,7 @@ function createCodexEngine(opts) {
|
|
|
2560
2605
|
let fatal = null;
|
|
2561
2606
|
let next = 0;
|
|
2562
2607
|
try {
|
|
2563
|
-
const workers = Array.from({ length: Math.min(
|
|
2608
|
+
const workers = Array.from({ length: Math.min(CODEX_POOL, count) }, async () => {
|
|
2564
2609
|
while (next < count && !inner.signal.aborted) {
|
|
2565
2610
|
const i = next++;
|
|
2566
2611
|
try {
|
|
@@ -2627,7 +2672,7 @@ function createCodexEngine(opts) {
|
|
|
2627
2672
|
}
|
|
2628
2673
|
const before = await snapshotGenerated();
|
|
2629
2674
|
try {
|
|
2630
|
-
await runCodex(args, signal, { stdin: promptText });
|
|
2675
|
+
await runCodex(args, signal, { stdin: promptText, label: `edit refs=${editRefs.length}` });
|
|
2631
2676
|
} catch (err) {
|
|
2632
2677
|
if (isFatalSetupError(err)) runner.invalidateProbe();
|
|
2633
2678
|
throw err;
|
|
@@ -5947,6 +5992,10 @@ async function cardCrop(core, hash) {
|
|
|
5947
5992
|
return { left: Math.max(0, Math.round((w - width) / 2)), top: 0, width, height };
|
|
5948
5993
|
});
|
|
5949
5994
|
}
|
|
5995
|
+
var AVATAR_FIGURE_FRACTION = 0.22;
|
|
5996
|
+
var AVATAR_HEADROOM = 0.1;
|
|
5997
|
+
var AVATAR_MAX_PX = 512;
|
|
5998
|
+
var FIGURE_TRIM_THRESHOLDS = [12, 25];
|
|
5950
5999
|
async function avatarCrop(core, hash) {
|
|
5951
6000
|
if (!hash || !core.images.has(hash)) return void 0;
|
|
5952
6001
|
let box = null;
|
|
@@ -5955,31 +6004,39 @@ async function avatarCrop(core, hash) {
|
|
|
5955
6004
|
} catch {
|
|
5956
6005
|
box = null;
|
|
5957
6006
|
}
|
|
5958
|
-
return crop(
|
|
5959
|
-
|
|
5960
|
-
|
|
5961
|
-
|
|
5962
|
-
|
|
5963
|
-
|
|
5964
|
-
|
|
5965
|
-
|
|
5966
|
-
|
|
5967
|
-
|
|
6007
|
+
return crop(
|
|
6008
|
+
core,
|
|
6009
|
+
hash,
|
|
6010
|
+
(w, h) => {
|
|
6011
|
+
if (!box) {
|
|
6012
|
+
const size2 = Math.min(w, h, Math.round(h * 0.16));
|
|
6013
|
+
return { left: Math.max(0, Math.round((w - size2) / 2)), top: 0, width: size2, height: size2 };
|
|
6014
|
+
}
|
|
6015
|
+
const size = Math.min(w, h, Math.max(16, Math.round(box.height * AVATAR_FIGURE_FRACTION)));
|
|
6016
|
+
const top = Math.min(Math.max(0, Math.round(box.top - size * AVATAR_HEADROOM)), h - size);
|
|
6017
|
+
const left = Math.min(Math.max(0, Math.round(box.left + box.width / 2 - size / 2)), w - size);
|
|
6018
|
+
return { left, top, width: size, height: size };
|
|
6019
|
+
},
|
|
6020
|
+
AVATAR_MAX_PX
|
|
6021
|
+
);
|
|
5968
6022
|
}
|
|
5969
6023
|
async function figureBox(buf) {
|
|
5970
6024
|
const meta = await sharp7(buf).metadata();
|
|
5971
6025
|
const W = meta.width ?? 0;
|
|
5972
6026
|
const H = meta.height ?? 0;
|
|
5973
6027
|
if (!W || !H) return null;
|
|
5974
|
-
|
|
5975
|
-
|
|
5976
|
-
|
|
5977
|
-
|
|
5978
|
-
|
|
5979
|
-
|
|
5980
|
-
|
|
5981
|
-
|
|
5982
|
-
|
|
6028
|
+
for (const threshold of FIGURE_TRIM_THRESHOLDS) {
|
|
6029
|
+
const { info } = await sharp7(buf).trim({ threshold }).toBuffer({ resolveWithObject: true });
|
|
6030
|
+
const left = Math.abs(info.trimOffsetLeft ?? 0);
|
|
6031
|
+
const top = Math.abs(info.trimOffsetTop ?? 0);
|
|
6032
|
+
const width = info.width ?? 0;
|
|
6033
|
+
const height = info.height ?? 0;
|
|
6034
|
+
if (!width || !height) continue;
|
|
6035
|
+
if (width >= W || height >= H) continue;
|
|
6036
|
+
if (height < H * 0.3 || width < W * 0.05) continue;
|
|
6037
|
+
return { left, top, width, height };
|
|
6038
|
+
}
|
|
6039
|
+
return null;
|
|
5983
6040
|
}
|
|
5984
6041
|
async function presenterCrops(core, hash, mode) {
|
|
5985
6042
|
const previewHash = mode === "generated" ? await cardCrop(core, hash) ?? await cardCropSmart(core, hash) : await cardCropSmart(core, hash) ?? await cardCrop(core, hash);
|
|
@@ -6014,14 +6071,16 @@ async function smartCover(core, hash, box) {
|
|
|
6014
6071
|
return void 0;
|
|
6015
6072
|
}
|
|
6016
6073
|
}
|
|
6017
|
-
async function crop(core, hash, region) {
|
|
6074
|
+
async function crop(core, hash, region, cap2) {
|
|
6018
6075
|
if (!hash || !core.images.has(hash)) return void 0;
|
|
6019
6076
|
try {
|
|
6020
6077
|
const meta = await sharp7(core.images.read(hash)).metadata();
|
|
6021
6078
|
const w = meta.width ?? 0;
|
|
6022
6079
|
const h = meta.height ?? 0;
|
|
6023
6080
|
if (!w || !h) return void 0;
|
|
6024
|
-
|
|
6081
|
+
let pipeline = sharp7(core.images.read(hash)).extract(region(w, h));
|
|
6082
|
+
if (cap2) pipeline = pipeline.resize(cap2, cap2, { fit: "inside", withoutEnlargement: true });
|
|
6083
|
+
const png = await pipeline.png().toBuffer();
|
|
6025
6084
|
return core.images.save(png);
|
|
6026
6085
|
} catch {
|
|
6027
6086
|
return void 0;
|
|
@@ -6986,6 +7045,23 @@ var COST_PROBE = {
|
|
|
6986
7045
|
};
|
|
6987
7046
|
var MARK_MAX_EDGE = 2048;
|
|
6988
7047
|
var toMarkPng = (buf) => sharp7(buf, { density: 384 }).rotate().resize({ width: MARK_MAX_EDGE, height: MARK_MAX_EDGE, fit: "inside", withoutEnlargement: true }).png().toBuffer();
|
|
7048
|
+
var cappedRefs = /* @__PURE__ */ new Map();
|
|
7049
|
+
async function capReferenceEdge(core, path, maxEdge) {
|
|
7050
|
+
const key = `${path}#${maxEdge}`;
|
|
7051
|
+
const hit = cappedRefs.get(key);
|
|
7052
|
+
if (hit) return hit;
|
|
7053
|
+
let out = path;
|
|
7054
|
+
try {
|
|
7055
|
+
const meta = await sharp7(path).metadata();
|
|
7056
|
+
if ((meta.width ?? 0) > maxEdge || (meta.height ?? 0) > maxEdge) {
|
|
7057
|
+
const buf = await sharp7(path).resize({ width: maxEdge, height: maxEdge, fit: "inside", withoutEnlargement: true }).png().toBuffer();
|
|
7058
|
+
out = core.images.pathFor(core.images.save(buf));
|
|
7059
|
+
}
|
|
7060
|
+
} catch {
|
|
7061
|
+
}
|
|
7062
|
+
cappedRefs.set(key, out);
|
|
7063
|
+
return out;
|
|
7064
|
+
}
|
|
6989
7065
|
var readImagePart = async (core, req, normalize2) => {
|
|
6990
7066
|
const part = await req.file();
|
|
6991
7067
|
if (!part) return { error: "multipart file field required" };
|
|
@@ -7302,7 +7378,7 @@ async function repairPresenterCrops(core, log = () => {
|
|
|
7302
7378
|
let repaired = 0;
|
|
7303
7379
|
for (const brand of core.store.listBrands()) {
|
|
7304
7380
|
for (const c of brandCharacters(brand.json)) {
|
|
7305
|
-
if (!
|
|
7381
|
+
if (!c?.id) continue;
|
|
7306
7382
|
try {
|
|
7307
7383
|
const firstShot = c.shots?.[0]?.file;
|
|
7308
7384
|
const hash = typeof firstShot === "string" && firstShot.startsWith("asset:") ? firstShot.slice(6) : null;
|
|
@@ -7999,6 +8075,30 @@ function registerImageRoutes(app, deps) {
|
|
|
7999
8075
|
|
|
8000
8076
|
// src/release/notes.data.ts
|
|
8001
8077
|
var RELEASES = [
|
|
8078
|
+
{
|
|
8079
|
+
version: "0.6.7",
|
|
8080
|
+
date: "2026-08-29",
|
|
8081
|
+
sections: [
|
|
8082
|
+
{
|
|
8083
|
+
heading: "Create",
|
|
8084
|
+
body: "The brief starts empty. A scene you never picked could turn up in it, come back on every later load, and reappear after you removed it and reloaded. Create an image no longer stands a scene in for one you have not shortlisted, and a scene that arrives from a link is spent once it lands rather than waiting in the address to be applied again. Briefs you were part way through writing still come back as they always have."
|
|
8085
|
+
}
|
|
8086
|
+
]
|
|
8087
|
+
},
|
|
8088
|
+
{
|
|
8089
|
+
version: "0.6.6",
|
|
8090
|
+
date: "2026-08-29",
|
|
8091
|
+
sections: [
|
|
8092
|
+
{
|
|
8093
|
+
heading: "Create",
|
|
8094
|
+
body: "Removing a chip from the brief now works on the first click, every time, including right after dragging chips around. Codex generations no longer fail as timeouts while they are still working, and when a run does fail, the message says what actually happened instead of suggesting a second try."
|
|
8095
|
+
},
|
|
8096
|
+
{
|
|
8097
|
+
heading: "Presenters",
|
|
8098
|
+
body: "Custom presenter avatars are cropped like real profile photos, head and shoulders from the front reference. Presenters you made earlier are reframed automatically, including ones that never got an avatar at all."
|
|
8099
|
+
}
|
|
8100
|
+
]
|
|
8101
|
+
},
|
|
8002
8102
|
{
|
|
8003
8103
|
version: "0.6.5",
|
|
8004
8104
|
date: "2026-08-29",
|
|
@@ -9075,19 +9175,21 @@ function buildServer(opts) {
|
|
|
9075
9175
|
}
|
|
9076
9176
|
}
|
|
9077
9177
|
const NODE_TIMEOUT_MS = 6e5;
|
|
9078
|
-
async function runNode(nodeId, engine, estimate, work, expect, post) {
|
|
9178
|
+
async function runNode(nodeId, engine, estimate, work, expect, post, timeoutMs) {
|
|
9079
9179
|
const engineId = engine?.capabilities().id ?? "local";
|
|
9080
9180
|
reserved.set(engineId, (reserved.get(engineId) ?? 0) + estimate);
|
|
9081
9181
|
const ctrl = new AbortController();
|
|
9082
9182
|
runningGenerations.set(nodeId, ctrl);
|
|
9183
|
+
const bound = opts.nodeTimeoutMs ?? timeoutMs ?? NODE_TIMEOUT_MS;
|
|
9083
9184
|
let watchdogFired = false;
|
|
9084
9185
|
const watchdog = setTimeout(() => {
|
|
9085
9186
|
watchdogFired = true;
|
|
9086
9187
|
ctrl.abort();
|
|
9087
|
-
},
|
|
9188
|
+
}, bound);
|
|
9088
9189
|
const startedAt = Date.now();
|
|
9089
9190
|
try {
|
|
9090
9191
|
const result = await work(ctrl.signal);
|
|
9192
|
+
clearTimeout(watchdog);
|
|
9091
9193
|
result.images = await normalizePngs(result.images);
|
|
9092
9194
|
if (post) result.images = await post(result.images);
|
|
9093
9195
|
if (expect) await assertAspect(result.images, expect);
|
|
@@ -9109,7 +9211,8 @@ function buildServer(opts) {
|
|
|
9109
9211
|
}
|
|
9110
9212
|
core.ledger.recordCost(engineId, nodeId, result.costUsd);
|
|
9111
9213
|
} catch (err) {
|
|
9112
|
-
if (watchdogFired)
|
|
9214
|
+
if (watchdogFired)
|
|
9215
|
+
core.store.failNode(nodeId, `generation timed out after ${Math.round(bound / 6e4)} minutes`);
|
|
9113
9216
|
else if (ctrl.signal.aborted) core.store.cancelNode(nodeId);
|
|
9114
9217
|
else core.store.failNode(nodeId, String(err?.message ?? err));
|
|
9115
9218
|
} finally {
|
|
@@ -9303,13 +9406,16 @@ function buildServer(opts) {
|
|
|
9303
9406
|
error: `${engine.capabilities().displayName} cannot carry enough reference images, so ${names} would be named in the prompt but never shown. The result would not be your ${kindWord}. Choose an engine that supports reference images, or remove ${names} from the brief.`
|
|
9304
9407
|
});
|
|
9305
9408
|
}
|
|
9409
|
+
const maxEdge = engine.capabilities().maxReferenceEdge;
|
|
9410
|
+
const keptRefs = referenceImages && cap2 > 0 ? referenceImages.slice(0, cap2) : void 0;
|
|
9411
|
+
const sentRefs = keptRefs && maxEdge ? await Promise.all(keptRefs.map((p) => capReferenceEdge(core, p, maxEdge))) : keptRefs;
|
|
9306
9412
|
const genReq = {
|
|
9307
9413
|
prompt: finalPrompt,
|
|
9308
9414
|
brand: ctx,
|
|
9309
9415
|
width: Number(width),
|
|
9310
9416
|
height: Number(height),
|
|
9311
9417
|
count: Math.min(Math.max(1, Number(count)), 8),
|
|
9312
|
-
...
|
|
9418
|
+
...sentRefs ? { referenceImages: sentRefs } : {},
|
|
9313
9419
|
...referenceRoles && cap2 > 0 ? { referenceRoles: referenceRoles.slice(0, cap2) } : {}
|
|
9314
9420
|
};
|
|
9315
9421
|
estimate = await engine.costEstimate(genReq);
|
|
@@ -9324,6 +9430,8 @@ function buildServer(opts) {
|
|
|
9324
9430
|
if (!engine.capabilities().supportsEdit)
|
|
9325
9431
|
return reply.status(400).send({ error: "engine does not support edits" });
|
|
9326
9432
|
const editRefs = mergedEdit ? mergedEdit.kept.map((a) => ({ path: core.images.pathFor(a.hash), role: a.role })) : (referenceImages ?? []).map((path, i) => ({ path, role: referenceRoles?.[i] })).slice(0, Math.max(0, engine.capabilities().maxReferenceImages - 1));
|
|
9433
|
+
const editEdge = engine.capabilities().maxReferenceEdge;
|
|
9434
|
+
if (editEdge) for (const r of editRefs) r.path = await capReferenceEdge(core, r.path, editEdge);
|
|
9327
9435
|
const srcBuf = core.images.read(String(srcHash));
|
|
9328
9436
|
const srcMeta = await sharp7(srcBuf).metadata();
|
|
9329
9437
|
if (srcMeta.width && srcMeta.height) expectShape = { width: srcMeta.width, height: srcMeta.height };
|
|
@@ -9496,7 +9604,8 @@ function buildServer(opts) {
|
|
|
9496
9604
|
}
|
|
9497
9605
|
return out;
|
|
9498
9606
|
} : void 0;
|
|
9499
|
-
|
|
9607
|
+
const nodeBudgetMs = kind === "generation" && runEngine.capabilities().id === "codex-cli" ? codexNodeBudgetMs(Math.min(Math.max(1, Number(count)), 8)) : void 0;
|
|
9608
|
+
void runNode(node.id, runEngine, estimate, work, expectShape, post, nodeBudgetMs).catch(
|
|
9500
9609
|
(err) => app.log.error({ err }, "node run failed")
|
|
9501
9610
|
);
|
|
9502
9611
|
const allWarnings = [...compiled2?.warnings ?? [], ...extraWarnings];
|