scenri 0.7.3 → 0.7.5
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 +15 -0
- package/dist/index.js +1 -1
- package/dist/{launcher-RETYUGSR.js → launcher-DIZ4MQRF.js} +2 -2
- package/dist/serve.js +430 -80
- package/package.json +1 -1
- package/studio-dist/assets/index-Bpw6p5Pj.js +103 -0
- package/studio-dist/assets/{index-B6onUYQF.css → index-C4vkwhbc.css} +1 -1
- package/studio-dist/index.html +2 -2
- package/studio-dist/assets/index-C-khfwCq.js +0 -103
package/dist/serve.js
CHANGED
|
@@ -212,11 +212,13 @@ function widenNodeStatusCheck(db) {
|
|
|
212
212
|
overlays TEXT NOT NULL DEFAULT '{}',
|
|
213
213
|
brief TEXT,
|
|
214
214
|
archived INTEGER NOT NULL DEFAULT 0,
|
|
215
|
-
duration_ms INTEGER
|
|
215
|
+
duration_ms INTEGER,
|
|
216
|
+
batch_id TEXT,
|
|
217
|
+
batch_index INTEGER NOT NULL DEFAULT 0
|
|
216
218
|
);
|
|
217
219
|
INSERT INTO nodes_new
|
|
218
220
|
SELECT id, project_id, parent_id, kind, prompt, engine_id, status, images, cost_usd, kept, error,
|
|
219
|
-
created_at, overlays, brief, archived, duration_ms
|
|
221
|
+
created_at, overlays, brief, archived, duration_ms, batch_id, batch_index
|
|
220
222
|
FROM nodes;
|
|
221
223
|
DROP TABLE nodes;
|
|
222
224
|
ALTER TABLE nodes_new RENAME TO nodes;
|
|
@@ -295,11 +297,104 @@ function collapseProjects(db) {
|
|
|
295
297
|
})();
|
|
296
298
|
}
|
|
297
299
|
}
|
|
298
|
-
|
|
300
|
+
function splitMultiImageNodes(db) {
|
|
301
|
+
const rows = db.prepare("SELECT * FROM nodes").all();
|
|
302
|
+
const multi = rows.filter((r) => {
|
|
303
|
+
try {
|
|
304
|
+
return JSON.parse(r.images).length > 1;
|
|
305
|
+
} catch {
|
|
306
|
+
return false;
|
|
307
|
+
}
|
|
308
|
+
});
|
|
309
|
+
if (!multi.length) return;
|
|
310
|
+
const parse2 = (s) => {
|
|
311
|
+
if (!s) return null;
|
|
312
|
+
try {
|
|
313
|
+
return JSON.parse(s);
|
|
314
|
+
} catch {
|
|
315
|
+
return null;
|
|
316
|
+
}
|
|
317
|
+
};
|
|
318
|
+
const stampOf = (iso, minusMs) => {
|
|
319
|
+
const t = (/* @__PURE__ */ new Date(`${iso.replace(" ", "T")}Z`)).getTime() - minusMs;
|
|
320
|
+
const d = new Date(t);
|
|
321
|
+
const p = (n, w = 2) => String(n).padStart(w, "0");
|
|
322
|
+
return `${d.getUTCFullYear()}-${p(d.getUTCMonth() + 1)}-${p(d.getUTCDate())} ${p(d.getUTCHours())}:${p(d.getUTCMinutes())}:${p(d.getUTCSeconds())}.${p(d.getUTCMilliseconds(), 3)}`;
|
|
323
|
+
};
|
|
324
|
+
const updateOriginal = db.prepare(
|
|
325
|
+
"UPDATE nodes SET images=?, overlays=?, brief=?, batch_id=?, batch_index=0 WHERE id=?"
|
|
326
|
+
);
|
|
327
|
+
const insertSibling = db.prepare(
|
|
328
|
+
`INSERT INTO nodes (id, project_id, parent_id, kind, prompt, engine_id, status, images, cost_usd, kept,
|
|
329
|
+
error, created_at, overlays, brief, archived, duration_ms, batch_id, batch_index)
|
|
330
|
+
VALUES (?,?,?,?,?,?,?,?,0,?,?,?,?,?,?,NULL,?,?)`
|
|
331
|
+
);
|
|
332
|
+
const childrenOf = db.prepare("SELECT id, brief FROM nodes WHERE parent_id=?");
|
|
333
|
+
const repoint = db.prepare("UPDATE nodes SET parent_id=? WHERE id=?");
|
|
334
|
+
const setsOf = db.prepare("SELECT set_id FROM set_nodes WHERE node_id=?");
|
|
335
|
+
const addMember = db.prepare("INSERT OR IGNORE INTO set_nodes (set_id, node_id) VALUES (?,?)");
|
|
336
|
+
db.transaction(() => {
|
|
337
|
+
for (const r of multi) {
|
|
338
|
+
const images = JSON.parse(r.images);
|
|
339
|
+
const overlays = parse2(r.overlays) ?? {};
|
|
340
|
+
const brief = parse2(r.brief);
|
|
341
|
+
const sizes = Array.isArray(brief?.rendered?.sizes) ? brief.rendered.sizes : null;
|
|
342
|
+
const briefFor = (i) => {
|
|
343
|
+
if (!brief) return null;
|
|
344
|
+
const b = { ...brief, variants: images.length };
|
|
345
|
+
if (brief.rendered) {
|
|
346
|
+
const { requested: _req, variantIndexes: _vi, ...rendered } = brief.rendered;
|
|
347
|
+
b.rendered = { ...rendered, ...sizes ? { sizes: sizes[i] !== void 0 ? [sizes[i]] : [] } : {} };
|
|
348
|
+
}
|
|
349
|
+
return JSON.stringify(b);
|
|
350
|
+
};
|
|
351
|
+
const siblingIds = [r.id];
|
|
352
|
+
updateOriginal.run(
|
|
353
|
+
JSON.stringify([images[0]]),
|
|
354
|
+
JSON.stringify(overlays["0"] !== void 0 ? { "0": overlays["0"] } : {}),
|
|
355
|
+
briefFor(0),
|
|
356
|
+
r.id,
|
|
357
|
+
r.id
|
|
358
|
+
);
|
|
359
|
+
for (let i = 1; i < images.length; i++) {
|
|
360
|
+
const id = randomUUID();
|
|
361
|
+
siblingIds.push(id);
|
|
362
|
+
insertSibling.run(
|
|
363
|
+
id,
|
|
364
|
+
r.project_id,
|
|
365
|
+
r.parent_id,
|
|
366
|
+
r.kind,
|
|
367
|
+
r.prompt,
|
|
368
|
+
r.engine_id,
|
|
369
|
+
r.status,
|
|
370
|
+
JSON.stringify([images[i]]),
|
|
371
|
+
r.kept,
|
|
372
|
+
r.error,
|
|
373
|
+
stampOf(r.created_at, i),
|
|
374
|
+
JSON.stringify(overlays[String(i)] !== void 0 ? { "0": overlays[String(i)] } : {}),
|
|
375
|
+
briefFor(i),
|
|
376
|
+
r.archived,
|
|
377
|
+
r.id,
|
|
378
|
+
i
|
|
379
|
+
);
|
|
380
|
+
}
|
|
381
|
+
for (const child of childrenOf.all(r.id)) {
|
|
382
|
+
const src = parse2(child.brief)?.sourceImage;
|
|
383
|
+
if (typeof src !== "string") continue;
|
|
384
|
+
const at = images.indexOf(src);
|
|
385
|
+
if (at > 0) repoint.run(siblingIds[at], child.id);
|
|
386
|
+
}
|
|
387
|
+
for (const s of setsOf.all(r.id)) {
|
|
388
|
+
for (let i = 1; i < siblingIds.length; i++) addMember.run(s.set_id, siblingIds[i]);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
})();
|
|
392
|
+
}
|
|
393
|
+
var SCHEMA_VERSION = 2;
|
|
299
394
|
var SchemaTooNewError = class extends Error {
|
|
300
|
-
constructor(found, supported) {
|
|
395
|
+
constructor(found, supported, backupsDir) {
|
|
301
396
|
super(
|
|
302
|
-
`This library was written by a newer Scenri (schema ${found}; this build understands ${supported}). Update and retry: npx scenri@latest`
|
|
397
|
+
`This library was written by a newer Scenri (schema ${found}; this build understands ${supported}). Update and retry: npx scenri@latest` + (backupsDir ? ` (a pre-migration snapshot of the library is kept in ${backupsDir})` : "")
|
|
303
398
|
);
|
|
304
399
|
this.name = "SchemaTooNewError";
|
|
305
400
|
}
|
|
@@ -329,7 +424,7 @@ function openDb(homeDir) {
|
|
|
329
424
|
const found = db.pragma("user_version", { simple: true });
|
|
330
425
|
if (found > SCHEMA_VERSION) {
|
|
331
426
|
db.close();
|
|
332
|
-
throw new SchemaTooNewError(found, SCHEMA_VERSION);
|
|
427
|
+
throw new SchemaTooNewError(found, SCHEMA_VERSION, join(homeDir, "backups"));
|
|
333
428
|
}
|
|
334
429
|
if (preExisting && found < SCHEMA_VERSION) backupBeforeMigration(db, homeDir, found);
|
|
335
430
|
db.exec(MIGRATIONS);
|
|
@@ -346,6 +441,12 @@ function openDb(homeDir) {
|
|
|
346
441
|
if (!nodeCols.includes("duration_ms")) {
|
|
347
442
|
db.exec("ALTER TABLE nodes ADD COLUMN duration_ms INTEGER");
|
|
348
443
|
}
|
|
444
|
+
if (!nodeCols.includes("batch_id")) {
|
|
445
|
+
db.exec("ALTER TABLE nodes ADD COLUMN batch_id TEXT");
|
|
446
|
+
}
|
|
447
|
+
if (!nodeCols.includes("batch_index")) {
|
|
448
|
+
db.exec("ALTER TABLE nodes ADD COLUMN batch_index INTEGER NOT NULL DEFAULT 0");
|
|
449
|
+
}
|
|
349
450
|
const projectCols = db.pragma("table_info(projects)").map((c) => c.name);
|
|
350
451
|
if (!projectCols.includes("slug")) {
|
|
351
452
|
db.exec("ALTER TABLE projects ADD COLUMN slug TEXT");
|
|
@@ -362,6 +463,7 @@ function openDb(homeDir) {
|
|
|
362
463
|
widenNodeStatusCheck(db);
|
|
363
464
|
backfillSlugs(db);
|
|
364
465
|
collapseProjects(db);
|
|
466
|
+
splitMultiImageNodes(db);
|
|
365
467
|
db.prepare(
|
|
366
468
|
"UPDATE nodes SET status='error', error='interrupted: server restarted mid-generation' WHERE status='running'"
|
|
367
469
|
).run();
|
|
@@ -482,9 +584,21 @@ function rowToNode(r) {
|
|
|
482
584
|
createdAt: r.created_at,
|
|
483
585
|
overlays: JSON.parse(r.overlays ?? "{}"),
|
|
484
586
|
brief: r.brief ? JSON.parse(r.brief) : null,
|
|
485
|
-
archived: !!r.archived
|
|
587
|
+
archived: !!r.archived,
|
|
588
|
+
batchId: r.batch_id ?? null,
|
|
589
|
+
batchIndex: r.batch_index ?? 0
|
|
486
590
|
};
|
|
487
591
|
}
|
|
592
|
+
var lastBatchStamp = 0;
|
|
593
|
+
function batchStamps(count) {
|
|
594
|
+
const base = Math.max(Date.now(), lastBatchStamp + count);
|
|
595
|
+
lastBatchStamp = base;
|
|
596
|
+
const p = (n, w = 2) => String(n).padStart(w, "0");
|
|
597
|
+
return Array.from({ length: count }, (_, i) => {
|
|
598
|
+
const d = new Date(base - i);
|
|
599
|
+
return `${d.getUTCFullYear()}-${p(d.getUTCMonth() + 1)}-${p(d.getUTCDate())} ${p(d.getUTCHours())}:${p(d.getUTCMinutes())}:${p(d.getUTCSeconds())}.${p(d.getUTCMilliseconds(), 3)}`;
|
|
600
|
+
});
|
|
601
|
+
}
|
|
488
602
|
function createStore(db) {
|
|
489
603
|
return {
|
|
490
604
|
// brands
|
|
@@ -648,6 +762,42 @@ function createStore(db) {
|
|
|
648
762
|
).run(id, input.projectId, input.parentId, input.kind, input.prompt, input.engineId);
|
|
649
763
|
return this.getNode(id);
|
|
650
764
|
},
|
|
765
|
+
/**
|
|
766
|
+
* One multi-shot request, N first-class sibling nodes, one transaction.
|
|
767
|
+
* Slot 0 gets the newest stamp (see batchStamps) so the newest-first feed
|
|
768
|
+
* reads the batch in request order; batch_id is the first node's id, held
|
|
769
|
+
* by every sibling including the first, and stays null for a single send
|
|
770
|
+
* — one shot is not a batch.
|
|
771
|
+
*/
|
|
772
|
+
addNodes(input) {
|
|
773
|
+
if (input.parentId) {
|
|
774
|
+
const parent = this.getNode(input.parentId);
|
|
775
|
+
if (!parent || parent.projectId !== input.projectId) throw new Error("parent node not found in project");
|
|
776
|
+
}
|
|
777
|
+
const count = Math.max(1, Math.floor(input.count));
|
|
778
|
+
const ids = Array.from({ length: count }, () => randomUUID());
|
|
779
|
+
const stamps = batchStamps(count);
|
|
780
|
+
const batchId = count > 1 ? ids[0] : null;
|
|
781
|
+
const insert = db.prepare(
|
|
782
|
+
"INSERT INTO nodes (id, project_id, parent_id, kind, prompt, engine_id, created_at, batch_id, batch_index) VALUES (?,?,?,?,?,?,?,?,?)"
|
|
783
|
+
);
|
|
784
|
+
db.transaction(() => {
|
|
785
|
+
for (let i = 0; i < count; i++) {
|
|
786
|
+
insert.run(
|
|
787
|
+
ids[i],
|
|
788
|
+
input.projectId,
|
|
789
|
+
input.parentId,
|
|
790
|
+
input.kind,
|
|
791
|
+
input.prompt,
|
|
792
|
+
input.engineId,
|
|
793
|
+
stamps[i],
|
|
794
|
+
batchId,
|
|
795
|
+
i
|
|
796
|
+
);
|
|
797
|
+
}
|
|
798
|
+
})();
|
|
799
|
+
return ids.map((id) => this.getNode(id));
|
|
800
|
+
},
|
|
651
801
|
completeNode(id, result) {
|
|
652
802
|
db.prepare("UPDATE nodes SET status='done', images=?, cost_usd=?, duration_ms=? WHERE id=?").run(
|
|
653
803
|
JSON.stringify(result.images),
|
|
@@ -1454,7 +1604,17 @@ function createOpenRouterEngine(opts) {
|
|
|
1454
1604
|
localOnly: false,
|
|
1455
1605
|
supportsEdit: true,
|
|
1456
1606
|
supportsMask: false,
|
|
1607
|
+
// Four is OUR conservative constant, not a provider fact: OpenRouter
|
|
1608
|
+
// multiplexes many image models and their input limits differ, so
|
|
1609
|
+
// there is no single upstream number to cite the way codex's five is
|
|
1610
|
+
// cited. Four keeps a full identity payload (product essential +
|
|
1611
|
+
// angle, presenter, mark) inside every model we have routed to.
|
|
1457
1612
|
maxReferenceImages: 4,
|
|
1613
|
+
// Same uplink argument as codex: the provider reads references at
|
|
1614
|
+
// reduced resolution anyway, and these ride base64-inlined inside a
|
|
1615
|
+
// JSON body — a full-resolution phone photo is tens of megabytes of
|
|
1616
|
+
// request for nothing.
|
|
1617
|
+
maxReferenceEdge: 2048,
|
|
1458
1618
|
// N sequential calls, one image each: the server budgets the node by
|
|
1459
1619
|
// that shape instead of handing the whole run one flat ten minutes.
|
|
1460
1620
|
perImageTimeoutMs: PER_IMAGE_TIMEOUT_MS,
|
|
@@ -2037,6 +2197,36 @@ function execArgs(dir, effort = "low") {
|
|
|
2037
2197
|
"-"
|
|
2038
2198
|
];
|
|
2039
2199
|
}
|
|
2200
|
+
var TAIL_BYTES = 4096;
|
|
2201
|
+
var DETAIL_CHARS = 800;
|
|
2202
|
+
function keepTail(buf, chunk) {
|
|
2203
|
+
const next = buf + chunk;
|
|
2204
|
+
return next.length > TAIL_BYTES ? next.slice(next.length - TAIL_BYTES) : next;
|
|
2205
|
+
}
|
|
2206
|
+
var BANNER_RULE = /^-{3,}\s*$/;
|
|
2207
|
+
var BANNER_LINE = /^(OpenAI Codex v|workdir:|model:|provider:|approval:|sandbox:|reasoning |-{3,}\s*$|\s*$)/;
|
|
2208
|
+
function afterBanner(stderr) {
|
|
2209
|
+
const lines = stderr.split(/\r?\n/);
|
|
2210
|
+
const rules = [];
|
|
2211
|
+
for (const [i2, line] of lines.entries()) if (BANNER_RULE.test(line)) rules.push(i2);
|
|
2212
|
+
if (rules.length >= 2)
|
|
2213
|
+
return lines.slice(rules[1] + 1).join("\n").trim();
|
|
2214
|
+
let i = 0;
|
|
2215
|
+
while (i < lines.length && BANNER_LINE.test(lines[i])) i++;
|
|
2216
|
+
return lines.slice(i).join("\n").trim();
|
|
2217
|
+
}
|
|
2218
|
+
function tailOf(text) {
|
|
2219
|
+
if (text.length <= DETAIL_CHARS) return text;
|
|
2220
|
+
const cut = text.slice(text.length - DETAIL_CHARS);
|
|
2221
|
+
const nl = cut.indexOf("\n");
|
|
2222
|
+
return (nl >= 0 ? cut.slice(nl + 1) : cut).trim();
|
|
2223
|
+
}
|
|
2224
|
+
function codexFailureDetail(stderr, stdout) {
|
|
2225
|
+
const body = afterBanner(stderr);
|
|
2226
|
+
const errorAt = body.lastIndexOf("\nERROR:");
|
|
2227
|
+
const marked = body.startsWith("ERROR:") ? body : errorAt >= 0 ? body.slice(errorAt + 1) : "";
|
|
2228
|
+
return tailOf(marked) || tailOf(body) || tailOf(stdout.trim()) || tailOf(stderr.trim());
|
|
2229
|
+
}
|
|
2040
2230
|
function killTree(child, platform, spawnImpl) {
|
|
2041
2231
|
if (platform === "win32" && child.pid) {
|
|
2042
2232
|
try {
|
|
@@ -2098,6 +2288,7 @@ function createRunner(opts = {}) {
|
|
|
2098
2288
|
}
|
|
2099
2289
|
let settled = false;
|
|
2100
2290
|
let stderr = "";
|
|
2291
|
+
let stdout = "";
|
|
2101
2292
|
const spawnedAt = Date.now();
|
|
2102
2293
|
let firstByteAt = 0;
|
|
2103
2294
|
let lastByteAt = 0;
|
|
@@ -2124,9 +2315,12 @@ function createRunner(opts = {}) {
|
|
|
2124
2315
|
}
|
|
2125
2316
|
lastByteAt = now;
|
|
2126
2317
|
}
|
|
2127
|
-
child.stdout?.on("data",
|
|
2318
|
+
child.stdout?.on("data", (d) => {
|
|
2319
|
+
stdout = keepTail(stdout, String(d));
|
|
2320
|
+
sawActivity();
|
|
2321
|
+
});
|
|
2128
2322
|
child.stderr?.on("data", (d) => {
|
|
2129
|
-
stderr
|
|
2323
|
+
stderr = keepTail(stderr, String(d));
|
|
2130
2324
|
sawActivity();
|
|
2131
2325
|
});
|
|
2132
2326
|
if (io?.stdin != null) {
|
|
@@ -2183,7 +2377,7 @@ function createRunner(opts = {}) {
|
|
|
2183
2377
|
);
|
|
2184
2378
|
return;
|
|
2185
2379
|
}
|
|
2186
|
-
const snippet2 = stderr
|
|
2380
|
+
const snippet2 = codexFailureDetail(stderr, stdout);
|
|
2187
2381
|
finish(
|
|
2188
2382
|
`exit-${code ?? "unknown"}`,
|
|
2189
2383
|
() => reject(new Error(`codex exited with code ${code ?? "unknown"}${snippet2 ? `: ${snippet2}` : ""}`))
|
|
@@ -3208,9 +3402,9 @@ var ROLE_PRIORITY = {
|
|
|
3208
3402
|
product: 0,
|
|
3209
3403
|
character: 1,
|
|
3210
3404
|
brand: 2,
|
|
3211
|
-
|
|
3212
|
-
|
|
3213
|
-
|
|
3405
|
+
reference: 3,
|
|
3406
|
+
scene: 4,
|
|
3407
|
+
composition: 5,
|
|
3214
3408
|
style: 6
|
|
3215
3409
|
};
|
|
3216
3410
|
function allocateAttachments(attachments, cap2) {
|
|
@@ -3560,6 +3754,7 @@ function validateBrief(brief) {
|
|
|
3560
3754
|
function compileBrief(brief, ctx) {
|
|
3561
3755
|
const warnings = [];
|
|
3562
3756
|
const attachments = [];
|
|
3757
|
+
const unattachable = [];
|
|
3563
3758
|
const rawSceneFallback = [];
|
|
3564
3759
|
const productDirectives = [];
|
|
3565
3760
|
const personDirectives = [];
|
|
@@ -3611,7 +3806,7 @@ function compileBrief(brief, ctx) {
|
|
|
3611
3806
|
...angle ? { angle } : {}
|
|
3612
3807
|
});
|
|
3613
3808
|
});
|
|
3614
|
-
productDirectives.push(
|
|
3809
|
+
productDirectives.push({ need: "fidelity", id: p.id });
|
|
3615
3810
|
productDirectives.push(...productFactDirectives(p));
|
|
3616
3811
|
if (p.description && !p.dimensions)
|
|
3617
3812
|
productDirectives.push(
|
|
@@ -3619,6 +3814,7 @@ function compileBrief(brief, ctx) {
|
|
|
3619
3814
|
);
|
|
3620
3815
|
} else {
|
|
3621
3816
|
warnings.push(`${p.name} has no usable photo, so it is named but not attached.`);
|
|
3817
|
+
unattachable.push({ role: "product", id: p.id, label: p.name, hash: "", essential: true, reason: "missing" });
|
|
3622
3818
|
}
|
|
3623
3819
|
break;
|
|
3624
3820
|
}
|
|
@@ -3660,6 +3856,14 @@ function compileBrief(brief, ctx) {
|
|
|
3660
3856
|
if (c.build) personDirectives.push(`${c.promptName ?? c.name}'s build: ${c.build}.`);
|
|
3661
3857
|
} else {
|
|
3662
3858
|
warnings.push(`${c.name} has no usable photo, so they are named but not attached.`);
|
|
3859
|
+
unattachable.push({
|
|
3860
|
+
role: "character",
|
|
3861
|
+
id: c.id,
|
|
3862
|
+
label: c.name,
|
|
3863
|
+
hash: "",
|
|
3864
|
+
essential: true,
|
|
3865
|
+
reason: "missing"
|
|
3866
|
+
});
|
|
3663
3867
|
}
|
|
3664
3868
|
break;
|
|
3665
3869
|
}
|
|
@@ -3679,7 +3883,12 @@ function compileBrief(brief, ctx) {
|
|
|
3679
3883
|
break;
|
|
3680
3884
|
}
|
|
3681
3885
|
attachments.push({ role: "reference", label: "Reference shot", hash: tok.imageHash });
|
|
3682
|
-
otherDirectives.push(
|
|
3886
|
+
otherDirectives.push({
|
|
3887
|
+
need: "attachment",
|
|
3888
|
+
role: "reference",
|
|
3889
|
+
hash: tok.imageHash,
|
|
3890
|
+
text: "Match the composition, lighting and treatment of the attached reference."
|
|
3891
|
+
});
|
|
3683
3892
|
break;
|
|
3684
3893
|
}
|
|
3685
3894
|
case "mark": {
|
|
@@ -3699,9 +3908,12 @@ function compileBrief(brief, ctx) {
|
|
|
3699
3908
|
);
|
|
3700
3909
|
} catch {
|
|
3701
3910
|
}
|
|
3702
|
-
otherDirectives.push(
|
|
3703
|
-
"
|
|
3704
|
-
|
|
3911
|
+
otherDirectives.push({
|
|
3912
|
+
need: "attachment",
|
|
3913
|
+
role: "brand",
|
|
3914
|
+
hash: tok.imageHash,
|
|
3915
|
+
text: "The attached brand mark is this brand's own mark. If the direction asks for the logo to appear, reproduce it exactly as drawn \u2014 same colours, letterforms and proportions, never redrawn or re-lettered. Every character it carries appears intact, including the smallest secondary lettering, in its original script and reading direction \u2014 never translated, transliterated or re-spelled. Otherwise take only its colour and treatment from it."
|
|
3916
|
+
});
|
|
3705
3917
|
break;
|
|
3706
3918
|
}
|
|
3707
3919
|
case "template": {
|
|
@@ -3781,8 +3993,32 @@ function compileBrief(brief, ctx) {
|
|
|
3781
3993
|
if (i !== -1) attachments.splice(i, 1);
|
|
3782
3994
|
}
|
|
3783
3995
|
}
|
|
3996
|
+
const identityHashes = /* @__PURE__ */ new Map();
|
|
3997
|
+
for (const a of attachments)
|
|
3998
|
+
if ((a.role === "product" || a.role === "character") && !identityHashes.has(a.hash))
|
|
3999
|
+
identityHashes.set(a.hash, a.label);
|
|
4000
|
+
for (let i = attachments.length - 1; i >= 0; i--) {
|
|
4001
|
+
const a = attachments[i];
|
|
4002
|
+
if (a.role === "reference" && identityHashes.has(a.hash)) {
|
|
4003
|
+
attachments.splice(i, 1);
|
|
4004
|
+
warnings.push(
|
|
4005
|
+
`That reference is the same image as ${identityHashes.get(a.hash)}'s own photo, so it rides once, as the identity.`
|
|
4006
|
+
);
|
|
4007
|
+
}
|
|
4008
|
+
}
|
|
3784
4009
|
const max = ctx.engineCaps.maxReferenceImages;
|
|
3785
|
-
const { kept, dropped } = allocateAttachments(attachments, max);
|
|
4010
|
+
const { kept, dropped: budgetDropped } = allocateAttachments(attachments, max);
|
|
4011
|
+
const presentKeys = new Set((ctx.presentAttachments ?? kept).map((a) => `${a.role}:${a.hash}`));
|
|
4012
|
+
const resolveDirective = (d) => {
|
|
4013
|
+
if (typeof d === "string") return d;
|
|
4014
|
+
if (d.need === "fidelity") {
|
|
4015
|
+
const n = attachments.filter(
|
|
4016
|
+
(a) => a.role === "product" && a.id === d.id && presentKeys.has(`product:${a.hash}`)
|
|
4017
|
+
).length;
|
|
4018
|
+
return n > 0 ? productFidelityDirective(n) : null;
|
|
4019
|
+
}
|
|
4020
|
+
return presentKeys.has(`${d.role}:${d.hash}`) ? d.text : null;
|
|
4021
|
+
};
|
|
3786
4022
|
const guard = scene ? sceneGuardDirectives({
|
|
3787
4023
|
hasProduct: !!productId,
|
|
3788
4024
|
hasPerson,
|
|
@@ -3804,8 +4040,9 @@ function compileBrief(brief, ctx) {
|
|
|
3804
4040
|
treatment: scene.figureTreatment,
|
|
3805
4041
|
hasPerson,
|
|
3806
4042
|
// The treatment's fictional-brands rule needs to know a real mark is
|
|
3807
|
-
// deliberately in play
|
|
3808
|
-
|
|
4043
|
+
// deliberately in play - and only one that actually rides counts,
|
|
4044
|
+
// same honesty rule as the photo guard above.
|
|
4045
|
+
hasMark: [...presentKeys].some((k) => k.startsWith("brand:"))
|
|
3809
4046
|
}) : [];
|
|
3810
4047
|
if (hasPerson) personDirectives.push(personSkinDirective());
|
|
3811
4048
|
const closeUpDirectives = hasPerson && /\bclose[- ]?up\b|\bmacro\b|\bzoom(?:ed)?\b|\bDOF\b|\bdepth of field\b/i.test(sentence) ? [
|
|
@@ -3843,23 +4080,26 @@ function compileBrief(brief, ctx) {
|
|
|
3843
4080
|
...refGuard,
|
|
3844
4081
|
...preservation
|
|
3845
4082
|
];
|
|
3846
|
-
|
|
3847
|
-
if (
|
|
4083
|
+
const spoken = dedupe(allDirectives.map(resolveDirective).filter((s) => s !== null));
|
|
4084
|
+
if (spoken.length) prompt = `${prompt}${prompt.endsWith(".") ? "" : "."} ${spoken.join(" ")}`;
|
|
4085
|
+
if (budgetDropped.some((d) => d.role !== "scene")) {
|
|
3848
4086
|
const keptLabels = new Set(kept.map((a) => a.label));
|
|
3849
|
-
const names = [...new Set(
|
|
4087
|
+
const names = [...new Set(budgetDropped.filter((d) => d.role !== "scene").map((d) => d.label))].filter(
|
|
3850
4088
|
(l) => !keptLabels.has(l)
|
|
3851
4089
|
);
|
|
3852
4090
|
if (names.length) {
|
|
3853
4091
|
const reads = max === 0 ? "reads no reference images" : `reads ${max} reference image${max === 1 ? "" : "s"}`;
|
|
3854
4092
|
warnings.push(
|
|
3855
|
-
`${
|
|
4093
|
+
`${names.join(" and ")} ${names.length === 1 ? "was" : "were"} left out \u2014 ${ctx.engineCaps.displayName} ${reads}.`
|
|
3856
4094
|
);
|
|
3857
4095
|
}
|
|
3858
4096
|
}
|
|
3859
4097
|
return {
|
|
3860
4098
|
prompt: prompt.trim(),
|
|
3861
4099
|
referenceImages: kept.map((a) => ctx.images.pathFor(a.hash)),
|
|
3862
|
-
|
|
4100
|
+
// The missing-photo identities lead: they are essential, and the refusal
|
|
4101
|
+
// path reads this list. Budget losses carry their reason for the chips.
|
|
4102
|
+
dropped: [...unattachable, ...budgetDropped.map((d) => ({ ...d, reason: "budget" }))],
|
|
3863
4103
|
width,
|
|
3864
4104
|
height,
|
|
3865
4105
|
attachments: kept,
|
|
@@ -8726,6 +8966,35 @@ function registerImageRoutes(app, deps) {
|
|
|
8726
8966
|
|
|
8727
8967
|
// src/release/notes.data.ts
|
|
8728
8968
|
var RELEASES = [
|
|
8969
|
+
{
|
|
8970
|
+
version: "0.7.5",
|
|
8971
|
+
date: "2026-09-01",
|
|
8972
|
+
title: "Every shot is one card.",
|
|
8973
|
+
sections: [
|
|
8974
|
+
{
|
|
8975
|
+
heading: "Create",
|
|
8976
|
+
body: "Every shot is now its own card with its own image. Asking for several shots gives you that many cards, made together and standing on their own, and older multi-image shots split into separate cards the first time this version opens. The shot panel is rebuilt around the brief itself, with inline ingredient chips and versions in a single strip under the picture."
|
|
8977
|
+
},
|
|
8978
|
+
{
|
|
8979
|
+
heading: "Refining",
|
|
8980
|
+
body: "The shot being refined appears as a regular chip in the composer, and the card it points at is marked in the feed. Scenes sit out of the attach panel while a refine is armed, with a note saying why, instead of quietly trading the refine for a new shot."
|
|
8981
|
+
},
|
|
8982
|
+
{
|
|
8983
|
+
heading: "Fixes",
|
|
8984
|
+
body: "Generation requests carry exactly the reference images they claim to carry, and a product or presenter whose photo is missing stops the shot with a clear message instead of running without it. Cards that are still rendering show a simple counter and a cancel button."
|
|
8985
|
+
}
|
|
8986
|
+
]
|
|
8987
|
+
},
|
|
8988
|
+
{
|
|
8989
|
+
version: "0.7.4",
|
|
8990
|
+
date: "2026-08-31",
|
|
8991
|
+
sections: [
|
|
8992
|
+
{
|
|
8993
|
+
heading: "Fixes",
|
|
8994
|
+
body: "A shot that Codex could not finish now says why it failed. Failures used to quote the banner Codex prints as it starts, which names the working folder and the model but never the reason. A Windows machine that cannot start the Codex tool host is now told which setting to change."
|
|
8995
|
+
}
|
|
8996
|
+
]
|
|
8997
|
+
},
|
|
8729
8998
|
{
|
|
8730
8999
|
version: "0.7.3",
|
|
8731
9000
|
date: "2026-08-31",
|
|
@@ -9856,7 +10125,7 @@ function buildServer(opts) {
|
|
|
9856
10125
|
}
|
|
9857
10126
|
}
|
|
9858
10127
|
if (inheritedPerson) inheritedDirectives.push(personSkinDirective());
|
|
9859
|
-
const
|
|
10128
|
+
const compileCtx = {
|
|
9860
10129
|
brand: brandJson,
|
|
9861
10130
|
images: core.images,
|
|
9862
10131
|
engineCaps: uncapped,
|
|
@@ -9874,7 +10143,8 @@ function buildServer(opts) {
|
|
|
9874
10143
|
// Only the explicit op drops the dimension promise: an implicit legacy
|
|
9875
10144
|
// expansion keeps its historical prompt byte for byte.
|
|
9876
10145
|
...opts2?.reshape === "extend" ? { editReshape: "extend" } : {}
|
|
9877
|
-
}
|
|
10146
|
+
};
|
|
10147
|
+
const compiled2 = compileBrief(brief, compileCtx);
|
|
9878
10148
|
let inheritedAttachments = [];
|
|
9879
10149
|
let identityWarnings = [];
|
|
9880
10150
|
if (inheritedTokens.length) {
|
|
@@ -9895,27 +10165,22 @@ function buildServer(opts) {
|
|
|
9895
10165
|
}
|
|
9896
10166
|
const cap2 = Math.max(0, engineCaps.maxReferenceImages - 1);
|
|
9897
10167
|
const merged = mergeEditAttachments(compiled2.attachments, inheritedAttachments, cap2);
|
|
10168
|
+
const prompt = compileBrief(brief, { ...compileCtx, presentAttachments: merged.kept }).prompt;
|
|
9898
10169
|
const warnings = [...compiled2.warnings, ...identityWarnings.filter((w) => !compiled2.warnings.includes(w))];
|
|
9899
10170
|
if (inherited.truncated)
|
|
9900
10171
|
warnings.push("This thread is deeper than 64 steps, so identity attached before that could not be carried.");
|
|
9901
10172
|
if (merged.dropped.length) {
|
|
9902
10173
|
if (engineCaps.maxReferenceImages <= 1) {
|
|
9903
|
-
warnings.push(
|
|
9904
|
-
`${engineCaps.displayName} cannot carry reference images, so the identity rides on the source frame alone.`
|
|
9905
|
-
);
|
|
10174
|
+
warnings.push(`The identity rides on the shot itself \u2014 ${engineCaps.displayName} reads no other images.`);
|
|
9906
10175
|
} else {
|
|
9907
10176
|
const keptLabels = new Set(merged.kept.map((a) => a.label));
|
|
9908
10177
|
const names = [...new Set(merged.dropped.map((d) => d.label))].filter((l) => !keptLabels.has(l));
|
|
9909
10178
|
if (names.length)
|
|
9910
|
-
warnings.push(
|
|
9911
|
-
`${engineCaps.displayName} reads ${engineCaps.maxReferenceImages} reference images and the frame being refined keeps one, so ${names.join(
|
|
9912
|
-
" and "
|
|
9913
|
-
)} ${names.length === 1 ? "was" : "were"} left out.`
|
|
9914
|
-
);
|
|
10179
|
+
warnings.push(`${names.join(" and ")} ${names.length === 1 ? "was" : "were"} left out of this refinement.`);
|
|
9915
10180
|
}
|
|
9916
10181
|
}
|
|
9917
10182
|
return {
|
|
9918
|
-
compiled: compiled2,
|
|
10183
|
+
compiled: { ...compiled2, prompt },
|
|
9919
10184
|
inheritedTokens,
|
|
9920
10185
|
merged,
|
|
9921
10186
|
warnings,
|
|
@@ -9942,7 +10207,9 @@ function buildServer(opts) {
|
|
|
9942
10207
|
return {
|
|
9943
10208
|
...rest2,
|
|
9944
10209
|
attachments: edit.merged.kept,
|
|
9945
|
-
dropped
|
|
10210
|
+
// The own compile runs uncapped, so its dropped list holds exactly the
|
|
10211
|
+
// missing-photo identities; the budget losses live on the merge.
|
|
10212
|
+
dropped: [...edit.compiled.dropped, ...edit.merged.dropped],
|
|
9946
10213
|
warnings: edit.warnings,
|
|
9947
10214
|
referenceCount: edit.merged.kept.length
|
|
9948
10215
|
};
|
|
@@ -10097,11 +10364,11 @@ function buildServer(opts) {
|
|
|
10097
10364
|
}
|
|
10098
10365
|
}
|
|
10099
10366
|
const NODE_TIMEOUT_MS = 6e5;
|
|
10100
|
-
async function runNode(
|
|
10367
|
+
async function runNode(nodeIds, engine, estimate, work, expect, postFor, timeoutMs) {
|
|
10101
10368
|
const engineId = engine?.capabilities().id ?? "local";
|
|
10102
10369
|
reserved.set(engineId, (reserved.get(engineId) ?? 0) + estimate);
|
|
10103
10370
|
const ctrl = new AbortController();
|
|
10104
|
-
runningGenerations.set(
|
|
10371
|
+
for (const id of nodeIds) runningGenerations.set(id, ctrl);
|
|
10105
10372
|
const bound = opts.nodeTimeoutMs ?? timeoutMs ?? NODE_TIMEOUT_MS;
|
|
10106
10373
|
let watchdogFired = false;
|
|
10107
10374
|
const watchdog = setTimeout(() => {
|
|
@@ -10109,43 +10376,68 @@ function buildServer(opts) {
|
|
|
10109
10376
|
ctrl.abort(BUDGET_EXHAUSTED);
|
|
10110
10377
|
}, bound);
|
|
10111
10378
|
const startedAt = Date.now();
|
|
10379
|
+
const settled = /* @__PURE__ */ new Set();
|
|
10112
10380
|
try {
|
|
10113
10381
|
const result = await work(ctrl.signal);
|
|
10114
10382
|
clearTimeout(watchdog);
|
|
10115
|
-
|
|
10116
|
-
|
|
10117
|
-
|
|
10118
|
-
|
|
10119
|
-
const
|
|
10120
|
-
|
|
10121
|
-
|
|
10122
|
-
|
|
10383
|
+
const images = await normalizePngs(result.images);
|
|
10384
|
+
const raw = result.raw;
|
|
10385
|
+
const bySlot = new Array(nodeIds.length);
|
|
10386
|
+
images.forEach((h, k) => {
|
|
10387
|
+
const slot = raw?.variantIndexes?.[k] ?? k;
|
|
10388
|
+
if (slot < nodeIds.length && bySlot[slot] === void 0) bySlot[slot] = h;
|
|
10389
|
+
});
|
|
10390
|
+
const wall = Date.now() - startedAt;
|
|
10391
|
+
const failures = [...raw?.partialFailures ?? []];
|
|
10392
|
+
for (let slot = 0; slot < nodeIds.length; slot++) {
|
|
10393
|
+
const id = nodeIds[slot];
|
|
10394
|
+
const hash = bySlot[slot];
|
|
10395
|
+
if (hash === void 0) {
|
|
10396
|
+
core.store.failNode(id, failures.shift() ?? "the engine returned no image for this shot");
|
|
10397
|
+
settled.add(id);
|
|
10398
|
+
continue;
|
|
10123
10399
|
}
|
|
10124
|
-
|
|
10125
|
-
|
|
10126
|
-
const
|
|
10127
|
-
|
|
10128
|
-
|
|
10129
|
-
|
|
10130
|
-
|
|
10131
|
-
|
|
10132
|
-
|
|
10133
|
-
|
|
10134
|
-
|
|
10135
|
-
|
|
10400
|
+
try {
|
|
10401
|
+
let own = [hash];
|
|
10402
|
+
const post = postFor?.(id);
|
|
10403
|
+
if (post) own = await post(own);
|
|
10404
|
+
if (expect) await assertAspect(own, expect);
|
|
10405
|
+
try {
|
|
10406
|
+
const meta2 = await sharp20(core.images.read(own[0])).metadata();
|
|
10407
|
+
const node = core.store.getNode(id);
|
|
10408
|
+
if (node && meta2.width && meta2.height) {
|
|
10409
|
+
const brief = node.brief ?? {};
|
|
10410
|
+
const asked = expect ? { requestedSize: [expect.width, expect.height] } : {};
|
|
10411
|
+
core.store.setBrief(id, { ...brief, rendered: { sizes: [[meta2.width, meta2.height]], ...asked } });
|
|
10412
|
+
if (node.kind === "generation" && engineId === "codex-cli" && expect && expect.width !== expect.height && meta2.width === expect.width && meta2.height === expect.height)
|
|
10413
|
+
app.log.warn(
|
|
10414
|
+
{ nodeId: id },
|
|
10415
|
+
"codex delivered exactly the requested pixels; its image tool cannot pin size - suggests a forbidden shell resize"
|
|
10416
|
+
);
|
|
10417
|
+
}
|
|
10418
|
+
} catch {
|
|
10419
|
+
}
|
|
10420
|
+
core.store.completeNode(id, {
|
|
10421
|
+
images: own,
|
|
10422
|
+
costUsd: slot === 0 ? result.costUsd : 0,
|
|
10423
|
+
durationMs: wall
|
|
10424
|
+
});
|
|
10425
|
+
} catch (err) {
|
|
10426
|
+
core.store.failNode(id, String(err?.message ?? err));
|
|
10136
10427
|
}
|
|
10137
|
-
|
|
10428
|
+
settled.add(id);
|
|
10138
10429
|
}
|
|
10139
|
-
core.
|
|
10140
|
-
core.ledger.recordCost(engineId, nodeId, result.costUsd);
|
|
10430
|
+
core.ledger.recordCost(engineId, nodeIds[0], result.costUsd);
|
|
10141
10431
|
} catch (err) {
|
|
10142
|
-
|
|
10143
|
-
|
|
10144
|
-
|
|
10145
|
-
|
|
10432
|
+
for (const id of nodeIds) {
|
|
10433
|
+
if (settled.has(id)) continue;
|
|
10434
|
+
if (watchdogFired) core.store.failNode(id, `generation timed out after ${Math.round(bound / 6e4)} minutes`);
|
|
10435
|
+
else if (ctrl.signal.aborted) core.store.cancelNode(id);
|
|
10436
|
+
else core.store.failNode(id, String(err?.message ?? err));
|
|
10437
|
+
}
|
|
10146
10438
|
} finally {
|
|
10147
10439
|
clearTimeout(watchdog);
|
|
10148
|
-
runningGenerations.delete(
|
|
10440
|
+
for (const id of nodeIds) runningGenerations.delete(id);
|
|
10149
10441
|
const left = (reserved.get(engineId) ?? 0) - estimate;
|
|
10150
10442
|
if (left > 1e-9) reserved.set(engineId, left);
|
|
10151
10443
|
else reserved.delete(engineId);
|
|
@@ -10194,7 +10486,7 @@ function buildServer(opts) {
|
|
|
10194
10486
|
images: [core.images.save(await sharp20(args.srcBuf).extract(window).png().toBuffer())],
|
|
10195
10487
|
costUsd: 0
|
|
10196
10488
|
});
|
|
10197
|
-
void runNode(node2.id, null, 0, work2, { width: plan2.width, height: plan2.height }).catch(
|
|
10489
|
+
void runNode([node2.id], null, 0, work2, { width: plan2.width, height: plan2.height }).catch(
|
|
10198
10490
|
(err) => app.log.error({ err }, "crop run failed")
|
|
10199
10491
|
);
|
|
10200
10492
|
return reply.status(202).send(args.note ? { ...node2, warnings: [args.note] } : node2);
|
|
@@ -10351,6 +10643,14 @@ function buildServer(opts) {
|
|
|
10351
10643
|
const wantedCount = Math.min(Math.max(1, Number(count)), 8);
|
|
10352
10644
|
const lostIdentity = engine.capabilities().placeholder ? [] : (compiled2?.dropped ?? []).filter((d) => d.essential);
|
|
10353
10645
|
if (lostIdentity.length) {
|
|
10646
|
+
const missing = lostIdentity.filter((d) => d.reason === "missing");
|
|
10647
|
+
if (missing.length) {
|
|
10648
|
+
const names2 = joinNames(missing.map((d) => d.label));
|
|
10649
|
+
const kindWord2 = missing[0].role === "product" ? "product" : "presenter";
|
|
10650
|
+
return reply.code(400).send({
|
|
10651
|
+
error: `${names2} ${missing.length === 1 ? "has" : "have"} no usable photo, so the result would not be your ${kindWord2}. Re-add ${missing.length === 1 ? "its" : "their"} photo, or remove ${names2} from the brief.`
|
|
10652
|
+
});
|
|
10653
|
+
}
|
|
10354
10654
|
const names = joinNames(lostIdentity.map((d) => d.label));
|
|
10355
10655
|
const kindWord = lostIdentity[0].role === "product" ? "product" : "presenter";
|
|
10356
10656
|
return reply.code(400).send({
|
|
@@ -10361,6 +10661,19 @@ function buildServer(opts) {
|
|
|
10361
10661
|
const keptRefs = referenceImages && cap2 > 0 ? referenceImages.slice(0, cap2) : void 0;
|
|
10362
10662
|
const sentRefs = keptRefs && maxEdge ? await Promise.all(keptRefs.map((p) => capReferenceEdge(core, p, maxEdge))) : keptRefs;
|
|
10363
10663
|
const sentRoles = referenceRoles && cap2 > 0 ? referenceRoles.slice(0, cap2) : referenceRoles ?? [];
|
|
10664
|
+
if (process.env.SCENRI_DEBUG) {
|
|
10665
|
+
const sent = {};
|
|
10666
|
+
for (const r of sentRoles) sent[r] = (sent[r] ?? 0) + 1;
|
|
10667
|
+
app.log.info(
|
|
10668
|
+
{
|
|
10669
|
+
engine: engine.capabilities().id,
|
|
10670
|
+
cap: cap2,
|
|
10671
|
+
sent,
|
|
10672
|
+
dropped: (compiled2?.dropped ?? []).map((d) => `${d.role}:${d.label} (${d.reason ?? "budget"})`)
|
|
10673
|
+
},
|
|
10674
|
+
"reference transport"
|
|
10675
|
+
);
|
|
10676
|
+
}
|
|
10364
10677
|
const briefText = Array.isArray(brief?.tokens) ? brief.tokens.filter((t) => t?.t === "text").map((t) => String(t?.v ?? "")).join(" ") : String(prompt ?? "");
|
|
10365
10678
|
const variations = variationPlan(wantedCount, {
|
|
10366
10679
|
hasPresenter: sentRoles.includes("character"),
|
|
@@ -10392,6 +10705,20 @@ function buildServer(opts) {
|
|
|
10392
10705
|
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));
|
|
10393
10706
|
const editEdge = engine.capabilities().maxReferenceEdge;
|
|
10394
10707
|
if (editEdge) for (const r of editRefs) r.path = await capReferenceEdge(core, r.path, editEdge);
|
|
10708
|
+
if (process.env.SCENRI_DEBUG) {
|
|
10709
|
+
const sent = {};
|
|
10710
|
+
for (const r of editRefs) sent[String(r.role ?? "reference")] = (sent[String(r.role ?? "reference")] ?? 0) + 1;
|
|
10711
|
+
app.log.info(
|
|
10712
|
+
{
|
|
10713
|
+
engine: engine.capabilities().id,
|
|
10714
|
+
cap: Math.max(0, engine.capabilities().maxReferenceImages - 1),
|
|
10715
|
+
sourceFrame: true,
|
|
10716
|
+
sent,
|
|
10717
|
+
dropped: (mergedEdit?.dropped ?? []).map((d) => `${d.role}:${d.label} (${d.reason ?? "budget"})`)
|
|
10718
|
+
},
|
|
10719
|
+
"reference transport"
|
|
10720
|
+
);
|
|
10721
|
+
}
|
|
10395
10722
|
const srcBuf = core.images.read(String(srcHash));
|
|
10396
10723
|
const srcMeta = await sharp20(srcBuf).metadata();
|
|
10397
10724
|
if (srcMeta.width && srcMeta.height) expectShape = { width: srcMeta.width, height: srcMeta.height };
|
|
@@ -10559,15 +10886,26 @@ function buildServer(opts) {
|
|
|
10559
10886
|
}
|
|
10560
10887
|
const billedId = runEngine.capabilities().id;
|
|
10561
10888
|
core.ledger.assertUnderCap(billedId, estimate + (reserved.get(billedId) ?? 0));
|
|
10562
|
-
const
|
|
10889
|
+
const nodes = kind === "generation" ? core.store.addNodes({
|
|
10563
10890
|
projectId: project.id,
|
|
10564
10891
|
parentId: resolvedParentId,
|
|
10565
10892
|
kind,
|
|
10566
10893
|
prompt: finalPrompt,
|
|
10567
|
-
engineId: billedId
|
|
10568
|
-
|
|
10569
|
-
|
|
10570
|
-
|
|
10894
|
+
engineId: billedId,
|
|
10895
|
+
// the same clamp the engine request and the watchdog use
|
|
10896
|
+
count: Math.min(Math.max(1, Number(count)), 8)
|
|
10897
|
+
}) : [
|
|
10898
|
+
core.store.addNode({
|
|
10899
|
+
projectId: project.id,
|
|
10900
|
+
parentId: resolvedParentId,
|
|
10901
|
+
kind,
|
|
10902
|
+
prompt: finalPrompt,
|
|
10903
|
+
engineId: billedId
|
|
10904
|
+
})
|
|
10905
|
+
];
|
|
10906
|
+
const node = nodes[0];
|
|
10907
|
+
for (const sibling of brief ? nodes : [])
|
|
10908
|
+
core.store.setBrief(sibling.id, {
|
|
10571
10909
|
...briefInputsOnly(brief),
|
|
10572
10910
|
...editedFrom ? { sourceImage: editedFrom } : {},
|
|
10573
10911
|
...kind === "edit" && reshape ? { reshape } : {},
|
|
@@ -10714,14 +11052,23 @@ function buildServer(opts) {
|
|
|
10714
11052
|
staged = await conformToCanvas(node.id, expectShape)(staged);
|
|
10715
11053
|
}
|
|
10716
11054
|
return enforceEditCanvas(staged);
|
|
10717
|
-
} :
|
|
11055
|
+
} : void 0;
|
|
11056
|
+
const genW = compiled2?.width;
|
|
11057
|
+
const genH = compiled2?.height;
|
|
11058
|
+
const postFor = post !== void 0 ? () => post : kind === "generation" && genW && genH ? (id) => conformToCanvas(id, { width: genW, height: genH }) : void 0;
|
|
10718
11059
|
const runCaps = runEngine.capabilities();
|
|
10719
11060
|
const nodeBudgetMs = kind === "generation" && runCaps.perImageTimeoutMs ? Math.ceil(Math.min(Math.max(1, Number(count)), 8) / Math.max(1, runCaps.imageConcurrency ?? 1)) * runCaps.perImageTimeoutMs + 6e4 : void 0;
|
|
10720
|
-
void runNode(
|
|
10721
|
-
(
|
|
10722
|
-
|
|
11061
|
+
void runNode(
|
|
11062
|
+
nodes.map((n) => n.id),
|
|
11063
|
+
runEngine,
|
|
11064
|
+
estimate,
|
|
11065
|
+
work,
|
|
11066
|
+
expectShape,
|
|
11067
|
+
postFor,
|
|
11068
|
+
nodeBudgetMs
|
|
11069
|
+
).catch((err) => app.log.error({ err }, "node run failed"));
|
|
10723
11070
|
const allWarnings = [...compiled2?.warnings ?? [], ...extraWarnings];
|
|
10724
|
-
return reply.status(202).send(allWarnings.length ? {
|
|
11071
|
+
return reply.status(202).send({ ...node, siblings: nodes, ...allWarnings.length ? { warnings: allWarnings } : {} });
|
|
10725
11072
|
});
|
|
10726
11073
|
app.post("/api/nodes/:id/cancel", async (req, reply) => {
|
|
10727
11074
|
const id = req.params.id;
|
|
@@ -10794,7 +11141,10 @@ function buildServer(opts) {
|
|
|
10794
11141
|
runtime,
|
|
10795
11142
|
stageImpl: opts.stageImpl,
|
|
10796
11143
|
exitImpl: opts.exitImpl,
|
|
10797
|
-
|
|
11144
|
+
// one physical run counts once, however many sibling nodes share its
|
|
11145
|
+
// controller — an update gate held open by a 4-shot batch is still held
|
|
11146
|
+
// open by exactly one piece of work
|
|
11147
|
+
busyCount: () => new Set(runningGenerations.values()).size + runningImportCount() + runningAssetBuildCount()
|
|
10798
11148
|
});
|
|
10799
11149
|
let drained = null;
|
|
10800
11150
|
app.decorate("drain", () => {
|