scenri 0.7.5 → 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 +81 -0
- package/dist/serve.js +246 -86
- package/package.json +2 -2
- package/studio-dist/assets/{index-C4vkwhbc.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-Bpw6p5Pj.js +0 -103
package/dist/serve.js
CHANGED
|
@@ -806,6 +806,16 @@ function createStore(db) {
|
|
|
806
806
|
id
|
|
807
807
|
);
|
|
808
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
|
+
},
|
|
809
819
|
failNode(id, error) {
|
|
810
820
|
db.prepare("UPDATE nodes SET status='error', error=? WHERE id=?").run(error, id);
|
|
811
821
|
},
|
|
@@ -1630,7 +1640,7 @@ function createOpenRouterEngine(opts) {
|
|
|
1630
1640
|
const count = "count" in req ? req.count : 1;
|
|
1631
1641
|
return count * perImageUsd;
|
|
1632
1642
|
},
|
|
1633
|
-
async generate(req, signal) {
|
|
1643
|
+
async generate(req, signal, onImage) {
|
|
1634
1644
|
const key = requireKey();
|
|
1635
1645
|
const roles = req.referenceRoles ?? [];
|
|
1636
1646
|
const refs = req.referenceImages ?? [];
|
|
@@ -1666,7 +1676,9 @@ function createOpenRouterEngine(opts) {
|
|
|
1666
1676
|
signal
|
|
1667
1677
|
);
|
|
1668
1678
|
raws.push(json);
|
|
1669
|
-
|
|
1679
|
+
const own = extractImages(json).map((buf) => opts.saveImage(buf));
|
|
1680
|
+
hashes.push(...own);
|
|
1681
|
+
if (own[0]) onImage?.(i, own[0]);
|
|
1670
1682
|
if (typeof json?.usage?.cost === "number") {
|
|
1671
1683
|
reportedCost += json.usage.cost;
|
|
1672
1684
|
sawReportedCost = true;
|
|
@@ -2863,7 +2875,7 @@ function createCodexEngine(opts) {
|
|
|
2863
2875
|
async costEstimate() {
|
|
2864
2876
|
return 0;
|
|
2865
2877
|
},
|
|
2866
|
-
async generate(req, signal) {
|
|
2878
|
+
async generate(req, signal, onImage) {
|
|
2867
2879
|
const count = Math.max(1, req.count);
|
|
2868
2880
|
const refs = req.referenceImages ?? [];
|
|
2869
2881
|
const roles = req.referenceRoles ?? refs.map(() => "reference");
|
|
@@ -2902,6 +2914,7 @@ function createCodexEngine(opts) {
|
|
|
2902
2914
|
const i = next++;
|
|
2903
2915
|
try {
|
|
2904
2916
|
results[i] = await jobs[i]();
|
|
2917
|
+
if (results[i][0]) onImage?.(i, results[i][0]);
|
|
2905
2918
|
} catch (err) {
|
|
2906
2919
|
if (signal?.aborted && signal.reason !== BUDGET_EXHAUSTED) throw err;
|
|
2907
2920
|
results[i] = [];
|
|
@@ -3041,7 +3054,30 @@ function createEngineRegistry(core, extra = []) {
|
|
|
3041
3054
|
codexRunner
|
|
3042
3055
|
};
|
|
3043
3056
|
}
|
|
3044
|
-
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 = {}) {
|
|
3045
3081
|
const paletteOf = (req) => {
|
|
3046
3082
|
const p = req.brand?.brand?.palette;
|
|
3047
3083
|
const hexes = [p?.primary?.hex, p?.secondary?.hex, ...(p?.accent ?? []).map((a) => a?.hex)].filter(
|
|
@@ -3087,13 +3123,31 @@ function createDemoEngine(saveImage) {
|
|
|
3087
3123
|
async costEstimate() {
|
|
3088
3124
|
return 0;
|
|
3089
3125
|
},
|
|
3090
|
-
async generate(req) {
|
|
3126
|
+
async generate(req, signal, onImage) {
|
|
3091
3127
|
const colors = paletteOf(req);
|
|
3092
|
-
const
|
|
3093
|
-
|
|
3094
|
-
|
|
3095
|
-
|
|
3096
|
-
|
|
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 } };
|
|
3097
3151
|
},
|
|
3098
3152
|
async edit(req) {
|
|
3099
3153
|
const colors = paletteOf(req);
|
|
@@ -3398,6 +3452,15 @@ function defaultDemoProductsDir() {
|
|
|
3398
3452
|
}
|
|
3399
3453
|
|
|
3400
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
|
+
};
|
|
3401
3464
|
var ROLE_PRIORITY = {
|
|
3402
3465
|
product: 0,
|
|
3403
3466
|
character: 1,
|
|
@@ -3420,11 +3483,8 @@ function allocateAttachments(attachments, cap2) {
|
|
|
3420
3483
|
kept.add(x.i);
|
|
3421
3484
|
keptGroups.add(groupOf(x.a));
|
|
3422
3485
|
};
|
|
3423
|
-
|
|
3424
|
-
|
|
3425
|
-
if (x.a.essential) admit(x);
|
|
3426
|
-
}
|
|
3427
|
-
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) {
|
|
3428
3488
|
if (kept.size >= max) break;
|
|
3429
3489
|
if (!kept.has(x.i) && !keptGroups.has(groupOf(x.a))) admit(x);
|
|
3430
3490
|
}
|
|
@@ -3449,7 +3509,13 @@ function allocateAttachments(attachments, cap2) {
|
|
|
3449
3509
|
}
|
|
3450
3510
|
return {
|
|
3451
3511
|
kept: legacyOrder.filter((x) => kept.has(x.i)).map((x) => x.a),
|
|
3452
|
-
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)
|
|
3453
3519
|
};
|
|
3454
3520
|
}
|
|
3455
3521
|
function mergeEditAttachments(own, inherited, cap2) {
|
|
@@ -3614,12 +3680,19 @@ function shotSpecifiesCamera(text) {
|
|
|
3614
3680
|
text
|
|
3615
3681
|
);
|
|
3616
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
|
+
}
|
|
3617
3686
|
function sceneFigureDirectives(opts) {
|
|
3618
3687
|
const figure = opts.figure.trim().replace(/[.\s]+$/, "");
|
|
3619
3688
|
if (!figure) return [];
|
|
3620
3689
|
const treatment = (opts.treatment ?? "").trim().replace(/[.\s]+$/, "");
|
|
3621
3690
|
const out = [];
|
|
3622
|
-
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) {
|
|
3623
3696
|
out.push(
|
|
3624
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.`
|
|
3625
3698
|
);
|
|
@@ -3740,6 +3813,8 @@ function validateBrief(brief) {
|
|
|
3740
3813
|
case "ref":
|
|
3741
3814
|
case "mark":
|
|
3742
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`);
|
|
3743
3818
|
break;
|
|
3744
3819
|
case "format":
|
|
3745
3820
|
if (!Number.isFinite(t.w) || !Number.isFinite(t.h) || Number(t.w) <= 0 || Number(t.h) <= 0)
|
|
@@ -3766,6 +3841,7 @@ function compileBrief(brief, ctx) {
|
|
|
3766
3841
|
const characters = ctx.brand?.characters ?? [];
|
|
3767
3842
|
const inlineTemplates = [];
|
|
3768
3843
|
let hasPerson = false;
|
|
3844
|
+
let people = 0;
|
|
3769
3845
|
let sentence = "";
|
|
3770
3846
|
const append = (s) => {
|
|
3771
3847
|
sentence += (sentence && !sentence.endsWith(" ") ? " " : "") + s;
|
|
@@ -3825,6 +3901,7 @@ function compileBrief(brief, ctx) {
|
|
|
3825
3901
|
break;
|
|
3826
3902
|
}
|
|
3827
3903
|
hasPerson = true;
|
|
3904
|
+
people += 1;
|
|
3828
3905
|
append(c.promptName ?? c.name);
|
|
3829
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));
|
|
3830
3907
|
if (cshots.length) {
|
|
@@ -3869,8 +3946,10 @@ function compileBrief(brief, ctx) {
|
|
|
3869
3946
|
}
|
|
3870
3947
|
case "color": {
|
|
3871
3948
|
const hex = tok.hex.toUpperCase();
|
|
3872
|
-
append(tok.name ?
|
|
3873
|
-
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
|
+
);
|
|
3874
3953
|
break;
|
|
3875
3954
|
}
|
|
3876
3955
|
case "ref": {
|
|
@@ -4007,7 +4086,7 @@ function compileBrief(brief, ctx) {
|
|
|
4007
4086
|
}
|
|
4008
4087
|
}
|
|
4009
4088
|
const max = ctx.engineCaps.maxReferenceImages;
|
|
4010
|
-
const { kept, dropped: budgetDropped } = allocateAttachments(attachments, max);
|
|
4089
|
+
const { kept, dropped: budgetDropped, seated } = allocateAttachments(attachments, max);
|
|
4011
4090
|
const presentKeys = new Set((ctx.presentAttachments ?? kept).map((a) => `${a.role}:${a.hash}`));
|
|
4012
4091
|
const resolveDirective = (d) => {
|
|
4013
4092
|
if (typeof d === "string") return d;
|
|
@@ -4019,6 +4098,24 @@ function compileBrief(brief, ctx) {
|
|
|
4019
4098
|
}
|
|
4020
4099
|
return presentKeys.has(`${d.role}:${d.hash}`) ? d.text : null;
|
|
4021
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
|
+
}
|
|
4022
4119
|
const guard = scene ? sceneGuardDirectives({
|
|
4023
4120
|
hasProduct: !!productId,
|
|
4024
4121
|
hasPerson,
|
|
@@ -4035,10 +4132,12 @@ function compileBrief(brief, ctx) {
|
|
|
4035
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.",
|
|
4036
4133
|
productHandlingDirective()
|
|
4037
4134
|
] : [];
|
|
4135
|
+
const nameDirectives = productId || hasPerson ? [namesAreNotLetteringDirective()] : [];
|
|
4038
4136
|
const figureDirectives = scene?.figure ? sceneFigureDirectives({
|
|
4039
4137
|
figure: scene.figure,
|
|
4040
4138
|
treatment: scene.figureTreatment,
|
|
4041
4139
|
hasPerson,
|
|
4140
|
+
people,
|
|
4042
4141
|
// The treatment's fictional-brands rule needs to know a real mark is
|
|
4043
4142
|
// deliberately in play - and only one that actually rides counts,
|
|
4044
4143
|
// same honesty rule as the photo guard above.
|
|
@@ -4067,12 +4166,16 @@ function compileBrief(brief, ctx) {
|
|
|
4067
4166
|
}) ? [garmentDisplayDirective()] : [];
|
|
4068
4167
|
const refGuard = ctx.mode !== "edit" && hasPerson && kept.some((a) => a.role === "reference") ? [referenceIdentityGuard()] : [];
|
|
4069
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,
|
|
4070
4172
|
...productDirectives,
|
|
4071
4173
|
...personDirectives,
|
|
4072
4174
|
...pairDirectives,
|
|
4073
4175
|
...figureDirectives,
|
|
4074
4176
|
...closeUpDirectives,
|
|
4075
4177
|
...otherDirectives,
|
|
4178
|
+
...absentDirectives,
|
|
4076
4179
|
...cameraDirectives,
|
|
4077
4180
|
...apparelUnworn,
|
|
4078
4181
|
...brandLines,
|
|
@@ -4082,15 +4185,14 @@ function compileBrief(brief, ctx) {
|
|
|
4082
4185
|
];
|
|
4083
4186
|
const spoken = dedupe(allDirectives.map(resolveDirective).filter((s) => s !== null));
|
|
4084
4187
|
if (spoken.length) prompt = `${prompt}${prompt.endsWith(".") ? "" : "."} ${spoken.join(" ")}`;
|
|
4085
|
-
if (budgetDropped.some((d) => d.role !== "scene")) {
|
|
4188
|
+
if (max === 0 && budgetDropped.some((d) => d.role !== "scene")) {
|
|
4086
4189
|
const keptLabels = new Set(kept.map((a) => a.label));
|
|
4087
4190
|
const names = [...new Set(budgetDropped.filter((d) => d.role !== "scene").map((d) => d.label))].filter(
|
|
4088
4191
|
(l) => !keptLabels.has(l)
|
|
4089
4192
|
);
|
|
4090
4193
|
if (names.length) {
|
|
4091
|
-
const reads = max === 0 ? "reads no reference images" : `reads ${max} reference image${max === 1 ? "" : "s"}`;
|
|
4092
4194
|
warnings.push(
|
|
4093
|
-
`${names.join(" and ")} ${names.length === 1 ? "was" : "were"} left out \u2014 ${ctx.engineCaps.displayName}
|
|
4195
|
+
`${names.join(" and ")} ${names.length === 1 ? "was" : "were"} left out \u2014 ${ctx.engineCaps.displayName} reads no reference images.`
|
|
4094
4196
|
);
|
|
4095
4197
|
}
|
|
4096
4198
|
}
|
|
@@ -4103,11 +4205,37 @@ function compileBrief(brief, ctx) {
|
|
|
4103
4205
|
width,
|
|
4104
4206
|
height,
|
|
4105
4207
|
attachments: kept,
|
|
4208
|
+
seated,
|
|
4106
4209
|
warnings,
|
|
4107
4210
|
productId
|
|
4108
4211
|
};
|
|
4109
4212
|
}
|
|
4110
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
|
+
}
|
|
4111
4239
|
var DEFAULT_CONTENT_URL = "https://github.com/tonygorb/scenri/releases/download/content-latest/scenri-content.zip";
|
|
4112
4240
|
var TIMEOUT_MS = 10 * 60 * 1e3;
|
|
4113
4241
|
function resolveContentUrl(env = process.env, override) {
|
|
@@ -5034,7 +5162,7 @@ function dedupeProducts(products) {
|
|
|
5034
5162
|
|
|
5035
5163
|
// ../catalog/src/http/fetch.ts
|
|
5036
5164
|
var USER_AGENT = "scenri-catalog/0.1 (+https://scenri.co)";
|
|
5037
|
-
function
|
|
5165
|
+
function sleep3(ms) {
|
|
5038
5166
|
return new Promise((r) => setTimeout(r, ms));
|
|
5039
5167
|
}
|
|
5040
5168
|
async function httpGet(url, opts = {}) {
|
|
@@ -5058,7 +5186,7 @@ async function httpGet(url, opts = {}) {
|
|
|
5058
5186
|
}
|
|
5059
5187
|
});
|
|
5060
5188
|
if ((res.status === 429 || res.status >= 500) && attempt < retries) {
|
|
5061
|
-
await
|
|
5189
|
+
await sleep3(400 * 2 ** attempt);
|
|
5062
5190
|
continue;
|
|
5063
5191
|
}
|
|
5064
5192
|
return res;
|
|
@@ -5066,7 +5194,7 @@ async function httpGet(url, opts = {}) {
|
|
|
5066
5194
|
lastErr = err;
|
|
5067
5195
|
if (opts.signal?.aborted) throw err;
|
|
5068
5196
|
if (attempt < retries) {
|
|
5069
|
-
await
|
|
5197
|
+
await sleep3(400 * 2 ** attempt);
|
|
5070
5198
|
continue;
|
|
5071
5199
|
}
|
|
5072
5200
|
throw err;
|
|
@@ -8036,11 +8164,6 @@ function registerLogoRoutes(app, deps) {
|
|
|
8036
8164
|
if (!v.valid) return reply.status(400).send({ error: "brand became invalid", details: v.errors });
|
|
8037
8165
|
return core.store.updateBrand(brand.id, json);
|
|
8038
8166
|
});
|
|
8039
|
-
app.get("/api/brands/:id/directives", async (req, reply) => {
|
|
8040
|
-
const brand = core.store.getBrand(req.params.id);
|
|
8041
|
-
if (!brand) return reply.status(404).send({ error: "brand not found" });
|
|
8042
|
-
return { directives: brandRuleDirectives(brand.json) };
|
|
8043
|
-
});
|
|
8044
8167
|
app.delete("/api/brands/:id/logos/:hash", async (req, reply) => {
|
|
8045
8168
|
const brand = core.store.getBrand(req.params.id);
|
|
8046
8169
|
if (!brand) return reply.status(404).send({ error: "brand not found" });
|
|
@@ -8966,6 +9089,29 @@ function registerImageRoutes(app, deps) {
|
|
|
8966
9089
|
|
|
8967
9090
|
// src/release/notes.data.ts
|
|
8968
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
|
+
},
|
|
8969
9115
|
{
|
|
8970
9116
|
version: "0.7.5",
|
|
8971
9117
|
date: "2026-09-01",
|
|
@@ -10128,6 +10274,7 @@ function buildServer(opts) {
|
|
|
10128
10274
|
const compileCtx = {
|
|
10129
10275
|
brand: brandJson,
|
|
10130
10276
|
images: core.images,
|
|
10277
|
+
wordsFor: shotWordsFor(core, brandId),
|
|
10131
10278
|
engineCaps: uncapped,
|
|
10132
10279
|
template: brief.templateId ? sceneById(String(brief.templateId)) : void 0,
|
|
10133
10280
|
templateById: sceneById,
|
|
@@ -10164,25 +10311,19 @@ function buildServer(opts) {
|
|
|
10164
10311
|
}).map((a) => ({ ...a, inherited: true }));
|
|
10165
10312
|
}
|
|
10166
10313
|
const cap2 = Math.max(0, engineCaps.maxReferenceImages - 1);
|
|
10167
|
-
const merged = mergeEditAttachments(compiled2.
|
|
10314
|
+
const merged = mergeEditAttachments(compiled2.seated, inheritedAttachments, cap2);
|
|
10168
10315
|
const prompt = compileBrief(brief, { ...compileCtx, presentAttachments: merged.kept }).prompt;
|
|
10169
10316
|
const warnings = [...compiled2.warnings, ...identityWarnings.filter((w) => !compiled2.warnings.includes(w))];
|
|
10170
10317
|
if (inherited.truncated)
|
|
10171
10318
|
warnings.push("This thread is deeper than 64 steps, so identity attached before that could not be carried.");
|
|
10172
|
-
if (merged.dropped.length)
|
|
10173
|
-
|
|
10174
|
-
warnings.push(`The identity rides on the shot itself \u2014 ${engineCaps.displayName} reads no other images.`);
|
|
10175
|
-
} else {
|
|
10176
|
-
const keptLabels = new Set(merged.kept.map((a) => a.label));
|
|
10177
|
-
const names = [...new Set(merged.dropped.map((d) => d.label))].filter((l) => !keptLabels.has(l));
|
|
10178
|
-
if (names.length)
|
|
10179
|
-
warnings.push(`${names.join(" and ")} ${names.length === 1 ? "was" : "were"} left out of this refinement.`);
|
|
10180
|
-
}
|
|
10181
|
-
}
|
|
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.`);
|
|
10182
10321
|
return {
|
|
10183
10322
|
compiled: { ...compiled2, prompt },
|
|
10184
10323
|
inheritedTokens,
|
|
10185
10324
|
merged,
|
|
10325
|
+
/** The seats this refinement had: the engine's, less the source frame. */
|
|
10326
|
+
cap: cap2,
|
|
10186
10327
|
warnings,
|
|
10187
10328
|
editScope: verdict.scope,
|
|
10188
10329
|
editRemoval: verdict.removal ?? false
|
|
@@ -10211,7 +10352,11 @@ function buildServer(opts) {
|
|
|
10211
10352
|
// missing-photo identities; the budget losses live on the merge.
|
|
10212
10353
|
dropped: [...edit.compiled.dropped, ...edit.merged.dropped],
|
|
10213
10354
|
warnings: edit.warnings,
|
|
10214
|
-
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
|
|
10215
10360
|
};
|
|
10216
10361
|
}
|
|
10217
10362
|
const brandJson = await brandJsonWithIdentityCrops(
|
|
@@ -10235,12 +10380,13 @@ function buildServer(opts) {
|
|
|
10235
10380
|
const compiled2 = compileBrief(brief, {
|
|
10236
10381
|
brand: brandJson,
|
|
10237
10382
|
images: core.images,
|
|
10383
|
+
wordsFor: shotWordsFor(core, brand.id),
|
|
10238
10384
|
engineCaps: engine.capabilities(),
|
|
10239
10385
|
template: brief.templateId ? sceneById(String(brief.templateId)) : void 0,
|
|
10240
10386
|
templateById: sceneById
|
|
10241
10387
|
});
|
|
10242
10388
|
const { referenceImages, ...rest } = compiled2;
|
|
10243
|
-
return { ...rest, referenceCount: referenceImages.length };
|
|
10389
|
+
return { ...rest, referenceCount: referenceImages.length, cap: engine.capabilities().maxReferenceImages };
|
|
10244
10390
|
});
|
|
10245
10391
|
registerProjectRoutes(app, { core });
|
|
10246
10392
|
registerCodexSetupRoutes(app, { codexSetup: opts.codexSetup, codexRunner: engines.codexRunner });
|
|
@@ -10377,58 +10523,68 @@ function buildServer(opts) {
|
|
|
10377
10523
|
}, bound);
|
|
10378
10524
|
const startedAt = Date.now();
|
|
10379
10525
|
const settled = /* @__PURE__ */ new Set();
|
|
10526
|
+
const settleSlot = async (slot, hash) => {
|
|
10527
|
+
const id = nodeIds[slot];
|
|
10528
|
+
try {
|
|
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 {
|
|
10547
|
+
}
|
|
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
|
+
};
|
|
10380
10561
|
try {
|
|
10381
|
-
const result = await work(ctrl.signal);
|
|
10562
|
+
const result = await work(ctrl.signal, onImage);
|
|
10563
|
+
accepting = false;
|
|
10382
10564
|
clearTimeout(watchdog);
|
|
10383
|
-
|
|
10565
|
+
await Promise.allSettled(landing.values());
|
|
10384
10566
|
const raw = result.raw;
|
|
10385
10567
|
const bySlot = new Array(nodeIds.length);
|
|
10386
|
-
images.forEach((h, k) => {
|
|
10568
|
+
result.images.forEach((h, k) => {
|
|
10387
10569
|
const slot = raw?.variantIndexes?.[k] ?? k;
|
|
10388
10570
|
if (slot < nodeIds.length && bySlot[slot] === void 0) bySlot[slot] = h;
|
|
10389
10571
|
});
|
|
10390
|
-
const wall = Date.now() - startedAt;
|
|
10391
10572
|
const failures = [...raw?.partialFailures ?? []];
|
|
10392
10573
|
for (let slot = 0; slot < nodeIds.length; slot++) {
|
|
10393
|
-
|
|
10574
|
+
if (landing.has(slot)) continue;
|
|
10394
10575
|
const hash = bySlot[slot];
|
|
10395
10576
|
if (hash === void 0) {
|
|
10396
|
-
core.store.failNode(
|
|
10397
|
-
settled.add(
|
|
10577
|
+
core.store.failNode(nodeIds[slot], failures.shift() ?? "the engine returned no image for this shot");
|
|
10578
|
+
settled.add(nodeIds[slot]);
|
|
10398
10579
|
continue;
|
|
10399
10580
|
}
|
|
10400
|
-
|
|
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));
|
|
10427
|
-
}
|
|
10428
|
-
settled.add(id);
|
|
10581
|
+
await settleSlot(slot, hash);
|
|
10429
10582
|
}
|
|
10583
|
+
core.store.chargeNode(nodeIds[0], result.costUsd);
|
|
10430
10584
|
core.ledger.recordCost(engineId, nodeIds[0], result.costUsd);
|
|
10431
10585
|
} catch (err) {
|
|
10586
|
+
accepting = false;
|
|
10587
|
+
await Promise.allSettled(landing.values());
|
|
10432
10588
|
for (const id of nodeIds) {
|
|
10433
10589
|
if (settled.has(id)) continue;
|
|
10434
10590
|
if (watchdogFired) core.store.failNode(id, `generation timed out after ${Math.round(bound / 6e4)} minutes`);
|
|
@@ -10580,6 +10736,7 @@ function buildServer(opts) {
|
|
|
10580
10736
|
compiled2 = compileBrief(brief, {
|
|
10581
10737
|
brand: brandJson,
|
|
10582
10738
|
images: core.images,
|
|
10739
|
+
wordsFor: shotWordsFor(core, project.brandId),
|
|
10583
10740
|
engineCaps: engine.capabilities(),
|
|
10584
10741
|
template: brief.templateId ? sceneById(String(brief.templateId)) : void 0,
|
|
10585
10742
|
templateById: sceneById
|
|
@@ -10617,6 +10774,7 @@ function buildServer(opts) {
|
|
|
10617
10774
|
compiled2 = compileBrief(legacyBrief, {
|
|
10618
10775
|
brand: brandJson,
|
|
10619
10776
|
images: core.images,
|
|
10777
|
+
wordsFor: shotWordsFor(core, project.brandId),
|
|
10620
10778
|
engineCaps: engine.capabilities(),
|
|
10621
10779
|
templateById: sceneFor(brandJson)
|
|
10622
10780
|
});
|
|
@@ -10641,7 +10799,8 @@ function buildServer(opts) {
|
|
|
10641
10799
|
if (kind === "generation") {
|
|
10642
10800
|
const cap2 = engine.capabilities().maxReferenceImages;
|
|
10643
10801
|
const wantedCount = Math.min(Math.max(1, Number(count)), 8);
|
|
10644
|
-
const
|
|
10802
|
+
const blind = engine.capabilities().maxReferenceImages === 0;
|
|
10803
|
+
const lostIdentity = engine.capabilities().placeholder ? [] : (compiled2?.dropped ?? []).filter((d) => d.essential && (d.reason === "missing" || blind));
|
|
10645
10804
|
if (lostIdentity.length) {
|
|
10646
10805
|
const missing = lostIdentity.filter((d) => d.reason === "missing");
|
|
10647
10806
|
if (missing.length) {
|
|
@@ -10692,7 +10851,7 @@ function buildServer(opts) {
|
|
|
10692
10851
|
...variations.length ? { variations } : {}
|
|
10693
10852
|
};
|
|
10694
10853
|
estimate = await engine.costEstimate(genReq);
|
|
10695
|
-
work = (signal) => engine.generate(genReq, signal);
|
|
10854
|
+
work = (signal, onImage) => engine.generate(genReq, signal, onImage);
|
|
10696
10855
|
expectShape = { width, height };
|
|
10697
10856
|
} else {
|
|
10698
10857
|
const parent = core.store.getNode(resolvedParentId);
|
|
@@ -10794,8 +10953,9 @@ function buildServer(opts) {
|
|
|
10794
10953
|
expectShape = { width: expandPlan.width, height: expandPlan.height };
|
|
10795
10954
|
}
|
|
10796
10955
|
const editPixelBudget = runEngine.capabilities().editPixelBudget;
|
|
10797
|
-
|
|
10798
|
-
|
|
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;
|
|
10799
10959
|
budgetSourceHash = core.images.save(
|
|
10800
10960
|
await sharp20(srcBuf).resize(sentSize.width, sentSize.height, { fit: "fill", kernel: "lanczos3" }).png().toBuffer()
|
|
10801
10961
|
);
|
|
@@ -10930,7 +11090,7 @@ function buildServer(opts) {
|
|
|
10930
11090
|
}
|
|
10931
11091
|
} : {},
|
|
10932
11092
|
// What the refinement carried, recorded apart from what it asked for:
|
|
10933
|
-
// the detail view shows both, and
|
|
11093
|
+
// the detail view shows both, and reuse setup merges both (mergeCarried).
|
|
10934
11094
|
...kind === "edit" && inheritedTokens.length ? { inherited: inheritedTokens } : {}
|
|
10935
11095
|
});
|
|
10936
11096
|
const plan = expandPlan;
|
|
@@ -11213,7 +11373,7 @@ async function serve() {
|
|
|
11213
11373
|
}
|
|
11214
11374
|
async function run() {
|
|
11215
11375
|
const core = createCore();
|
|
11216
|
-
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))] : [];
|
|
11217
11377
|
const engines = createEngineRegistry(core, stubs);
|
|
11218
11378
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
11219
11379
|
const candidates = [join(here, "..", "..", "..", "apps", "studio", "dist"), join(here, "..", "studio-dist")];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "scenri",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"author": "Tony Gorb <hello@scenri.co>",
|
|
5
5
|
"license": "AGPL-3.0-only",
|
|
6
6
|
"type": "module",
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"open": "^10.1.0",
|
|
29
29
|
"pixelmatch": "^7.1.0",
|
|
30
30
|
"pngjs": "^7.0.0",
|
|
31
|
-
"sharp": "^0.35.
|
|
31
|
+
"sharp": "^0.35.4"
|
|
32
32
|
},
|
|
33
33
|
"devDependencies": {
|
|
34
34
|
"@scenri/brand": "workspace:*",
|