scenri 0.7.4 → 0.8.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 +88 -0
- package/dist/index.js +1 -1
- package/dist/{launcher-RETYUGSR.js → launcher-DIZ4MQRF.js} +2 -2
- package/dist/serve.js +590 -124
- package/package.json +2 -2
- package/studio-dist/assets/{index-B6onUYQF.css → index-CLW2Ca_7.css} +1 -1
- package/studio-dist/assets/index-D0hEbSDx.js +104 -0
- package/studio-dist/index.html +2 -2
- package/studio-dist/assets/index-BxcB2_fl.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),
|
|
@@ -656,6 +806,16 @@ function createStore(db) {
|
|
|
656
806
|
id
|
|
657
807
|
);
|
|
658
808
|
},
|
|
809
|
+
/**
|
|
810
|
+
* The run's money, written once it is known. A batch's first sibling used
|
|
811
|
+
* to be charged inside completeNode, at the end of the whole call; a
|
|
812
|
+
* sibling now completes the moment its own image lands, and the cost is
|
|
813
|
+
* only known when the call resolves, so it is written afterwards, onto a
|
|
814
|
+
* node that finished. A failed or running node keeps 0, as before.
|
|
815
|
+
*/
|
|
816
|
+
chargeNode(id, costUsd) {
|
|
817
|
+
db.prepare("UPDATE nodes SET cost_usd=? WHERE id=? AND status='done'").run(costUsd, id);
|
|
818
|
+
},
|
|
659
819
|
failNode(id, error) {
|
|
660
820
|
db.prepare("UPDATE nodes SET status='error', error=? WHERE id=?").run(error, id);
|
|
661
821
|
},
|
|
@@ -1454,7 +1614,17 @@ function createOpenRouterEngine(opts) {
|
|
|
1454
1614
|
localOnly: false,
|
|
1455
1615
|
supportsEdit: true,
|
|
1456
1616
|
supportsMask: false,
|
|
1617
|
+
// Four is OUR conservative constant, not a provider fact: OpenRouter
|
|
1618
|
+
// multiplexes many image models and their input limits differ, so
|
|
1619
|
+
// there is no single upstream number to cite the way codex's five is
|
|
1620
|
+
// cited. Four keeps a full identity payload (product essential +
|
|
1621
|
+
// angle, presenter, mark) inside every model we have routed to.
|
|
1457
1622
|
maxReferenceImages: 4,
|
|
1623
|
+
// Same uplink argument as codex: the provider reads references at
|
|
1624
|
+
// reduced resolution anyway, and these ride base64-inlined inside a
|
|
1625
|
+
// JSON body — a full-resolution phone photo is tens of megabytes of
|
|
1626
|
+
// request for nothing.
|
|
1627
|
+
maxReferenceEdge: 2048,
|
|
1458
1628
|
// N sequential calls, one image each: the server budgets the node by
|
|
1459
1629
|
// that shape instead of handing the whole run one flat ten minutes.
|
|
1460
1630
|
perImageTimeoutMs: PER_IMAGE_TIMEOUT_MS,
|
|
@@ -1470,7 +1640,7 @@ function createOpenRouterEngine(opts) {
|
|
|
1470
1640
|
const count = "count" in req ? req.count : 1;
|
|
1471
1641
|
return count * perImageUsd;
|
|
1472
1642
|
},
|
|
1473
|
-
async generate(req, signal) {
|
|
1643
|
+
async generate(req, signal, onImage) {
|
|
1474
1644
|
const key = requireKey();
|
|
1475
1645
|
const roles = req.referenceRoles ?? [];
|
|
1476
1646
|
const refs = req.referenceImages ?? [];
|
|
@@ -1506,7 +1676,9 @@ function createOpenRouterEngine(opts) {
|
|
|
1506
1676
|
signal
|
|
1507
1677
|
);
|
|
1508
1678
|
raws.push(json);
|
|
1509
|
-
|
|
1679
|
+
const own = extractImages(json).map((buf) => opts.saveImage(buf));
|
|
1680
|
+
hashes.push(...own);
|
|
1681
|
+
if (own[0]) onImage?.(i, own[0]);
|
|
1510
1682
|
if (typeof json?.usage?.cost === "number") {
|
|
1511
1683
|
reportedCost += json.usage.cost;
|
|
1512
1684
|
sawReportedCost = true;
|
|
@@ -2703,7 +2875,7 @@ function createCodexEngine(opts) {
|
|
|
2703
2875
|
async costEstimate() {
|
|
2704
2876
|
return 0;
|
|
2705
2877
|
},
|
|
2706
|
-
async generate(req, signal) {
|
|
2878
|
+
async generate(req, signal, onImage) {
|
|
2707
2879
|
const count = Math.max(1, req.count);
|
|
2708
2880
|
const refs = req.referenceImages ?? [];
|
|
2709
2881
|
const roles = req.referenceRoles ?? refs.map(() => "reference");
|
|
@@ -2742,6 +2914,7 @@ function createCodexEngine(opts) {
|
|
|
2742
2914
|
const i = next++;
|
|
2743
2915
|
try {
|
|
2744
2916
|
results[i] = await jobs[i]();
|
|
2917
|
+
if (results[i][0]) onImage?.(i, results[i][0]);
|
|
2745
2918
|
} catch (err) {
|
|
2746
2919
|
if (signal?.aborted && signal.reason !== BUDGET_EXHAUSTED) throw err;
|
|
2747
2920
|
results[i] = [];
|
|
@@ -2881,7 +3054,30 @@ function createEngineRegistry(core, extra = []) {
|
|
|
2881
3054
|
codexRunner
|
|
2882
3055
|
};
|
|
2883
3056
|
}
|
|
2884
|
-
function
|
|
3057
|
+
function demoOptionsFromEnv(env) {
|
|
3058
|
+
const out = {};
|
|
3059
|
+
const stagger = Number(env.SCENRI_DEMO_STAGGER_MS);
|
|
3060
|
+
if (env.SCENRI_DEMO_STAGGER_MS && Number.isFinite(stagger) && stagger > 0) out.staggerMs = stagger;
|
|
3061
|
+
if (env.SCENRI_DEMO_ORDER === "reverse") out.order = "reverse";
|
|
3062
|
+
const fail = Number(env.SCENRI_DEMO_FAIL_SLOT);
|
|
3063
|
+
if (env.SCENRI_DEMO_FAIL_SLOT && Number.isInteger(fail) && fail >= 0) out.failSlot = fail;
|
|
3064
|
+
return out;
|
|
3065
|
+
}
|
|
3066
|
+
function sleep2(ms, signal) {
|
|
3067
|
+
return new Promise((resolve) => {
|
|
3068
|
+
if (signal?.aborted) return resolve();
|
|
3069
|
+
const onAbort = () => {
|
|
3070
|
+
clearTimeout(timer);
|
|
3071
|
+
resolve();
|
|
3072
|
+
};
|
|
3073
|
+
const timer = setTimeout(() => {
|
|
3074
|
+
signal?.removeEventListener("abort", onAbort);
|
|
3075
|
+
resolve();
|
|
3076
|
+
}, ms);
|
|
3077
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
3078
|
+
});
|
|
3079
|
+
}
|
|
3080
|
+
function createDemoEngine(saveImage, opts = {}) {
|
|
2885
3081
|
const paletteOf = (req) => {
|
|
2886
3082
|
const p = req.brand?.brand?.palette;
|
|
2887
3083
|
const hexes = [p?.primary?.hex, p?.secondary?.hex, ...(p?.accent ?? []).map((a) => a?.hex)].filter(
|
|
@@ -2927,13 +3123,31 @@ function createDemoEngine(saveImage) {
|
|
|
2927
3123
|
async costEstimate() {
|
|
2928
3124
|
return 0;
|
|
2929
3125
|
},
|
|
2930
|
-
async generate(req) {
|
|
3126
|
+
async generate(req, signal, onImage) {
|
|
2931
3127
|
const colors = paletteOf(req);
|
|
2932
|
-
const
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
|
|
3128
|
+
const count = Math.max(1, req.count);
|
|
3129
|
+
const slots = Array.from({ length: count }, (_, i) => i);
|
|
3130
|
+
if (opts.order === "reverse") slots.reverse();
|
|
3131
|
+
const landed = /* @__PURE__ */ new Map();
|
|
3132
|
+
const failures = [];
|
|
3133
|
+
for (const [position, slot] of slots.entries()) {
|
|
3134
|
+
if (position > 0 && opts.staggerMs) await sleep2(opts.staggerMs, signal);
|
|
3135
|
+
if (signal?.aborted) {
|
|
3136
|
+
if (signal.reason === BUDGET_EXHAUSTED) break;
|
|
3137
|
+
throw Object.assign(new Error("generation cancelled"), { name: "AbortError" });
|
|
3138
|
+
}
|
|
3139
|
+
if (slot === opts.failSlot) {
|
|
3140
|
+
failures.push(`demo: slot ${slot + 1} refused`);
|
|
3141
|
+
continue;
|
|
3142
|
+
}
|
|
3143
|
+
const hash = saveImage(await render(colors, req.prompt, req.width, req.height, slot + req.prompt.length));
|
|
3144
|
+
landed.set(slot, hash);
|
|
3145
|
+
onImage?.(slot, hash);
|
|
3146
|
+
}
|
|
3147
|
+
const done = [...landed.keys()].sort((a, b) => a - b);
|
|
3148
|
+
const images = done.map((slot) => landed.get(slot));
|
|
3149
|
+
if (done.length === count) return { images, costUsd: 0 };
|
|
3150
|
+
return { images, costUsd: 0, raw: { requested: count, variantIndexes: done, partialFailures: failures } };
|
|
2937
3151
|
},
|
|
2938
3152
|
async edit(req) {
|
|
2939
3153
|
const colors = paletteOf(req);
|
|
@@ -3238,13 +3452,22 @@ function defaultDemoProductsDir() {
|
|
|
3238
3452
|
}
|
|
3239
3453
|
|
|
3240
3454
|
// src/attachmentBudget.ts
|
|
3455
|
+
var SEAT_TIER = {
|
|
3456
|
+
brand: 0,
|
|
3457
|
+
reference: 0,
|
|
3458
|
+
product: 0,
|
|
3459
|
+
character: 0,
|
|
3460
|
+
scene: 0,
|
|
3461
|
+
composition: 1,
|
|
3462
|
+
style: 2
|
|
3463
|
+
};
|
|
3241
3464
|
var ROLE_PRIORITY = {
|
|
3242
3465
|
product: 0,
|
|
3243
3466
|
character: 1,
|
|
3244
3467
|
brand: 2,
|
|
3245
|
-
|
|
3246
|
-
|
|
3247
|
-
|
|
3468
|
+
reference: 3,
|
|
3469
|
+
scene: 4,
|
|
3470
|
+
composition: 5,
|
|
3248
3471
|
style: 6
|
|
3249
3472
|
};
|
|
3250
3473
|
function allocateAttachments(attachments, cap2) {
|
|
@@ -3260,11 +3483,8 @@ function allocateAttachments(attachments, cap2) {
|
|
|
3260
3483
|
kept.add(x.i);
|
|
3261
3484
|
keptGroups.add(groupOf(x.a));
|
|
3262
3485
|
};
|
|
3263
|
-
|
|
3264
|
-
|
|
3265
|
-
if (x.a.essential) admit(x);
|
|
3266
|
-
}
|
|
3267
|
-
for (const x of legacyOrder) {
|
|
3486
|
+
const seatOrder = [...indexed].sort((x, y) => SEAT_TIER[x.a.role] - SEAT_TIER[y.a.role] || x.i - y.i);
|
|
3487
|
+
for (const x of seatOrder) {
|
|
3268
3488
|
if (kept.size >= max) break;
|
|
3269
3489
|
if (!kept.has(x.i) && !keptGroups.has(groupOf(x.a))) admit(x);
|
|
3270
3490
|
}
|
|
@@ -3289,7 +3509,13 @@ function allocateAttachments(attachments, cap2) {
|
|
|
3289
3509
|
}
|
|
3290
3510
|
return {
|
|
3291
3511
|
kept: legacyOrder.filter((x) => kept.has(x.i)).map((x) => x.a),
|
|
3292
|
-
dropped: legacyOrder.filter((x) => !kept.has(x.i)).map((x) => x.a)
|
|
3512
|
+
dropped: legacyOrder.filter((x) => !kept.has(x.i)).map((x) => x.a),
|
|
3513
|
+
// The same images in the order the brief placed them, for a caller that
|
|
3514
|
+
// will allocate again: `kept` is re-sorted by role for the consumers
|
|
3515
|
+
// below, and feeding that back into a second, tighter allocation put
|
|
3516
|
+
// every product ahead of every face on a refinement whatever the line
|
|
3517
|
+
// said.
|
|
3518
|
+
seated: seatOrder.filter((x) => kept.has(x.i)).map((x) => x.a)
|
|
3293
3519
|
};
|
|
3294
3520
|
}
|
|
3295
3521
|
function mergeEditAttachments(own, inherited, cap2) {
|
|
@@ -3454,12 +3680,19 @@ function shotSpecifiesCamera(text) {
|
|
|
3454
3680
|
text
|
|
3455
3681
|
);
|
|
3456
3682
|
}
|
|
3683
|
+
function namesAreNotLetteringDirective() {
|
|
3684
|
+
return "The names in this brief identify what to show and are never text to render: no caption, label, signage, engraving or lettering spells a product name or a person's name, in any language or script, anywhere in the picture. Printing that is part of a product's own packaging stays exactly as photographed, and nothing else spells a name unless the direction above explicitly asks for it to be written.";
|
|
3685
|
+
}
|
|
3457
3686
|
function sceneFigureDirectives(opts) {
|
|
3458
3687
|
const figure = opts.figure.trim().replace(/[.\s]+$/, "");
|
|
3459
3688
|
if (!figure) return [];
|
|
3460
3689
|
const treatment = (opts.treatment ?? "").trim().replace(/[.\s]+$/, "");
|
|
3461
3690
|
const out = [];
|
|
3462
|
-
if (opts.hasPerson) {
|
|
3691
|
+
if (opts.hasPerson && (opts.people ?? 1) > 1) {
|
|
3692
|
+
out.push(
|
|
3693
|
+
`This world is built around one figure: ${figure}. The attached presenters share that role: the first named presenter takes the figure position and the others stand with them in the same frame, every one of them clearly visible. Any person the scene direction describes IS one of the attached presenters and never an extra person, and each identity comes from their own attached photograph alone, never from anything the scene direction says about a body.`
|
|
3694
|
+
);
|
|
3695
|
+
} else if (opts.hasPerson) {
|
|
3463
3696
|
out.push(
|
|
3464
3697
|
`This world is built around one figure: ${figure}. The attached presenter is that figure. Any person the scene direction describes IS the presenter and never a second person, and their identity comes from their own attached photograph alone, never from anything the scene direction says about a body.`
|
|
3465
3698
|
);
|
|
@@ -3580,6 +3813,8 @@ function validateBrief(brief) {
|
|
|
3580
3813
|
case "ref":
|
|
3581
3814
|
case "mark":
|
|
3582
3815
|
if (!str4(t.imageHash)) errors.push(`${at}.imageHash must be a non-empty string`);
|
|
3816
|
+
if (t.t === "ref" && t.label !== void 0 && typeof t.label !== "string")
|
|
3817
|
+
errors.push(`${at}.label must be a string when present`);
|
|
3583
3818
|
break;
|
|
3584
3819
|
case "format":
|
|
3585
3820
|
if (!Number.isFinite(t.w) || !Number.isFinite(t.h) || Number(t.w) <= 0 || Number(t.h) <= 0)
|
|
@@ -3594,6 +3829,7 @@ function validateBrief(brief) {
|
|
|
3594
3829
|
function compileBrief(brief, ctx) {
|
|
3595
3830
|
const warnings = [];
|
|
3596
3831
|
const attachments = [];
|
|
3832
|
+
const unattachable = [];
|
|
3597
3833
|
const rawSceneFallback = [];
|
|
3598
3834
|
const productDirectives = [];
|
|
3599
3835
|
const personDirectives = [];
|
|
@@ -3605,6 +3841,7 @@ function compileBrief(brief, ctx) {
|
|
|
3605
3841
|
const characters = ctx.brand?.characters ?? [];
|
|
3606
3842
|
const inlineTemplates = [];
|
|
3607
3843
|
let hasPerson = false;
|
|
3844
|
+
let people = 0;
|
|
3608
3845
|
let sentence = "";
|
|
3609
3846
|
const append = (s) => {
|
|
3610
3847
|
sentence += (sentence && !sentence.endsWith(" ") ? " " : "") + s;
|
|
@@ -3645,7 +3882,7 @@ function compileBrief(brief, ctx) {
|
|
|
3645
3882
|
...angle ? { angle } : {}
|
|
3646
3883
|
});
|
|
3647
3884
|
});
|
|
3648
|
-
productDirectives.push(
|
|
3885
|
+
productDirectives.push({ need: "fidelity", id: p.id });
|
|
3649
3886
|
productDirectives.push(...productFactDirectives(p));
|
|
3650
3887
|
if (p.description && !p.dimensions)
|
|
3651
3888
|
productDirectives.push(
|
|
@@ -3653,6 +3890,7 @@ function compileBrief(brief, ctx) {
|
|
|
3653
3890
|
);
|
|
3654
3891
|
} else {
|
|
3655
3892
|
warnings.push(`${p.name} has no usable photo, so it is named but not attached.`);
|
|
3893
|
+
unattachable.push({ role: "product", id: p.id, label: p.name, hash: "", essential: true, reason: "missing" });
|
|
3656
3894
|
}
|
|
3657
3895
|
break;
|
|
3658
3896
|
}
|
|
@@ -3663,6 +3901,7 @@ function compileBrief(brief, ctx) {
|
|
|
3663
3901
|
break;
|
|
3664
3902
|
}
|
|
3665
3903
|
hasPerson = true;
|
|
3904
|
+
people += 1;
|
|
3666
3905
|
append(c.promptName ?? c.name);
|
|
3667
3906
|
const cshots = (c.shots ?? []).slice(0, CHARACTER_REF_MAX).map((s) => ({ h: assetHash2(s?.file), angle: s?.angle ? String(s.angle) : void 0 })).filter((x) => !!x.h && ctx.images.has(x.h));
|
|
3668
3907
|
if (cshots.length) {
|
|
@@ -3694,13 +3933,23 @@ function compileBrief(brief, ctx) {
|
|
|
3694
3933
|
if (c.build) personDirectives.push(`${c.promptName ?? c.name}'s build: ${c.build}.`);
|
|
3695
3934
|
} else {
|
|
3696
3935
|
warnings.push(`${c.name} has no usable photo, so they are named but not attached.`);
|
|
3936
|
+
unattachable.push({
|
|
3937
|
+
role: "character",
|
|
3938
|
+
id: c.id,
|
|
3939
|
+
label: c.name,
|
|
3940
|
+
hash: "",
|
|
3941
|
+
essential: true,
|
|
3942
|
+
reason: "missing"
|
|
3943
|
+
});
|
|
3697
3944
|
}
|
|
3698
3945
|
break;
|
|
3699
3946
|
}
|
|
3700
3947
|
case "color": {
|
|
3701
3948
|
const hex = tok.hex.toUpperCase();
|
|
3702
|
-
append(tok.name ?
|
|
3703
|
-
otherDirectives.push(
|
|
3949
|
+
append(tok.name ? `(brand color ${tok.name} ${hex})` : `(brand color ${hex})`);
|
|
3950
|
+
otherDirectives.push(
|
|
3951
|
+
`Use ${hex} as a defining color in the composition, in surfaces, materials and light, never as lettering.`
|
|
3952
|
+
);
|
|
3704
3953
|
break;
|
|
3705
3954
|
}
|
|
3706
3955
|
case "ref": {
|
|
@@ -3713,7 +3962,12 @@ function compileBrief(brief, ctx) {
|
|
|
3713
3962
|
break;
|
|
3714
3963
|
}
|
|
3715
3964
|
attachments.push({ role: "reference", label: "Reference shot", hash: tok.imageHash });
|
|
3716
|
-
otherDirectives.push(
|
|
3965
|
+
otherDirectives.push({
|
|
3966
|
+
need: "attachment",
|
|
3967
|
+
role: "reference",
|
|
3968
|
+
hash: tok.imageHash,
|
|
3969
|
+
text: "Match the composition, lighting and treatment of the attached reference."
|
|
3970
|
+
});
|
|
3717
3971
|
break;
|
|
3718
3972
|
}
|
|
3719
3973
|
case "mark": {
|
|
@@ -3733,9 +3987,12 @@ function compileBrief(brief, ctx) {
|
|
|
3733
3987
|
);
|
|
3734
3988
|
} catch {
|
|
3735
3989
|
}
|
|
3736
|
-
otherDirectives.push(
|
|
3737
|
-
"
|
|
3738
|
-
|
|
3990
|
+
otherDirectives.push({
|
|
3991
|
+
need: "attachment",
|
|
3992
|
+
role: "brand",
|
|
3993
|
+
hash: tok.imageHash,
|
|
3994
|
+
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."
|
|
3995
|
+
});
|
|
3739
3996
|
break;
|
|
3740
3997
|
}
|
|
3741
3998
|
case "template": {
|
|
@@ -3815,8 +4072,50 @@ function compileBrief(brief, ctx) {
|
|
|
3815
4072
|
if (i !== -1) attachments.splice(i, 1);
|
|
3816
4073
|
}
|
|
3817
4074
|
}
|
|
4075
|
+
const identityHashes = /* @__PURE__ */ new Map();
|
|
4076
|
+
for (const a of attachments)
|
|
4077
|
+
if ((a.role === "product" || a.role === "character") && !identityHashes.has(a.hash))
|
|
4078
|
+
identityHashes.set(a.hash, a.label);
|
|
4079
|
+
for (let i = attachments.length - 1; i >= 0; i--) {
|
|
4080
|
+
const a = attachments[i];
|
|
4081
|
+
if (a.role === "reference" && identityHashes.has(a.hash)) {
|
|
4082
|
+
attachments.splice(i, 1);
|
|
4083
|
+
warnings.push(
|
|
4084
|
+
`That reference is the same image as ${identityHashes.get(a.hash)}'s own photo, so it rides once, as the identity.`
|
|
4085
|
+
);
|
|
4086
|
+
}
|
|
4087
|
+
}
|
|
3818
4088
|
const max = ctx.engineCaps.maxReferenceImages;
|
|
3819
|
-
const { kept, dropped } = allocateAttachments(attachments, max);
|
|
4089
|
+
const { kept, dropped: budgetDropped, seated } = allocateAttachments(attachments, max);
|
|
4090
|
+
const presentKeys = new Set((ctx.presentAttachments ?? kept).map((a) => `${a.role}:${a.hash}`));
|
|
4091
|
+
const resolveDirective = (d) => {
|
|
4092
|
+
if (typeof d === "string") return d;
|
|
4093
|
+
if (d.need === "fidelity") {
|
|
4094
|
+
const n = attachments.filter(
|
|
4095
|
+
(a) => a.role === "product" && a.id === d.id && presentKeys.has(`product:${a.hash}`)
|
|
4096
|
+
).length;
|
|
4097
|
+
return n > 0 ? productFidelityDirective(n) : null;
|
|
4098
|
+
}
|
|
4099
|
+
return presentKeys.has(`${d.role}:${d.hash}`) ? d.text : null;
|
|
4100
|
+
};
|
|
4101
|
+
const absentDirectives = [];
|
|
4102
|
+
const absentSeen = /* @__PURE__ */ new Set();
|
|
4103
|
+
for (const a of attachments) {
|
|
4104
|
+
const key = `${a.role}:${a.hash}`;
|
|
4105
|
+
if (presentKeys.has(key) || absentSeen.has(key)) continue;
|
|
4106
|
+
if (a.role === "reference") {
|
|
4107
|
+
absentSeen.add(key);
|
|
4108
|
+
const words = ctx.wordsFor?.(a.hash) ?? null;
|
|
4109
|
+
absentDirectives.push(
|
|
4110
|
+
words ? `A reference shot was not attached this time; it showed ${words}. Match that composition, lighting and treatment.` : "A reference image was attached but not sent this time."
|
|
4111
|
+
);
|
|
4112
|
+
} else if (a.role === "brand") {
|
|
4113
|
+
absentSeen.add(key);
|
|
4114
|
+
absentDirectives.push(
|
|
4115
|
+
`The brand mark "${a.label}" was not attached this time; keep every branded surface plain and do not invent a logo.`
|
|
4116
|
+
);
|
|
4117
|
+
}
|
|
4118
|
+
}
|
|
3820
4119
|
const guard = scene ? sceneGuardDirectives({
|
|
3821
4120
|
hasProduct: !!productId,
|
|
3822
4121
|
hasPerson,
|
|
@@ -3833,13 +4132,16 @@ function compileBrief(brief, ctx) {
|
|
|
3833
4132
|
"Any earlier instruction that bans props, hands, people, or a presenter from the frame is a solo-packshot rule for this product and does not apply to this shot: the attached presenter is deliberate and must appear as directed.",
|
|
3834
4133
|
productHandlingDirective()
|
|
3835
4134
|
] : [];
|
|
4135
|
+
const nameDirectives = productId || hasPerson ? [namesAreNotLetteringDirective()] : [];
|
|
3836
4136
|
const figureDirectives = scene?.figure ? sceneFigureDirectives({
|
|
3837
4137
|
figure: scene.figure,
|
|
3838
4138
|
treatment: scene.figureTreatment,
|
|
3839
4139
|
hasPerson,
|
|
4140
|
+
people,
|
|
3840
4141
|
// The treatment's fictional-brands rule needs to know a real mark is
|
|
3841
|
-
// deliberately in play
|
|
3842
|
-
|
|
4142
|
+
// deliberately in play - and only one that actually rides counts,
|
|
4143
|
+
// same honesty rule as the photo guard above.
|
|
4144
|
+
hasMark: [...presentKeys].some((k) => k.startsWith("brand:"))
|
|
3843
4145
|
}) : [];
|
|
3844
4146
|
if (hasPerson) personDirectives.push(personSkinDirective());
|
|
3845
4147
|
const closeUpDirectives = hasPerson && /\bclose[- ]?up\b|\bmacro\b|\bzoom(?:ed)?\b|\bDOF\b|\bdepth of field\b/i.test(sentence) ? [
|
|
@@ -3864,12 +4166,16 @@ function compileBrief(brief, ctx) {
|
|
|
3864
4166
|
}) ? [garmentDisplayDirective()] : [];
|
|
3865
4167
|
const refGuard = ctx.mode !== "edit" && hasPerson && kept.some((a) => a.role === "reference") ? [referenceIdentityGuard()] : [];
|
|
3866
4168
|
const allDirectives = [
|
|
4169
|
+
// First, beside the sentence that names things: the model reads the names
|
|
4170
|
+
// and this line in one breath, before any spec repeats them.
|
|
4171
|
+
...nameDirectives,
|
|
3867
4172
|
...productDirectives,
|
|
3868
4173
|
...personDirectives,
|
|
3869
4174
|
...pairDirectives,
|
|
3870
4175
|
...figureDirectives,
|
|
3871
4176
|
...closeUpDirectives,
|
|
3872
4177
|
...otherDirectives,
|
|
4178
|
+
...absentDirectives,
|
|
3873
4179
|
...cameraDirectives,
|
|
3874
4180
|
...apparelUnworn,
|
|
3875
4181
|
...brandLines,
|
|
@@ -3877,31 +4183,59 @@ function compileBrief(brief, ctx) {
|
|
|
3877
4183
|
...refGuard,
|
|
3878
4184
|
...preservation
|
|
3879
4185
|
];
|
|
3880
|
-
|
|
3881
|
-
if (
|
|
4186
|
+
const spoken = dedupe(allDirectives.map(resolveDirective).filter((s) => s !== null));
|
|
4187
|
+
if (spoken.length) prompt = `${prompt}${prompt.endsWith(".") ? "" : "."} ${spoken.join(" ")}`;
|
|
4188
|
+
if (max === 0 && budgetDropped.some((d) => d.role !== "scene")) {
|
|
3882
4189
|
const keptLabels = new Set(kept.map((a) => a.label));
|
|
3883
|
-
const names = [...new Set(
|
|
4190
|
+
const names = [...new Set(budgetDropped.filter((d) => d.role !== "scene").map((d) => d.label))].filter(
|
|
3884
4191
|
(l) => !keptLabels.has(l)
|
|
3885
4192
|
);
|
|
3886
4193
|
if (names.length) {
|
|
3887
|
-
const reads = max === 0 ? "reads no reference images" : `reads ${max} reference image${max === 1 ? "" : "s"}`;
|
|
3888
4194
|
warnings.push(
|
|
3889
|
-
`${
|
|
4195
|
+
`${names.join(" and ")} ${names.length === 1 ? "was" : "were"} left out \u2014 ${ctx.engineCaps.displayName} reads no reference images.`
|
|
3890
4196
|
);
|
|
3891
4197
|
}
|
|
3892
4198
|
}
|
|
3893
4199
|
return {
|
|
3894
4200
|
prompt: prompt.trim(),
|
|
3895
4201
|
referenceImages: kept.map((a) => ctx.images.pathFor(a.hash)),
|
|
3896
|
-
|
|
4202
|
+
// The missing-photo identities lead: they are essential, and the refusal
|
|
4203
|
+
// path reads this list. Budget losses carry their reason for the chips.
|
|
4204
|
+
dropped: [...unattachable, ...budgetDropped.map((d) => ({ ...d, reason: "budget" }))],
|
|
3897
4205
|
width,
|
|
3898
4206
|
height,
|
|
3899
4207
|
attachments: kept,
|
|
4208
|
+
seated,
|
|
3900
4209
|
warnings,
|
|
3901
4210
|
productId
|
|
3902
4211
|
};
|
|
3903
4212
|
}
|
|
3904
4213
|
var dedupe = (xs) => [...new Set(xs)];
|
|
4214
|
+
|
|
4215
|
+
// src/shotWords.ts
|
|
4216
|
+
function shotWords(prompt, max = 160) {
|
|
4217
|
+
if (!prompt) return null;
|
|
4218
|
+
const head = prompt.split(/\. (?=[A-Z])/)[0]?.trim() ?? "";
|
|
4219
|
+
if (!head) return null;
|
|
4220
|
+
const clipped = head.length > max ? `${head.slice(0, max).replace(/\s+\S*$/, "")}\u2026` : head;
|
|
4221
|
+
return clipped.replace(/[.\s]+$/, "");
|
|
4222
|
+
}
|
|
4223
|
+
function shotWordsFor(core, brandId) {
|
|
4224
|
+
let byHash = null;
|
|
4225
|
+
return (hash) => {
|
|
4226
|
+
if (!byHash) {
|
|
4227
|
+
byHash = /* @__PURE__ */ new Map();
|
|
4228
|
+
for (const p of core.store.listProjects(brandId)) {
|
|
4229
|
+
for (const n of core.store.treeFor(p.id)) {
|
|
4230
|
+
const words = shotWords(n.prompt);
|
|
4231
|
+
if (!words) continue;
|
|
4232
|
+
for (const h of n.images ?? []) if (!byHash.has(h)) byHash.set(h, words);
|
|
4233
|
+
}
|
|
4234
|
+
}
|
|
4235
|
+
}
|
|
4236
|
+
return byHash.get(hash) ?? null;
|
|
4237
|
+
};
|
|
4238
|
+
}
|
|
3905
4239
|
var DEFAULT_CONTENT_URL = "https://github.com/tonygorb/scenri/releases/download/content-latest/scenri-content.zip";
|
|
3906
4240
|
var TIMEOUT_MS = 10 * 60 * 1e3;
|
|
3907
4241
|
function resolveContentUrl(env = process.env, override) {
|
|
@@ -4828,7 +5162,7 @@ function dedupeProducts(products) {
|
|
|
4828
5162
|
|
|
4829
5163
|
// ../catalog/src/http/fetch.ts
|
|
4830
5164
|
var USER_AGENT = "scenri-catalog/0.1 (+https://scenri.co)";
|
|
4831
|
-
function
|
|
5165
|
+
function sleep3(ms) {
|
|
4832
5166
|
return new Promise((r) => setTimeout(r, ms));
|
|
4833
5167
|
}
|
|
4834
5168
|
async function httpGet(url, opts = {}) {
|
|
@@ -4852,7 +5186,7 @@ async function httpGet(url, opts = {}) {
|
|
|
4852
5186
|
}
|
|
4853
5187
|
});
|
|
4854
5188
|
if ((res.status === 429 || res.status >= 500) && attempt < retries) {
|
|
4855
|
-
await
|
|
5189
|
+
await sleep3(400 * 2 ** attempt);
|
|
4856
5190
|
continue;
|
|
4857
5191
|
}
|
|
4858
5192
|
return res;
|
|
@@ -4860,7 +5194,7 @@ async function httpGet(url, opts = {}) {
|
|
|
4860
5194
|
lastErr = err;
|
|
4861
5195
|
if (opts.signal?.aborted) throw err;
|
|
4862
5196
|
if (attempt < retries) {
|
|
4863
|
-
await
|
|
5197
|
+
await sleep3(400 * 2 ** attempt);
|
|
4864
5198
|
continue;
|
|
4865
5199
|
}
|
|
4866
5200
|
throw err;
|
|
@@ -7830,11 +8164,6 @@ function registerLogoRoutes(app, deps) {
|
|
|
7830
8164
|
if (!v.valid) return reply.status(400).send({ error: "brand became invalid", details: v.errors });
|
|
7831
8165
|
return core.store.updateBrand(brand.id, json);
|
|
7832
8166
|
});
|
|
7833
|
-
app.get("/api/brands/:id/directives", async (req, reply) => {
|
|
7834
|
-
const brand = core.store.getBrand(req.params.id);
|
|
7835
|
-
if (!brand) return reply.status(404).send({ error: "brand not found" });
|
|
7836
|
-
return { directives: brandRuleDirectives(brand.json) };
|
|
7837
|
-
});
|
|
7838
8167
|
app.delete("/api/brands/:id/logos/:hash", async (req, reply) => {
|
|
7839
8168
|
const brand = core.store.getBrand(req.params.id);
|
|
7840
8169
|
if (!brand) return reply.status(404).send({ error: "brand not found" });
|
|
@@ -8760,6 +9089,48 @@ function registerImageRoutes(app, deps) {
|
|
|
8760
9089
|
|
|
8761
9090
|
// src/release/notes.data.ts
|
|
8762
9091
|
var RELEASES = [
|
|
9092
|
+
{
|
|
9093
|
+
version: "0.8.0",
|
|
9094
|
+
date: "2026-09-02",
|
|
9095
|
+
title: "The composer, rebuilt around what a shot is made of.",
|
|
9096
|
+
sections: [
|
|
9097
|
+
{
|
|
9098
|
+
heading: "Create",
|
|
9099
|
+
body: "Ingredients are compact chips now, each carrying the picture it stands for, and hovering one shows you what it is holding. A shot can hold twelve of them: the ones your engine can photograph are lit, and the rest ride along in words rather than being dropped, with every chip saying which it is. Photos go out in the order you wrote them, so the first thing you named is the first thing pictured. The asset panel beside the brief is a second door into it, so a tile ticks when its chip is in and clicking it again takes the chip out."
|
|
9100
|
+
},
|
|
9101
|
+
{
|
|
9102
|
+
heading: "Refine",
|
|
9103
|
+
body: "The panel beside an open shot has been redrawn, and its edge can be dragged to the width you want. Above the brief it shows what the picture is actually made of, resolved through the whole chain of refinements rather than just the last one, so nothing is repeated and nothing is lost at depth."
|
|
9104
|
+
},
|
|
9105
|
+
{
|
|
9106
|
+
heading: "Brand",
|
|
9107
|
+
body: "A product name is something to show in a shot, never lettering to paint into it, and a brand colour is now spoken as a note rather than an instruction, which keeps invented signage out of your pictures. Brand rules apply to every shot from Settings, so the composer no longer carries a row to say so."
|
|
9108
|
+
},
|
|
9109
|
+
{
|
|
9110
|
+
heading: "Fixes",
|
|
9111
|
+
body: "A finished shot appears the moment it lands instead of waiting for the rest of its batch, and a batch sent from a set files every shot in it. Typing around a chip behaves: the chip owns the space beside it, one press removes it, and two chips always keep the single space between them. Two presenters in a scene no longer compose one of them out."
|
|
9112
|
+
}
|
|
9113
|
+
]
|
|
9114
|
+
},
|
|
9115
|
+
{
|
|
9116
|
+
version: "0.7.5",
|
|
9117
|
+
date: "2026-09-01",
|
|
9118
|
+
title: "Every shot is one card.",
|
|
9119
|
+
sections: [
|
|
9120
|
+
{
|
|
9121
|
+
heading: "Create",
|
|
9122
|
+
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."
|
|
9123
|
+
},
|
|
9124
|
+
{
|
|
9125
|
+
heading: "Refining",
|
|
9126
|
+
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."
|
|
9127
|
+
},
|
|
9128
|
+
{
|
|
9129
|
+
heading: "Fixes",
|
|
9130
|
+
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."
|
|
9131
|
+
}
|
|
9132
|
+
]
|
|
9133
|
+
},
|
|
8763
9134
|
{
|
|
8764
9135
|
version: "0.7.4",
|
|
8765
9136
|
date: "2026-08-31",
|
|
@@ -9900,9 +10271,10 @@ function buildServer(opts) {
|
|
|
9900
10271
|
}
|
|
9901
10272
|
}
|
|
9902
10273
|
if (inheritedPerson) inheritedDirectives.push(personSkinDirective());
|
|
9903
|
-
const
|
|
10274
|
+
const compileCtx = {
|
|
9904
10275
|
brand: brandJson,
|
|
9905
10276
|
images: core.images,
|
|
10277
|
+
wordsFor: shotWordsFor(core, brandId),
|
|
9906
10278
|
engineCaps: uncapped,
|
|
9907
10279
|
template: brief.templateId ? sceneById(String(brief.templateId)) : void 0,
|
|
9908
10280
|
templateById: sceneById,
|
|
@@ -9918,7 +10290,8 @@ function buildServer(opts) {
|
|
|
9918
10290
|
// Only the explicit op drops the dimension promise: an implicit legacy
|
|
9919
10291
|
// expansion keeps its historical prompt byte for byte.
|
|
9920
10292
|
...opts2?.reshape === "extend" ? { editReshape: "extend" } : {}
|
|
9921
|
-
}
|
|
10293
|
+
};
|
|
10294
|
+
const compiled2 = compileBrief(brief, compileCtx);
|
|
9922
10295
|
let inheritedAttachments = [];
|
|
9923
10296
|
let identityWarnings = [];
|
|
9924
10297
|
if (inheritedTokens.length) {
|
|
@@ -9938,30 +10311,19 @@ function buildServer(opts) {
|
|
|
9938
10311
|
}).map((a) => ({ ...a, inherited: true }));
|
|
9939
10312
|
}
|
|
9940
10313
|
const cap2 = Math.max(0, engineCaps.maxReferenceImages - 1);
|
|
9941
|
-
const merged = mergeEditAttachments(compiled2.
|
|
10314
|
+
const merged = mergeEditAttachments(compiled2.seated, inheritedAttachments, cap2);
|
|
10315
|
+
const prompt = compileBrief(brief, { ...compileCtx, presentAttachments: merged.kept }).prompt;
|
|
9942
10316
|
const warnings = [...compiled2.warnings, ...identityWarnings.filter((w) => !compiled2.warnings.includes(w))];
|
|
9943
10317
|
if (inherited.truncated)
|
|
9944
10318
|
warnings.push("This thread is deeper than 64 steps, so identity attached before that could not be carried.");
|
|
9945
|
-
if (merged.dropped.length)
|
|
9946
|
-
|
|
9947
|
-
warnings.push(
|
|
9948
|
-
`${engineCaps.displayName} cannot carry reference images, so the identity rides on the source frame alone.`
|
|
9949
|
-
);
|
|
9950
|
-
} else {
|
|
9951
|
-
const keptLabels = new Set(merged.kept.map((a) => a.label));
|
|
9952
|
-
const names = [...new Set(merged.dropped.map((d) => d.label))].filter((l) => !keptLabels.has(l));
|
|
9953
|
-
if (names.length)
|
|
9954
|
-
warnings.push(
|
|
9955
|
-
`${engineCaps.displayName} reads ${engineCaps.maxReferenceImages} reference images and the frame being refined keeps one, so ${names.join(
|
|
9956
|
-
" and "
|
|
9957
|
-
)} ${names.length === 1 ? "was" : "were"} left out.`
|
|
9958
|
-
);
|
|
9959
|
-
}
|
|
9960
|
-
}
|
|
10319
|
+
if (merged.dropped.length && engineCaps.maxReferenceImages <= 1)
|
|
10320
|
+
warnings.push(`The identity rides on the shot itself \u2014 ${engineCaps.displayName} reads no other images.`);
|
|
9961
10321
|
return {
|
|
9962
|
-
compiled: compiled2,
|
|
10322
|
+
compiled: { ...compiled2, prompt },
|
|
9963
10323
|
inheritedTokens,
|
|
9964
10324
|
merged,
|
|
10325
|
+
/** The seats this refinement had: the engine's, less the source frame. */
|
|
10326
|
+
cap: cap2,
|
|
9965
10327
|
warnings,
|
|
9966
10328
|
editScope: verdict.scope,
|
|
9967
10329
|
editRemoval: verdict.removal ?? false
|
|
@@ -9986,9 +10348,15 @@ function buildServer(opts) {
|
|
|
9986
10348
|
return {
|
|
9987
10349
|
...rest2,
|
|
9988
10350
|
attachments: edit.merged.kept,
|
|
9989
|
-
dropped
|
|
10351
|
+
// The own compile runs uncapped, so its dropped list holds exactly the
|
|
10352
|
+
// missing-photo identities; the budget losses live on the merge.
|
|
10353
|
+
dropped: [...edit.compiled.dropped, ...edit.merged.dropped],
|
|
9990
10354
|
warnings: edit.warnings,
|
|
9991
|
-
referenceCount: edit.merged.kept.length
|
|
10355
|
+
referenceCount: edit.merged.kept.length,
|
|
10356
|
+
// How many photo groups this refine can carry in total: the engine's
|
|
10357
|
+
// slots less the one the source frame holds. The composer refuses a
|
|
10358
|
+
// pick past it rather than warning after the fact.
|
|
10359
|
+
cap: edit.cap
|
|
9992
10360
|
};
|
|
9993
10361
|
}
|
|
9994
10362
|
const brandJson = await brandJsonWithIdentityCrops(
|
|
@@ -10012,12 +10380,13 @@ function buildServer(opts) {
|
|
|
10012
10380
|
const compiled2 = compileBrief(brief, {
|
|
10013
10381
|
brand: brandJson,
|
|
10014
10382
|
images: core.images,
|
|
10383
|
+
wordsFor: shotWordsFor(core, brand.id),
|
|
10015
10384
|
engineCaps: engine.capabilities(),
|
|
10016
10385
|
template: brief.templateId ? sceneById(String(brief.templateId)) : void 0,
|
|
10017
10386
|
templateById: sceneById
|
|
10018
10387
|
});
|
|
10019
10388
|
const { referenceImages, ...rest } = compiled2;
|
|
10020
|
-
return { ...rest, referenceCount: referenceImages.length };
|
|
10389
|
+
return { ...rest, referenceCount: referenceImages.length, cap: engine.capabilities().maxReferenceImages };
|
|
10021
10390
|
});
|
|
10022
10391
|
registerProjectRoutes(app, { core });
|
|
10023
10392
|
registerCodexSetupRoutes(app, { codexSetup: opts.codexSetup, codexRunner: engines.codexRunner });
|
|
@@ -10141,11 +10510,11 @@ function buildServer(opts) {
|
|
|
10141
10510
|
}
|
|
10142
10511
|
}
|
|
10143
10512
|
const NODE_TIMEOUT_MS = 6e5;
|
|
10144
|
-
async function runNode(
|
|
10513
|
+
async function runNode(nodeIds, engine, estimate, work, expect, postFor, timeoutMs) {
|
|
10145
10514
|
const engineId = engine?.capabilities().id ?? "local";
|
|
10146
10515
|
reserved.set(engineId, (reserved.get(engineId) ?? 0) + estimate);
|
|
10147
10516
|
const ctrl = new AbortController();
|
|
10148
|
-
runningGenerations.set(
|
|
10517
|
+
for (const id of nodeIds) runningGenerations.set(id, ctrl);
|
|
10149
10518
|
const bound = opts.nodeTimeoutMs ?? timeoutMs ?? NODE_TIMEOUT_MS;
|
|
10150
10519
|
let watchdogFired = false;
|
|
10151
10520
|
const watchdog = setTimeout(() => {
|
|
@@ -10153,43 +10522,78 @@ function buildServer(opts) {
|
|
|
10153
10522
|
ctrl.abort(BUDGET_EXHAUSTED);
|
|
10154
10523
|
}, bound);
|
|
10155
10524
|
const startedAt = Date.now();
|
|
10156
|
-
|
|
10157
|
-
|
|
10158
|
-
|
|
10159
|
-
result.images = await normalizePngs(result.images);
|
|
10160
|
-
if (post) result.images = await post(result.images);
|
|
10161
|
-
if (expect) await assertAspect(result.images, expect);
|
|
10525
|
+
const settled = /* @__PURE__ */ new Set();
|
|
10526
|
+
const settleSlot = async (slot, hash) => {
|
|
10527
|
+
const id = nodeIds[slot];
|
|
10162
10528
|
try {
|
|
10163
|
-
|
|
10164
|
-
|
|
10165
|
-
|
|
10166
|
-
|
|
10529
|
+
let own = await normalizePngs([hash]);
|
|
10530
|
+
const post = postFor?.(id);
|
|
10531
|
+
if (post) own = await post(own);
|
|
10532
|
+
if (expect) await assertAspect(own, expect);
|
|
10533
|
+
try {
|
|
10534
|
+
const meta2 = await sharp20(core.images.read(own[0])).metadata();
|
|
10535
|
+
const node = core.store.getNode(id);
|
|
10536
|
+
if (node && meta2.width && meta2.height) {
|
|
10537
|
+
const brief = node.brief ?? {};
|
|
10538
|
+
const asked = expect ? { requestedSize: [expect.width, expect.height] } : {};
|
|
10539
|
+
core.store.setBrief(id, { ...brief, rendered: { sizes: [[meta2.width, meta2.height]], ...asked } });
|
|
10540
|
+
if (node.kind === "generation" && engineId === "codex-cli" && expect && expect.width !== expect.height && meta2.width === expect.width && meta2.height === expect.height)
|
|
10541
|
+
app.log.warn(
|
|
10542
|
+
{ nodeId: id },
|
|
10543
|
+
"codex delivered exactly the requested pixels; its image tool cannot pin size - suggests a forbidden shell resize"
|
|
10544
|
+
);
|
|
10545
|
+
}
|
|
10546
|
+
} catch {
|
|
10167
10547
|
}
|
|
10168
|
-
|
|
10169
|
-
|
|
10170
|
-
|
|
10171
|
-
|
|
10172
|
-
|
|
10173
|
-
|
|
10174
|
-
|
|
10175
|
-
|
|
10176
|
-
|
|
10177
|
-
|
|
10178
|
-
|
|
10179
|
-
|
|
10548
|
+
core.store.completeNode(id, { images: own, costUsd: 0, durationMs: Date.now() - startedAt });
|
|
10549
|
+
} catch (err) {
|
|
10550
|
+
core.store.failNode(id, String(err?.message ?? err));
|
|
10551
|
+
} finally {
|
|
10552
|
+
settled.add(id);
|
|
10553
|
+
}
|
|
10554
|
+
};
|
|
10555
|
+
const landing = /* @__PURE__ */ new Map();
|
|
10556
|
+
let accepting = true;
|
|
10557
|
+
const onImage = (slot, hash) => {
|
|
10558
|
+
if (!accepting || !Number.isInteger(slot) || slot < 0 || slot >= nodeIds.length || landing.has(slot)) return;
|
|
10559
|
+
landing.set(slot, settleSlot(slot, hash));
|
|
10560
|
+
};
|
|
10561
|
+
try {
|
|
10562
|
+
const result = await work(ctrl.signal, onImage);
|
|
10563
|
+
accepting = false;
|
|
10564
|
+
clearTimeout(watchdog);
|
|
10565
|
+
await Promise.allSettled(landing.values());
|
|
10566
|
+
const raw = result.raw;
|
|
10567
|
+
const bySlot = new Array(nodeIds.length);
|
|
10568
|
+
result.images.forEach((h, k) => {
|
|
10569
|
+
const slot = raw?.variantIndexes?.[k] ?? k;
|
|
10570
|
+
if (slot < nodeIds.length && bySlot[slot] === void 0) bySlot[slot] = h;
|
|
10571
|
+
});
|
|
10572
|
+
const failures = [...raw?.partialFailures ?? []];
|
|
10573
|
+
for (let slot = 0; slot < nodeIds.length; slot++) {
|
|
10574
|
+
if (landing.has(slot)) continue;
|
|
10575
|
+
const hash = bySlot[slot];
|
|
10576
|
+
if (hash === void 0) {
|
|
10577
|
+
core.store.failNode(nodeIds[slot], failures.shift() ?? "the engine returned no image for this shot");
|
|
10578
|
+
settled.add(nodeIds[slot]);
|
|
10579
|
+
continue;
|
|
10180
10580
|
}
|
|
10181
|
-
|
|
10581
|
+
await settleSlot(slot, hash);
|
|
10182
10582
|
}
|
|
10183
|
-
core.store.
|
|
10184
|
-
core.ledger.recordCost(engineId,
|
|
10583
|
+
core.store.chargeNode(nodeIds[0], result.costUsd);
|
|
10584
|
+
core.ledger.recordCost(engineId, nodeIds[0], result.costUsd);
|
|
10185
10585
|
} catch (err) {
|
|
10186
|
-
|
|
10187
|
-
|
|
10188
|
-
|
|
10189
|
-
|
|
10586
|
+
accepting = false;
|
|
10587
|
+
await Promise.allSettled(landing.values());
|
|
10588
|
+
for (const id of nodeIds) {
|
|
10589
|
+
if (settled.has(id)) continue;
|
|
10590
|
+
if (watchdogFired) core.store.failNode(id, `generation timed out after ${Math.round(bound / 6e4)} minutes`);
|
|
10591
|
+
else if (ctrl.signal.aborted) core.store.cancelNode(id);
|
|
10592
|
+
else core.store.failNode(id, String(err?.message ?? err));
|
|
10593
|
+
}
|
|
10190
10594
|
} finally {
|
|
10191
10595
|
clearTimeout(watchdog);
|
|
10192
|
-
runningGenerations.delete(
|
|
10596
|
+
for (const id of nodeIds) runningGenerations.delete(id);
|
|
10193
10597
|
const left = (reserved.get(engineId) ?? 0) - estimate;
|
|
10194
10598
|
if (left > 1e-9) reserved.set(engineId, left);
|
|
10195
10599
|
else reserved.delete(engineId);
|
|
@@ -10238,7 +10642,7 @@ function buildServer(opts) {
|
|
|
10238
10642
|
images: [core.images.save(await sharp20(args.srcBuf).extract(window).png().toBuffer())],
|
|
10239
10643
|
costUsd: 0
|
|
10240
10644
|
});
|
|
10241
|
-
void runNode(node2.id, null, 0, work2, { width: plan2.width, height: plan2.height }).catch(
|
|
10645
|
+
void runNode([node2.id], null, 0, work2, { width: plan2.width, height: plan2.height }).catch(
|
|
10242
10646
|
(err) => app.log.error({ err }, "crop run failed")
|
|
10243
10647
|
);
|
|
10244
10648
|
return reply.status(202).send(args.note ? { ...node2, warnings: [args.note] } : node2);
|
|
@@ -10332,6 +10736,7 @@ function buildServer(opts) {
|
|
|
10332
10736
|
compiled2 = compileBrief(brief, {
|
|
10333
10737
|
brand: brandJson,
|
|
10334
10738
|
images: core.images,
|
|
10739
|
+
wordsFor: shotWordsFor(core, project.brandId),
|
|
10335
10740
|
engineCaps: engine.capabilities(),
|
|
10336
10741
|
template: brief.templateId ? sceneById(String(brief.templateId)) : void 0,
|
|
10337
10742
|
templateById: sceneById
|
|
@@ -10369,6 +10774,7 @@ function buildServer(opts) {
|
|
|
10369
10774
|
compiled2 = compileBrief(legacyBrief, {
|
|
10370
10775
|
brand: brandJson,
|
|
10371
10776
|
images: core.images,
|
|
10777
|
+
wordsFor: shotWordsFor(core, project.brandId),
|
|
10372
10778
|
engineCaps: engine.capabilities(),
|
|
10373
10779
|
templateById: sceneFor(brandJson)
|
|
10374
10780
|
});
|
|
@@ -10393,8 +10799,17 @@ function buildServer(opts) {
|
|
|
10393
10799
|
if (kind === "generation") {
|
|
10394
10800
|
const cap2 = engine.capabilities().maxReferenceImages;
|
|
10395
10801
|
const wantedCount = Math.min(Math.max(1, Number(count)), 8);
|
|
10396
|
-
const
|
|
10802
|
+
const blind = engine.capabilities().maxReferenceImages === 0;
|
|
10803
|
+
const lostIdentity = engine.capabilities().placeholder ? [] : (compiled2?.dropped ?? []).filter((d) => d.essential && (d.reason === "missing" || blind));
|
|
10397
10804
|
if (lostIdentity.length) {
|
|
10805
|
+
const missing = lostIdentity.filter((d) => d.reason === "missing");
|
|
10806
|
+
if (missing.length) {
|
|
10807
|
+
const names2 = joinNames(missing.map((d) => d.label));
|
|
10808
|
+
const kindWord2 = missing[0].role === "product" ? "product" : "presenter";
|
|
10809
|
+
return reply.code(400).send({
|
|
10810
|
+
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.`
|
|
10811
|
+
});
|
|
10812
|
+
}
|
|
10398
10813
|
const names = joinNames(lostIdentity.map((d) => d.label));
|
|
10399
10814
|
const kindWord = lostIdentity[0].role === "product" ? "product" : "presenter";
|
|
10400
10815
|
return reply.code(400).send({
|
|
@@ -10405,6 +10820,19 @@ function buildServer(opts) {
|
|
|
10405
10820
|
const keptRefs = referenceImages && cap2 > 0 ? referenceImages.slice(0, cap2) : void 0;
|
|
10406
10821
|
const sentRefs = keptRefs && maxEdge ? await Promise.all(keptRefs.map((p) => capReferenceEdge(core, p, maxEdge))) : keptRefs;
|
|
10407
10822
|
const sentRoles = referenceRoles && cap2 > 0 ? referenceRoles.slice(0, cap2) : referenceRoles ?? [];
|
|
10823
|
+
if (process.env.SCENRI_DEBUG) {
|
|
10824
|
+
const sent = {};
|
|
10825
|
+
for (const r of sentRoles) sent[r] = (sent[r] ?? 0) + 1;
|
|
10826
|
+
app.log.info(
|
|
10827
|
+
{
|
|
10828
|
+
engine: engine.capabilities().id,
|
|
10829
|
+
cap: cap2,
|
|
10830
|
+
sent,
|
|
10831
|
+
dropped: (compiled2?.dropped ?? []).map((d) => `${d.role}:${d.label} (${d.reason ?? "budget"})`)
|
|
10832
|
+
},
|
|
10833
|
+
"reference transport"
|
|
10834
|
+
);
|
|
10835
|
+
}
|
|
10408
10836
|
const briefText = Array.isArray(brief?.tokens) ? brief.tokens.filter((t) => t?.t === "text").map((t) => String(t?.v ?? "")).join(" ") : String(prompt ?? "");
|
|
10409
10837
|
const variations = variationPlan(wantedCount, {
|
|
10410
10838
|
hasPresenter: sentRoles.includes("character"),
|
|
@@ -10423,7 +10851,7 @@ function buildServer(opts) {
|
|
|
10423
10851
|
...variations.length ? { variations } : {}
|
|
10424
10852
|
};
|
|
10425
10853
|
estimate = await engine.costEstimate(genReq);
|
|
10426
|
-
work = (signal) => engine.generate(genReq, signal);
|
|
10854
|
+
work = (signal, onImage) => engine.generate(genReq, signal, onImage);
|
|
10427
10855
|
expectShape = { width, height };
|
|
10428
10856
|
} else {
|
|
10429
10857
|
const parent = core.store.getNode(resolvedParentId);
|
|
@@ -10436,6 +10864,20 @@ function buildServer(opts) {
|
|
|
10436
10864
|
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));
|
|
10437
10865
|
const editEdge = engine.capabilities().maxReferenceEdge;
|
|
10438
10866
|
if (editEdge) for (const r of editRefs) r.path = await capReferenceEdge(core, r.path, editEdge);
|
|
10867
|
+
if (process.env.SCENRI_DEBUG) {
|
|
10868
|
+
const sent = {};
|
|
10869
|
+
for (const r of editRefs) sent[String(r.role ?? "reference")] = (sent[String(r.role ?? "reference")] ?? 0) + 1;
|
|
10870
|
+
app.log.info(
|
|
10871
|
+
{
|
|
10872
|
+
engine: engine.capabilities().id,
|
|
10873
|
+
cap: Math.max(0, engine.capabilities().maxReferenceImages - 1),
|
|
10874
|
+
sourceFrame: true,
|
|
10875
|
+
sent,
|
|
10876
|
+
dropped: (mergedEdit?.dropped ?? []).map((d) => `${d.role}:${d.label} (${d.reason ?? "budget"})`)
|
|
10877
|
+
},
|
|
10878
|
+
"reference transport"
|
|
10879
|
+
);
|
|
10880
|
+
}
|
|
10439
10881
|
const srcBuf = core.images.read(String(srcHash));
|
|
10440
10882
|
const srcMeta = await sharp20(srcBuf).metadata();
|
|
10441
10883
|
if (srcMeta.width && srcMeta.height) expectShape = { width: srcMeta.width, height: srcMeta.height };
|
|
@@ -10511,8 +10953,9 @@ function buildServer(opts) {
|
|
|
10511
10953
|
expectShape = { width: expandPlan.width, height: expandPlan.height };
|
|
10512
10954
|
}
|
|
10513
10955
|
const editPixelBudget = runEngine.capabilities().editPixelBudget;
|
|
10514
|
-
|
|
10515
|
-
|
|
10956
|
+
const stepped = !expandPlan && editPixelBudget && srcMeta.width && srcMeta.height && srcMeta.width * srcMeta.height > editPixelBudget ? budgetSize(srcMeta.width, srcMeta.height, editPixelBudget) : null;
|
|
10957
|
+
if (editPixelBudget && stepped && (stepped.width !== srcMeta.width || stepped.height !== srcMeta.height)) {
|
|
10958
|
+
sentSize = stepped;
|
|
10516
10959
|
budgetSourceHash = core.images.save(
|
|
10517
10960
|
await sharp20(srcBuf).resize(sentSize.width, sentSize.height, { fit: "fill", kernel: "lanczos3" }).png().toBuffer()
|
|
10518
10961
|
);
|
|
@@ -10603,15 +11046,26 @@ function buildServer(opts) {
|
|
|
10603
11046
|
}
|
|
10604
11047
|
const billedId = runEngine.capabilities().id;
|
|
10605
11048
|
core.ledger.assertUnderCap(billedId, estimate + (reserved.get(billedId) ?? 0));
|
|
10606
|
-
const
|
|
11049
|
+
const nodes = kind === "generation" ? core.store.addNodes({
|
|
10607
11050
|
projectId: project.id,
|
|
10608
11051
|
parentId: resolvedParentId,
|
|
10609
11052
|
kind,
|
|
10610
11053
|
prompt: finalPrompt,
|
|
10611
|
-
engineId: billedId
|
|
10612
|
-
|
|
10613
|
-
|
|
10614
|
-
|
|
11054
|
+
engineId: billedId,
|
|
11055
|
+
// the same clamp the engine request and the watchdog use
|
|
11056
|
+
count: Math.min(Math.max(1, Number(count)), 8)
|
|
11057
|
+
}) : [
|
|
11058
|
+
core.store.addNode({
|
|
11059
|
+
projectId: project.id,
|
|
11060
|
+
parentId: resolvedParentId,
|
|
11061
|
+
kind,
|
|
11062
|
+
prompt: finalPrompt,
|
|
11063
|
+
engineId: billedId
|
|
11064
|
+
})
|
|
11065
|
+
];
|
|
11066
|
+
const node = nodes[0];
|
|
11067
|
+
for (const sibling of brief ? nodes : [])
|
|
11068
|
+
core.store.setBrief(sibling.id, {
|
|
10615
11069
|
...briefInputsOnly(brief),
|
|
10616
11070
|
...editedFrom ? { sourceImage: editedFrom } : {},
|
|
10617
11071
|
...kind === "edit" && reshape ? { reshape } : {},
|
|
@@ -10636,7 +11090,7 @@ function buildServer(opts) {
|
|
|
10636
11090
|
}
|
|
10637
11091
|
} : {},
|
|
10638
11092
|
// What the refinement carried, recorded apart from what it asked for:
|
|
10639
|
-
// the detail view shows both, and
|
|
11093
|
+
// the detail view shows both, and reuse setup merges both (mergeCarried).
|
|
10640
11094
|
...kind === "edit" && inheritedTokens.length ? { inherited: inheritedTokens } : {}
|
|
10641
11095
|
});
|
|
10642
11096
|
const plan = expandPlan;
|
|
@@ -10758,14 +11212,23 @@ function buildServer(opts) {
|
|
|
10758
11212
|
staged = await conformToCanvas(node.id, expectShape)(staged);
|
|
10759
11213
|
}
|
|
10760
11214
|
return enforceEditCanvas(staged);
|
|
10761
|
-
} :
|
|
11215
|
+
} : void 0;
|
|
11216
|
+
const genW = compiled2?.width;
|
|
11217
|
+
const genH = compiled2?.height;
|
|
11218
|
+
const postFor = post !== void 0 ? () => post : kind === "generation" && genW && genH ? (id) => conformToCanvas(id, { width: genW, height: genH }) : void 0;
|
|
10762
11219
|
const runCaps = runEngine.capabilities();
|
|
10763
11220
|
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;
|
|
10764
|
-
void runNode(
|
|
10765
|
-
(
|
|
10766
|
-
|
|
11221
|
+
void runNode(
|
|
11222
|
+
nodes.map((n) => n.id),
|
|
11223
|
+
runEngine,
|
|
11224
|
+
estimate,
|
|
11225
|
+
work,
|
|
11226
|
+
expectShape,
|
|
11227
|
+
postFor,
|
|
11228
|
+
nodeBudgetMs
|
|
11229
|
+
).catch((err) => app.log.error({ err }, "node run failed"));
|
|
10767
11230
|
const allWarnings = [...compiled2?.warnings ?? [], ...extraWarnings];
|
|
10768
|
-
return reply.status(202).send(allWarnings.length ? {
|
|
11231
|
+
return reply.status(202).send({ ...node, siblings: nodes, ...allWarnings.length ? { warnings: allWarnings } : {} });
|
|
10769
11232
|
});
|
|
10770
11233
|
app.post("/api/nodes/:id/cancel", async (req, reply) => {
|
|
10771
11234
|
const id = req.params.id;
|
|
@@ -10838,7 +11301,10 @@ function buildServer(opts) {
|
|
|
10838
11301
|
runtime,
|
|
10839
11302
|
stageImpl: opts.stageImpl,
|
|
10840
11303
|
exitImpl: opts.exitImpl,
|
|
10841
|
-
|
|
11304
|
+
// one physical run counts once, however many sibling nodes share its
|
|
11305
|
+
// controller — an update gate held open by a 4-shot batch is still held
|
|
11306
|
+
// open by exactly one piece of work
|
|
11307
|
+
busyCount: () => new Set(runningGenerations.values()).size + runningImportCount() + runningAssetBuildCount()
|
|
10842
11308
|
});
|
|
10843
11309
|
let drained = null;
|
|
10844
11310
|
app.decorate("drain", () => {
|
|
@@ -10907,7 +11373,7 @@ async function serve() {
|
|
|
10907
11373
|
}
|
|
10908
11374
|
async function run() {
|
|
10909
11375
|
const core = createCore();
|
|
10910
|
-
const stubs = process.env.SCENRI_DEMO_ENGINE === "1" ? [createDemoEngine((b) => core.images.save(b))] : [];
|
|
11376
|
+
const stubs = process.env.SCENRI_DEMO_ENGINE === "1" ? [createDemoEngine((b) => core.images.save(b), demoOptionsFromEnv(process.env))] : [];
|
|
10911
11377
|
const engines = createEngineRegistry(core, stubs);
|
|
10912
11378
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
10913
11379
|
const candidates = [join(here, "..", "..", "..", "apps", "studio", "dist"), join(here, "..", "studio-dist")];
|