@gtanczyk/diorama 0.2.0 → 0.3.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/README.md +1 -0
- package/app/src/actions/fix.ts +28 -10
- package/app/src/commands/backdrop.ts +23 -3
- package/app/src/commands/export.ts +1 -0
- package/app/src/commands/play.ts +1 -0
- package/app/src/play/geometry.ts +12 -2
- package/app/src/play/parallax.ts +2 -0
- package/app/src/scene/derive.ts +46 -1
- package/app/src/scene/layers.ts +52 -0
- package/app/src/scene/verify.ts +12 -0
- package/app/src/setup/checks.ts +2 -2
- package/app/src/studio/agent.ts +7 -6
- package/app/src/studio/repair.ts +1 -1
- package/app/web/living.ts +36 -0
- package/app/web/main.ts +41 -6
- package/docs/releasing.md +22 -0
- package/docs/scenes.md +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -113,6 +113,7 @@ Model answers are memoised by a hash of their inputs, so running a step again wi
|
|
|
113
113
|
- [Characters, style and icons](docs/characters.md)
|
|
114
114
|
- [NPC story stages, optional walks and travel](docs/npc-motion.md)
|
|
115
115
|
- [Sound and voices](docs/sound.md)
|
|
116
|
+
- [Releasing and the changelog](docs/releasing.md) ([CHANGELOG.md](CHANGELOG.md))
|
|
116
117
|
|
|
117
118
|
## Built at the Meta AI Global Hackathon
|
|
118
119
|
|
package/app/src/actions/fix.ts
CHANGED
|
@@ -22,19 +22,11 @@ async function fixPlace(ctx: ActionContext, slug: string, place: string, from: '
|
|
|
22
22
|
const dir = path.join(ctx.root, slug);
|
|
23
23
|
const blocked = agentBlocked(dir, ctx.by, place);
|
|
24
24
|
if (blocked) throw new ActionError(423, blocked);
|
|
25
|
-
|
|
26
|
-
if (ctx.by === 'agent') {
|
|
27
|
-
const since = new Date(Date.now() - 30 * 60_000).toISOString();
|
|
28
|
-
const tries = ctx.jobs.list(slug).filter((j) => j.by === 'agent' && j.args.includes('--place') && j.args[j.args.indexOf('--place') + 1] === place && (j.queuedAt ?? '') >= since);
|
|
29
|
-
const lastOk = tries.findLastIndex((j) => j.state === 'done');
|
|
30
|
-
if (tries.slice(lastOk + 1).filter((j) => j.state === 'failed').length >= 3) {
|
|
31
|
-
throw new ActionError(429, `three fixes of ${place} in a row left it unplayable: stop here and tell the person what you tried and what you would do next`);
|
|
32
|
-
}
|
|
33
|
-
}
|
|
25
|
+
const note = ctx.by === 'agent' ? repairGuard(ctx, slug, place, label, from) : undefined;
|
|
34
26
|
const placeDir = path.join(dir, 'places', place);
|
|
35
27
|
const needs = { layout: null, paint: 'blockin.png', masks: 'painting.png', derive: 'masks.json' }[from];
|
|
36
28
|
if (needs && !fs.existsSync(path.join(placeDir, needs))) throw new ActionError(404, `${slug}/${place} has no ${needs} yet: build it first`);
|
|
37
|
-
|
|
29
|
+
const job = ctx.jobs.start(slug, ['build', slug, '--from', from, '--to', 'verify', '--place', place], `${label} · ${place}`, ctx.by, () => {
|
|
38
30
|
// The job snapshots first, then changes its source. Queued fixes must not change a running build.
|
|
39
31
|
fs.mkdirSync(placeDir, { recursive: true });
|
|
40
32
|
const fixes = readCorrections(placeDir);
|
|
@@ -42,6 +34,32 @@ async function fixPlace(ctx: ActionContext, slug: string, place: string, from: '
|
|
|
42
34
|
writeCorrections(placeDir, Corrections.parse(fixes));
|
|
43
35
|
fs.rmSync(path.join(placeDir, 'verify.json'), { force: true });
|
|
44
36
|
}, addedThing ? { place, key: addedThing } : undefined);
|
|
37
|
+
return note ? { ...job, note } : job;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Fixes that repaint or relayout cost a Muse call; cheap ones only rerun SAM and the walk grid. */
|
|
41
|
+
const COSTLY = new Set(['layout', 'paint']);
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* The agent's repair guardrail. A streak of fixes that left the place unplayable does not stop the agent: it steers it.
|
|
45
|
+
* After three, a fix that already failed in the streak is refused (it would fail the same way) and a new path goes
|
|
46
|
+
* ahead with a short reminder; after six, costly repaints and relayouts wait for the person, while cheap fixes stay open.
|
|
47
|
+
*/
|
|
48
|
+
function repairGuard(ctx: ActionContext, slug: string, place: string, label: string, from: string): string | undefined {
|
|
49
|
+
const since = new Date(Date.now() - 30 * 60_000).toISOString();
|
|
50
|
+
const tries = ctx.jobs.list(slug).filter((j) => j.by === 'agent' && j.args.includes('--place') && j.args[j.args.indexOf('--place') + 1] === place && (j.queuedAt ?? '') >= since);
|
|
51
|
+
const lastOk = tries.findLastIndex((j) => j.state === 'done');
|
|
52
|
+
const failed = tries.slice(lastOk + 1).filter((j) => j.state === 'failed');
|
|
53
|
+
if (failed.length < 3) return undefined;
|
|
54
|
+
const tried = [...new Set(failed.map((j) => j.label.replace(` · ${place}`, '')))];
|
|
55
|
+
const paths = 'other paths: mask_brush (paint or erase a thing\'s mask by hand), floor_cells (add or take back 60 px floor cells), stand_set or hero_start_set, thing_remove, corrections_clear of a fix that made it worse, or ask_person';
|
|
56
|
+
if (tried.includes(label)) {
|
|
57
|
+
throw new ActionError(429, `${place}: "${label}" already failed in this streak (${failed.length} fixes in a row left it unplayable: ${tried.join(', ')}). Do not repeat it; look with place_view (walk, masks) and take a different path. ${paths}.`);
|
|
58
|
+
}
|
|
59
|
+
if (failed.length >= 6 && COSTLY.has(from)) {
|
|
60
|
+
throw new ActionError(429, `${place}: ${failed.length} fixes in a row left it unplayable (${tried.join(', ')}). A repaint or relayout now costs credits on a guess: ask_person first, or keep to the cheap fixes. ${paths}.`);
|
|
61
|
+
}
|
|
62
|
+
return `${failed.length} fixes of ${place} in a row left it unplayable (${tried.join(', ')}); this one is a new path. Check place_status after it ends; if it fails too, tell the person briefly what you tried.`;
|
|
45
63
|
}
|
|
46
64
|
|
|
47
65
|
const thingOf = (c: Corrections, key: string) => (c.things[key] ??= { strokes: [] });
|
|
@@ -9,7 +9,7 @@ import { Geometry } from '../play/geometry.ts';
|
|
|
9
9
|
import { defineCommand } from '../registry.ts';
|
|
10
10
|
import { BACKDROP_GROW, backdropFor, smearedBackdrop, type Card } from '../scene/backdrop.ts';
|
|
11
11
|
import { carryBetween, carryFrom, paintingFor, ran, remakes, tilesLine, withChange } from '../scene/carry.ts';
|
|
12
|
-
import { cloudSheet, CRITTER, critterSheet, Depth, FALLING, fallSheet, farFlats, floatSheet, cloneFill, mistSheet, smokeSheet, waterSheet } from '../scene/layers.ts';
|
|
12
|
+
import { cloudSheet, CRITTER, critterSheet, Depth, FALLING, fallSheet, STREAM, streamSheet, farFlats, floatSheet, cloneFill, mistSheet, smokeSheet, waterSheet } from '../scene/layers.ts';
|
|
13
13
|
import { maskFromPng as waterMaskOf } from '../image/mask.ts';
|
|
14
14
|
import { WorldFiles } from '../world/files.ts';
|
|
15
15
|
|
|
@@ -80,8 +80,22 @@ defineCommand({
|
|
|
80
80
|
png = await sharp(cloneFill(raw, FRAME.width, FRAME.height, dilate(critterMask, 3)), { raw: { width: FRAME.width, height: FRAME.height, channels: 3 } }).png().toBuffer();
|
|
81
81
|
files.write(at('backdrop.png'), png);
|
|
82
82
|
}
|
|
83
|
+
// Small things painted falling in a stream (code rain, snow, petals) fall on the page (layers/stream-<i>.png):
|
|
84
|
+
// cut out of the backdrop here, each column falling at its own pace. Other things' masks stay put (a doorway
|
|
85
|
+
// inside the rain keeps what is seen through it).
|
|
86
|
+
const looks = (k: string) => place.things.find((t) => t.key === k)?.looks ?? '';
|
|
87
|
+
const streamKeys = Object.keys(notes).filter((k) => !keys.includes(k) && !critterKeys.includes(k) && STREAM.test(`${k} ${notes[k].phrase} ${looks(k)}`) && fs.existsSync(at('masks', `${k}.png`)));
|
|
88
|
+
const streams: Array<{ png: Buffer; region: Buffer; box: [number, number, number, number] }> = [];
|
|
89
|
+
for (const k of streamKeys) {
|
|
90
|
+
const others = await Promise.all(Object.keys(notes).filter((o) => o !== k && fs.existsSync(at('masks', `${o}.png`))).map(async (o) => maskFromPng(fs.readFileSync(at('masks', `${o}.png`)))));
|
|
91
|
+
const made = await streamSheet(png, await maskFromPng(fs.readFileSync(at('masks', `${k}.png`))), dilate(unionAll(FRAME.width, FRAME.height, others), 2));
|
|
92
|
+
if (!made) continue;
|
|
93
|
+
png = made.backdrop;
|
|
94
|
+
streams.push(made);
|
|
95
|
+
}
|
|
96
|
+
if (streams.length) files.write(at('backdrop.png'), png);
|
|
83
97
|
// Painted waterfalls pour (layers/fall-<i>.png); they stay out of the flat water below them.
|
|
84
|
-
const fallKeys = Object.keys(notes).filter((k) => !keys.includes(k) && FALLING.test(`${k} ${notes[k].phrase}`) && fs.existsSync(at('masks', `${k}.png`)));
|
|
98
|
+
const fallKeys = Object.keys(notes).filter((k) => !keys.includes(k) && !streamKeys.includes(k) && FALLING.test(`${k} ${notes[k].phrase}`) && fs.existsSync(at('masks', `${k}.png`)));
|
|
85
99
|
const fallMasks = await Promise.all(fallKeys.map(async (k) => maskFromPng(fs.readFileSync(at('masks', `${k}.png`)))));
|
|
86
100
|
// The far view as flats one behind another (layers/), so it has depth of its own.
|
|
87
101
|
fs.rmSync(at('layers'), { recursive: true, force: true });
|
|
@@ -133,6 +147,11 @@ defineCommand({
|
|
|
133
147
|
const falls = (await Promise.all(fallMasks.map((m) => fallSheet(png, m)))).filter((f) => !!f);
|
|
134
148
|
falls.forEach((f, i) => files.write(at('layers', `fall-${i}.png`), f.png));
|
|
135
149
|
if (falls.length) console.log(` ${place.key}: ${falls.length} waterfall(s) pouring`);
|
|
150
|
+
streams.forEach((f, i) => {
|
|
151
|
+
files.write(at('layers', `stream-${i}.png`), f.png);
|
|
152
|
+
files.write(at('layers', `stream-${i}-region.png`), f.region);
|
|
153
|
+
});
|
|
154
|
+
if (streams.length) console.log(` ${place.key}: ${streams.length} stream(s) of small things falling (${streamKeys.join(', ')})`);
|
|
136
155
|
const critters = critterKeys.length ? await critterSheet(withCritters, critterMask, wetMask) : [];
|
|
137
156
|
critters.forEach((c, i) => files.write(at('layers', `critter-${i}.png`), c.png));
|
|
138
157
|
if (critters.length) console.log(` ${place.key}: ${critters.length} painted animal(s) ${critters.some((c) => c.medium === 'air') ? 'flying and ' : ''}swimming (${critterKeys.join(', ')})`);
|
|
@@ -162,7 +181,7 @@ defineCommand({
|
|
|
162
181
|
files.write(at('layers', 'mist-mask.png'), mist.mask);
|
|
163
182
|
console.log(` ${place.key}: painted ${mist.phrase} drifting ${mist.drift === null ? 'with the wind' : mist.drift < 0 ? 'left' : 'right'}${mist.speed !== 1 ? ` x${mist.speed}` : ''}`);
|
|
164
183
|
}
|
|
165
|
-
if (far || water || mist || smoke.length || falls.length || critters.length || clouds) {
|
|
184
|
+
if (far || water || mist || smoke.length || falls.length || streams.length || critters.length || clouds) {
|
|
166
185
|
if (far) {
|
|
167
186
|
files.write(at('layers', 'sky.png'), far.sky);
|
|
168
187
|
far.flats.forEach((f, i) => files.write(at('layers', `${i}.png`), f.png));
|
|
@@ -176,6 +195,7 @@ defineCommand({
|
|
|
176
195
|
...(smoke.length && { smoke: smoke.map((s, i) => ({ file: `layers/smoke-${i}.png`, box: s.box })) }),
|
|
177
196
|
...(floats.length && { floats: floats.map((s, i) => ({ file: `layers/float-${i}.png`, box: s.box })) }),
|
|
178
197
|
...(falls.length && { falls: falls.map((f, i) => ({ file: `layers/fall-${i}.png`, box: f.box })) }),
|
|
198
|
+
...(streams.length && { streams: streams.map((f, i) => ({ file: `layers/stream-${i}.png`, region: `layers/stream-${i}-region.png`, box: f.box })) }),
|
|
179
199
|
...(critters.length && { critters: critters.map((c, i) => ({ file: `layers/critter-${i}.png`, box: c.box, medium: c.medium, facing: c.facing })) }),
|
|
180
200
|
...(clouds && { clouds: { box: clouds.box, clip: 'layers/cloud-clip.png', ...(clouds.keep && { keep: 'layers/cloud-keep.png' }), bands: clouds.bands.map((b, i) => ({ file: `layers/cloud-${i}.png`, box: b.box, speed: b.speed })) } }),
|
|
181
201
|
});
|
|
@@ -90,6 +90,7 @@ export async function exportWorld(slug: string, out: string, home?: string, url?
|
|
|
90
90
|
entry.layers.mist.mask = await copy(entry.layers.mist.mask);
|
|
91
91
|
}
|
|
92
92
|
for (const f of entry.layers.flats) f.file = await art(f.file); // transparent around the flat
|
|
93
|
+
for (const f of entry.layers.streams ?? []) [f.file, f.region] = [await art(f.file), await copy(f.region)];
|
|
93
94
|
for (const f of [...(entry.layers.smoke ?? []), ...(entry.layers.floats ?? []), ...(entry.layers.falls ?? []), ...(entry.layers.critters ?? []), ...(entry.layers.clouds?.bands ?? [])]) f.file = await art(f.file);
|
|
94
95
|
if (entry.layers.clouds) {
|
|
95
96
|
entry.layers.clouds.clip = await copy(entry.layers.clouds.clip);
|
package/app/src/commands/play.ts
CHANGED
|
@@ -65,6 +65,7 @@ export function worldData(files: WorldFiles) {
|
|
|
65
65
|
...(flats.smoke?.length && { smoke: flats.smoke.map((f) => ({ ...f, file: rel(f.file) })) }),
|
|
66
66
|
...(flats.floats?.length && { floats: flats.floats.map((f) => ({ ...f, file: rel(f.file) })) }),
|
|
67
67
|
...(flats.falls?.length && { falls: flats.falls.map((f) => ({ ...f, file: rel(f.file) })) }),
|
|
68
|
+
...(flats.streams?.length && { streams: flats.streams.map((f) => ({ ...f, file: rel(f.file), region: rel(f.region) })) }),
|
|
68
69
|
...(flats.critters?.length && { critters: flats.critters.map((f) => ({ ...f, file: rel(f.file) })) }),
|
|
69
70
|
...(flats.clouds && { clouds: { ...flats.clouds, clip: rel(flats.clouds.clip), ...(flats.clouds.keep && { keep: rel(flats.clouds.keep) }), bands: flats.clouds.bands.map((b) => ({ ...b, file: rel(b.file) })) } }),
|
|
70
71
|
};
|
package/app/src/play/geometry.ts
CHANGED
|
@@ -122,11 +122,21 @@ export function hides(o: Occluder, x: number, feetY: number, support?: (key: str
|
|
|
122
122
|
return feetY < floorYAt(o, x) - BEHIND_MARGIN;
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
-
/**
|
|
126
|
-
|
|
125
|
+
/**
|
|
126
|
+
* Column runs [x0, x1) inside [left, right) where the occluder hides the person; a few runs per occluder.
|
|
127
|
+
* With `feetX` the person is one body at one distance: the floor line where their feet are decides for their
|
|
128
|
+
* whole box, so a slanted desk whose near leg is nearer than the feet and far leg farther hides all of them or
|
|
129
|
+
* none, never cuts them in half down one column. Past the occluder's ends its nearest column decides.
|
|
130
|
+
*/
|
|
131
|
+
export function hiddenRuns(o: Occluder, left: number, right: number, feetY: number, support?: (key: string) => Occluder | undefined, feetX?: number): Array<[number, number]> {
|
|
127
132
|
const runs: Array<[number, number]> = [];
|
|
128
133
|
const x0 = Math.max(Math.floor(left), Math.floor(o.box[0]));
|
|
129
134
|
const x1 = Math.min(Math.ceil(right), Math.ceil(o.box[2]));
|
|
135
|
+
if (feetX !== undefined) {
|
|
136
|
+
if (x1 <= x0) return runs;
|
|
137
|
+
const at = Math.min(o.box[2] - 0.5, Math.max(o.box[0] + 0.5, feetX));
|
|
138
|
+
return hides(o, at, feetY, support) ? [[x0, x1]] : runs;
|
|
139
|
+
}
|
|
130
140
|
let start = -1;
|
|
131
141
|
for (let x = x0; x < x1; x++) {
|
|
132
142
|
const hid = hides(o, x + 0.5, feetY, support);
|
package/app/src/play/parallax.ts
CHANGED
|
@@ -164,6 +164,8 @@ export type Layers = {
|
|
|
164
164
|
floats?: Array<{ file: string; box: [number, number, number, number] }>;
|
|
165
165
|
/** Painted waterfalls to set pouring: each a feathered copy of the fall over `box`. */
|
|
166
166
|
falls?: Array<{ file: string; box: [number, number, number, number] }>;
|
|
167
|
+
/** Small things painted falling (code rain, snow, petals): what stands out of the paint over `box`, to fall column by column inside `region` (a soft mask). */
|
|
168
|
+
streams?: Array<{ file: string; region: string; box: [number, number, number, number] }>;
|
|
167
169
|
/** Painted animals as small cards to swim (in the water) or fly (in the air), and which way each faces (1 right). */
|
|
168
170
|
critters?: Array<{ file: string; box: [number, number, number, number]; medium: 'water' | 'air'; facing: 1 | -1 }>;
|
|
169
171
|
/** The painted clouds as bands (bottom first) to drift with the wind at their own speeds, cut to `clip` over `box`; `keep`: what stays put in the sky. */
|
package/app/src/scene/derive.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import { emptyMask, maskArea, type Mask } from '../image/mask.ts';
|
|
6
6
|
import { dilate, maskBox, polygonMask, rectMask, subtract, union, unionAll } from '../image/raster.ts';
|
|
7
7
|
import type { Memo } from '../meta/memo.ts';
|
|
8
|
-
import type { Geometry } from '../play/geometry.ts';
|
|
8
|
+
import type { GeoThing, Geometry } from '../play/geometry.ts';
|
|
9
9
|
import type { Place } from '../world/schema.ts';
|
|
10
10
|
import { FRAME, type Layout } from './layout.ts';
|
|
11
11
|
import type { Kind } from './locate.ts';
|
|
@@ -426,6 +426,51 @@ export async function derive(memo: Memo, input: DeriveIn, log: (line: string) =>
|
|
|
426
426
|
const stand = thing.kind === 'walk-on' || thing.kind === 'flat' ? spot(cx, (box[1] + box[3]) / 2) : spot(cx, baseY + 14);
|
|
427
427
|
return { key: t.key, role: t.role, kind: thing.kind, box, mask: `masks/${t.key}.png`, stand, ...(thing.coarse && { coarse: true }), ...(input.plates?.includes(t.key) && { plate: `plates/${t.key}.png` }) };
|
|
428
428
|
});
|
|
429
|
+
// Npcs are drawn by the game over the painting: one standing in front of an item or an exit (a scientist
|
|
430
|
+
// before the potion on his table) hides it, and nobody can click it. They step aside along their row, to the
|
|
431
|
+
// nearest reachable spot where they hide little of any.
|
|
432
|
+
type Box = [number, number, number, number];
|
|
433
|
+
const covered = (box: Box, by: GeoThing): number => {
|
|
434
|
+
const t = things.find((m) => m.key === by.key);
|
|
435
|
+
if (!t) return 0;
|
|
436
|
+
const [bx0, by0, bx1, by1] = by.box;
|
|
437
|
+
let [all, hid] = [0, 0];
|
|
438
|
+
for (let y = Math.max(0, Math.floor(by0)); y < Math.min(H, Math.ceil(by1)); y++) for (let x = Math.max(0, Math.floor(bx0)); x < Math.min(W, Math.ceil(bx1)); x++) {
|
|
439
|
+
if (!t.mask.bits[y * W + x]) continue;
|
|
440
|
+
all++;
|
|
441
|
+
if (x >= box[0] && x < box[2] && y >= box[1] && y < box[3]) hid++;
|
|
442
|
+
}
|
|
443
|
+
return all ? hid / all : 0;
|
|
444
|
+
};
|
|
445
|
+
for (const npc of geoThings.filter((t) => t.role === 'npc' && !t.mask)) {
|
|
446
|
+
const foot = npc.box[3];
|
|
447
|
+
const behind = geoThings.filter((t) => (t.role === 'item' || t.role === 'exit') && t.mask && t.box[3] < foot + 4 && t.kind !== 'wall');
|
|
448
|
+
const worst = (box: Box) => Math.max(0, ...behind.map((t) => covered(box, t)));
|
|
449
|
+
if (worst(npc.box) < 0.4) continue;
|
|
450
|
+
const w = npc.box[2] - npc.box[0];
|
|
451
|
+
const cx = (npc.box[0] + npc.box[2]) / 2;
|
|
452
|
+
let moved: Box | undefined;
|
|
453
|
+
for (let d = 16; d <= 480 && !moved; d += 16) for (const x of [cx + d, cx - d]) {
|
|
454
|
+
if (x - w / 2 < 0 || x + w / 2 > W) continue;
|
|
455
|
+
const i = nearestRoomy(grid, x, foot, reached);
|
|
456
|
+
if (i === undefined) continue;
|
|
457
|
+
const [gx, gy] = grid.centre(i);
|
|
458
|
+
if (Math.hypot(gx - x, gy - foot) > 2 * CELL) continue;
|
|
459
|
+
const box: Box = [x - w / 2, npc.box[1], x + w / 2, foot];
|
|
460
|
+
if (worst(box) < 0.15) {
|
|
461
|
+
moved = box;
|
|
462
|
+
break;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
const hidden = behind.filter((t) => covered(npc.box, t) >= 0.4).map((t) => t.key).join(', ');
|
|
466
|
+
if (!moved) {
|
|
467
|
+
flag(`${npc.key}: stands in front of ${hidden} and hides it, with nowhere near to step aside to`);
|
|
468
|
+
continue;
|
|
469
|
+
}
|
|
470
|
+
log(`${npc.key}: steps ${Math.round((moved[0] + moved[2]) / 2 - cx)} px aside so ${hidden} stays in sight`);
|
|
471
|
+
npc.box = moved;
|
|
472
|
+
npc.stand = spot((moved[0] + moved[2]) / 2, foot);
|
|
473
|
+
}
|
|
429
474
|
for (const t of geoThings) {
|
|
430
475
|
// Npcs stand where the layout put them; wall things (a ship at sea) may be far from any floor.
|
|
431
476
|
if (role.get(t.key) === 'npc' || t.kind === 'wall' || t.kind === 'on-top') continue;
|
package/app/src/scene/layers.ts
CHANGED
|
@@ -597,6 +597,58 @@ export const FALLING = /\b(water ?falls?|cascades?|falls|weir|spillway|wodospad\
|
|
|
597
597
|
export const CRITTER = /\b(fish(es)?|goldfish|carp|trout|salmon|koi|eels?|dolphins?|whales?|seals?|ducks?|ducklings?|swans?|geese|goose|frogs?|birds?|gulls?|seagulls?|sparrows?|crows?|ravens?|pigeons?|doves?|swallows?|owls?|bats?|butterfl(y|ies)|dragonfl(y|ies)|jellyfish|ryb\w*|ptak\w*|mew\w*|kacz\w*|łabęd\w*|wron\w*|motyl\w*)\b/i;
|
|
598
598
|
|
|
599
599
|
export type Fall = { png: Buffer; box: [number, number, number, number] };
|
|
600
|
+
/**
|
|
601
|
+
* Small things painted falling in a stream (code rain, falling letters, snowflakes, petals, sparks): the page
|
|
602
|
+
* sets them falling, each column at its own pace, instead of rippling them like water.
|
|
603
|
+
*/
|
|
604
|
+
const STREAM_BITS = String.raw`letters?|code|symbols?|characters?|glyphs?|digits?|numbers?|numerals?|pixels?|bits|sparks?|petals?|leaves|snow\w*|stars?|confetti|runes?|litery|liter\w*|znak\w*|kod\w*|cyfr\w*|płatk\w*|liści\w*|gwiazd\w*|iskr\w*`;
|
|
605
|
+
export const STREAM = new RegExp(String.raw`\b(falling|raining|rain of|shower of|showers of|streams? of|stream of|cascad\w*|curtains? of|spadaj\w*|deszcz\w*)\b[\w\s-]{0,30}?\b(${STREAM_BITS})\b|\b(code|digital|matrix|data) rain\b|\b(${STREAM_BITS})[\s-]+(falling|raining|fall|rain|spadaj\w*)\b`, 'i');
|
|
606
|
+
|
|
607
|
+
export type Stream = Fall & { region: Buffer; backdrop: Buffer };
|
|
608
|
+
|
|
609
|
+
/** How far a stream's mask is closed over the glyphs SAM left out of it, px. */
|
|
610
|
+
const STREAM_CLOSE = 16;
|
|
611
|
+
|
|
612
|
+
/**
|
|
613
|
+
* A painted stream of small falling things, cut out to fall on the page: what stands out of the paint behind it
|
|
614
|
+
* (the glyphs: lighter or more coloured than the median around them) under `mask` is the sheet; the backdrop
|
|
615
|
+
* gets the paint behind them in their place, so no still copy stays under the falling one. `region` is the
|
|
616
|
+
* mask, softened, that the falling sheet is cut to. Undefined when nothing stands out.
|
|
617
|
+
*/
|
|
618
|
+
export async function streamSheet(backdrop: Buffer, painted: Mask, keepOut?: Mask): Promise<Stream | undefined> {
|
|
619
|
+
const [W, H]: [number, number] = [FRAME.width, FRAME.height];
|
|
620
|
+
// SAM finds the paint between the glyphs more often than the glyphs: closed, the mask holds them too.
|
|
621
|
+
const closed = dilate(dilate(painted, STREAM_CLOSE), -STREAM_CLOSE);
|
|
622
|
+
const mask = keepOut ? subtract(closed, keepOut) : closed;
|
|
623
|
+
if (maskArea(mask) < 1500) return undefined;
|
|
624
|
+
const rgb = new Uint8Array(await sharp(backdrop).resize(W, H, { fit: 'fill' }).removeAlpha().raw().toBuffer());
|
|
625
|
+
const bg = new Uint8Array(await sharp(rgb, { raw: { width: W, height: H, channels: 3 } }).median(15).raw().toBuffer());
|
|
626
|
+
const glyph = emptyMask(W, H);
|
|
627
|
+
let n = 0;
|
|
628
|
+
for (let i = 0; i < W * H; i++) {
|
|
629
|
+
if (!mask.bits[i]) continue;
|
|
630
|
+
const d = [0, 1, 2].map((c) => rgb[i * 3 + c] - bg[i * 3 + c]);
|
|
631
|
+
const luma = 0.3 * d[0] + 0.59 * d[1] + 0.11 * d[2];
|
|
632
|
+
if (luma > 22 || Math.max(...d.map(Math.abs)) > 45) {
|
|
633
|
+
glyph.bits[i] = 1;
|
|
634
|
+
n++;
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
if (n < 400) return undefined;
|
|
638
|
+
const cut = intersect(dilate(glyph, 2), mask);
|
|
639
|
+
const [x0, y0, x1, y1] = boxOf(mask);
|
|
640
|
+
const region = { left: x0, top: y0, width: x1 - x0, height: y1 - y0 };
|
|
641
|
+
const alpha = await sharp(await maskToPng(cut)).blur(0.8).extract(region).extractChannel(0).raw().toBuffer();
|
|
642
|
+
const png = await sharp(rgb, { raw: { width: W, height: H, channels: 3 } }).extract(region).joinChannel(alpha, { raw: { width: region.width, height: region.height, channels: 1 } }).png().toBuffer();
|
|
643
|
+
const soft = await sharp(await maskToPng(mask)).blur(6).extract(region).extractChannel(0).raw().toBuffer();
|
|
644
|
+
const regionPng = await sharp(Buffer.alloc(region.width * region.height * 3, 255), { raw: { width: region.width, height: region.height, channels: 3 } }).joinChannel(soft, { raw: { width: region.width, height: region.height, channels: 1 } }).png().toBuffer();
|
|
645
|
+
// The paint behind the glyphs where they were: the median of the paint around them.
|
|
646
|
+
const out = new Uint8Array(rgb);
|
|
647
|
+
for (let i = 0; i < W * H; i++) if (cut.bits[i]) for (let c = 0; c < 3; c++) out[i * 3 + c] = bg[i * 3 + c];
|
|
648
|
+
const filled = await sharp(out, { raw: { width: W, height: H, channels: 3 } }).png().toBuffer();
|
|
649
|
+
return { png, box: [x0, y0, x1, y1], region: regionPng, backdrop: filled };
|
|
650
|
+
}
|
|
651
|
+
|
|
600
652
|
|
|
601
653
|
/**
|
|
602
654
|
* A painted waterfall for the page to set pouring: its pixels (from the backdrop) under its mask, feathered, over
|
package/app/src/scene/verify.ts
CHANGED
|
@@ -93,6 +93,10 @@ export function verifyPlace(place: Place, g: Geometry, notes: MaskNotes, plates:
|
|
|
93
93
|
// of thing: a doubt about details ("but no long rays in its mask", when the painting has none), not a wrong
|
|
94
94
|
// mask. Brushing a disc of sky onto the mask to please it only gave the thing a round outline.
|
|
95
95
|
if (check.box && check.seen && rectIou(check.box, t.box) >= 0.6 && sameKind(check.seen, thing.key, thing.looks)) flags.push({ code: 'MASK_DOUBT', key: thing.key, detail: `Spark sees ${seen} in its mask, where the mask is` });
|
|
96
|
+
// A way out in a wall is cut as the opening its frame holds, and Spark judges the opening by what shows through
|
|
97
|
+
// it (green-code-hall: "a desk and servers seen through the doorway"). When the opening lies inside the box
|
|
98
|
+
// Spark gives the doorway, the mask is the way out as meant: a doubt to look at, not a mask to fix again.
|
|
99
|
+
else if (check.box && thing.role === 'exit' && t.kind === 'wall' && inside(t.box, check.box) >= 0.9) flags.push({ code: 'MASK_DOUBT', key: thing.key, detail: `Spark sees ${seen} in its opening, inside the doorway at [${check.box.join(', ')}]` });
|
|
96
100
|
// Spark points to where the thing really is: the mask is wrong for sure, whatever the thing's role.
|
|
97
101
|
else if (check.box) errors.push({ code: 'MASK_WRONG', key: thing.key, detail: `Spark sees ${seen} in its mask; it is at [${check.box.join(', ')}]`, box: check.box });
|
|
98
102
|
// Spark sees no such thing anywhere, and what it sees is not even the same kind of thing: it was never
|
|
@@ -154,3 +158,11 @@ export function verifyPlace(place: Place, g: Geometry, notes: MaskNotes, plates:
|
|
|
154
158
|
for (const line of g.flagged ?? []) flags.push({ code: 'DERIVE', detail: line });
|
|
155
159
|
return { ok: errors.length === 0, errors, flags };
|
|
156
160
|
}
|
|
161
|
+
|
|
162
|
+
/** How much of rect a lies inside rect b, 0 to 1. */
|
|
163
|
+
function inside(a: readonly number[], b: readonly number[]): number {
|
|
164
|
+
const w = Math.max(0, Math.min(a[2], b[2]) - Math.max(a[0], b[0]));
|
|
165
|
+
const h = Math.max(0, Math.min(a[3], b[3]) - Math.max(a[1], b[1]));
|
|
166
|
+
const area = (a[2] - a[0]) * (a[3] - a[1]);
|
|
167
|
+
return area > 0 ? (w * h) / area : 0;
|
|
168
|
+
}
|
package/app/src/setup/checks.ts
CHANGED
|
@@ -55,7 +55,7 @@ export async function runChecks(opts: { live?: boolean } = {}): Promise<Check[]>
|
|
|
55
55
|
|
|
56
56
|
const muse = await sh('muse', ['--version']);
|
|
57
57
|
// Other agents the Studio can switch to (genaicode drives them the same way, with the Studio's MCP tools).
|
|
58
|
-
const others = await Promise.all([['claude', 'Claude Code'], ['codex', 'Codex']].map(async ([cmd, label]) => ({ label, version: await sh(cmd, ['--version']) })));
|
|
58
|
+
const others = await Promise.all([['claude', 'Claude Code'], ['codex', 'Codex'], ['agy', 'Antigravity']].map(async ([cmd, label]) => ({ label, version: await sh(cmd, ['--version']) })));
|
|
59
59
|
const found = others.filter((o) => o.version);
|
|
60
60
|
const install = ['curl -fsSL https://dev.meta.ai/install.sh | bash', 'muse login', 'muse --version'];
|
|
61
61
|
checks.push(muse
|
|
@@ -68,7 +68,7 @@ export async function runChecks(opts: { live?: boolean } = {}): Promise<Check[]>
|
|
|
68
68
|
id: 'agents',
|
|
69
69
|
label: 'Other agents',
|
|
70
70
|
state: found.length ? 'ok' : 'optional',
|
|
71
|
-
detail: `${others.map((o) => (o.version ? `${o.label} ${/\d+\.\d+[\w.-]*/.exec(o.version)?.[0] ?? 'found'}` : `${o.label} not found`)).join(' · ')}. Switch in the agent panel; DIORAMA_AGENT=muse, claude or
|
|
71
|
+
detail: `${others.map((o) => (o.version ? `${o.label} ${/\d+\.\d+[\w.-]*/.exec(o.version)?.[0] ?? 'found'}` : `${o.label} not found`)).join(' · ')}. Switch in the agent panel; DIORAMA_AGENT=muse, claude, codex or antigravity sets the default.`,
|
|
72
72
|
});
|
|
73
73
|
|
|
74
74
|
let chromium: string | null = null;
|
package/app/src/studio/agent.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import { randomBytes } from 'node:crypto';
|
|
6
6
|
import fs from 'node:fs';
|
|
7
7
|
import path from 'node:path';
|
|
8
|
-
import { claude, codexLive, createAgentSession, detectAgents, museLive, type CodingAgent, withoutProviderCredentials, type AgentSession, type ApprovalDecision, type ApprovalRequest, type SessionState, type SessionTurn } from 'genaicode/agents';
|
|
8
|
+
import { antigravity, claude, codexLive, createAgentSession, detectAgents, museLive, type CodingAgent, withoutProviderCredentials, type AgentSession, type ApprovalDecision, type ApprovalRequest, type SessionState, type SessionTurn } from 'genaicode/agents';
|
|
9
9
|
import { configDir, redact, withoutSecrets } from '../secrets.ts';
|
|
10
10
|
import type { Events } from './events.ts';
|
|
11
11
|
import { History } from './history.ts';
|
|
@@ -28,7 +28,7 @@ When the person wants the game in another language ("make it English", "po polsk
|
|
|
28
28
|
When the person wants the game to look different (more realistic, watercolour, like a picture book, less cartoon), call style_set with the style in a few words (medium, palette, light; scope place or cast for only one place or the characters): first without go, tell them what it repaints and about what it costs, then call it with go true once they agree. It repaints each picture keeping where everything is, so masks and their fixes still fit; children's drawings stay the reference; history_undo takes it back. Never write world.json's style by hand.
|
|
29
29
|
Before a risky change, say what you will do; history_undo can take back a job. Respect locks. Keys are never yours: if a key is missing, ask the person to run \`diorama setup\` in a terminal.
|
|
30
30
|
Read files freely to find out why something failed, but do not edit world files or run commands that change them: make every change through the diorama MCP tools, so the person sees it live, can undo it, and it survives the next build (a hand edit is overwritten by the next build). When no single tool makes the change the person asks for, do not stop at "there is no tool": find the nearest way with the tools you have (several story_text calls, a story_redo with their wish, a run_step) and offer it with what it costs, using ask_person; mention the missing tool only as a side note, so it can be added later. Refuse only what would need a hand edit or would throw away their work. Start from world_status, place_status, place_view, job_wait, corrections_get and feedback_pending: place_view shows you a place as a picture (layer walk, masks or painting), which files and the shell cannot. Reach for the shell only when no Studio tool answers the question.
|
|
31
|
-
When a build fails for another reason (a step that broke, a service that did not answer), the Studio wakes you with the error and the job's last lines: job_inspect it, give a service error (HTTP 5xx, a timeout, a busy Space) one job_retry, which picks up at the failed step, and fix anything else with that step's own tool; never retry the same error twice, tell the person instead. A character the 3D service left in 2D (cast_status: the 3D service did not answer) still plays with its 2D rig; cast_animate tries its model again once the service is back. When a build leaves places not playable, repair them one by one: place_status lists each error with its playbook (where to look, the fix tools in order, the last resort), and place_view shows you the walkable floor with the 60 px cells floor_cells takes, the gaps to bridge and the spots off the floor. An unreached patch where nothing is floor (a wall, the sky) is taken back, not bridged: corrections_clear the fix that made it, or mask_brush erase. Try the tools in that order; each fix rebuilds only that place, so read place_status again after its job ends.
|
|
31
|
+
When a build fails for another reason (a step that broke, a service that did not answer), the Studio wakes you with the error and the job's last lines: job_inspect it, give a service error (HTTP 5xx, a timeout, a busy Space) one job_retry, which picks up at the failed step, and fix anything else with that step's own tool; never retry the same error twice, tell the person instead. A character the 3D service left in 2D (cast_status: the 3D service did not answer) still plays with its 2D rig; cast_animate tries its model again once the service is back. When a build leaves places not playable, repair them one by one: place_status lists each error with its playbook (where to look, the fix tools in order, the last resort), and place_view shows you the walkable floor with the 60 px cells floor_cells takes, the gaps to bridge and the spots off the floor. An unreached patch where nothing is floor (a wall, the sky) is taken back, not bridged: corrections_clear the fix that made it, or mask_brush erase. Try the tools in that order; each fix rebuilds only that place, so read place_status again after its job ends. After three fixes that did not change the error, do not repeat one that failed: look again with place_view and take a different path (mask_brush by hand, floor_cells, thing_remove, corrections_clear); the fix tools refuse an exact repeat, and after six they keep repaints and relayouts for the person. If the new path fails too, tell the person briefly what you tried and what you would do next. Ask before the last resort, except one repaint_region per place.
|
|
32
32
|
When layout failed and there is no picture, do not wait for place_view: inspect place_status errors, then call layout_fix with concrete layout guidance, or place_fix to resume. Both work before the place directory exists. Failed layout batches retain their diagnostics and retry with new feedback instead of replaying the same rejected batch. When paint fails with content_policy_violation, the image safety filter refused a word, not the picture: paint already lets Spark reword the place twice, so call paint_words with calm, plain words for the scene and the looks the brief had (leafless, not bare; a seat that rests, not hangs on chains; no children, blood or weapons), never an edit of world.json. A place that never got past a step is rebuilt from that step: place_fix (the Fix it button on its card) or job_retry does that by itself.
|
|
33
33
|
Stairs: derive finds the steps of floors and walk-on things named as stairs (stair, step, schody), and people then step from tread to tread; place_view (layer walk) draws the steps it found in blue. Scale: derive measures how tall a person is at each spot (Spark's people, held against a door on the floor: a person by a door is about 0.8 of its height). When the hero looks far too small or too big next to a door, run_step derive with that place, which rebuilds it through verify; when it stays wrong, the door is not named as a door: give it a door phrase with mask_fix. When people glide up stairs as on a ramp, the stairs thing or floor is not named as stairs: give it a stairs phrase with mask_fix (for example "stone stairs"), which rebuilds the place.
|
|
34
34
|
A message that starts with "[On screen: …]" says what the person had open in Studio as they wrote ("place", "place / thing", "cast", "cast / character" or "cast / item key"; older messages say "[Looking at: …]"): when they say "this", "it" or "here" without a name, they mean that; look at it (place_view for a place) before you change it. Never mention the prefix. A message that starts with "[Pictures: studio/pictures/01.jpg, …]" comes with pictures the person pasted, uploaded or took with the webcam: look at each with picture_view before you answer, and act on what they show. When the person is playing and your change shows in the game, call play_reload yourself rather than asking them to refresh. You cannot watch the game, but place_motion can: after a change to motion (water flow, fog, fire, weather, an animated thing) call it and check the motion shows before you say it works. A message that starts with "[Studio]" is a private note from the Studio, not from the person: follow it, never mention it or reply to it. The story has story_status, story_text (rewording) and story_redo (a wish to Spark); story_status lists unanswerable questions (a question in a "say" or in a line without choices leaves the player stuck): reword it into a statement with story_text, or story_redo when it needs real answers; it also lists unseen story parts (an item thing whose item has another key stays in the painting; a change on a person is never drawn): fix those with story_redo; the cast has cast_status, cast_redo, cast_animate, cast_rewalk, cast_holds and voice_set; a figure whose cut-out lost what it holds or carries (a toast over its head, a lantern, a basket; cast_status cut.holds lists what is kept) gets cast_holds, not cast_redo: it cuts the same painting again with the held thing; speaking faces have faces_status and faces_prepare. Talking mouths can be off in Setup (faces_get; off by default): then never prepare, repair or calibrate faces and never offer to; characters speak with voice and gestures. Free fixes before paid ones: a 3D figure whose face shows as a patch of another colour, or whose colours look pale or plastic beside its painting, is a render matter; run sprites again (free for UV sprites, no Muse) before ever offering sprites_redo or per-frame Muse repaints, and when you do offer paid work say the real count, not a range. Order: a playable game first, talking mouths last. While any place is unpainted, being built or unplayable, never start face work (faces_prepare, face_frame_repair, face_lips_set, face_calibration_set refuse it); repair the places, and leave lips for after everything plays, or skip them: characters talk with gestures. A failed speaking animation is separate from game readiness; recordings and the previous appearance remain usable. Inspect coverage and repairWork first. Accepted atlases and calibrated mouths are saved, so resume rather than repainting the whole character. For a rejected mouth, faces_inspect with its work id and mouth returns the exact head and reference; face_frame_repair uses the returned revision and concrete visual guidance to edit ONLY that mouth, then revalidate the atlas. For an accepted atlas with uncertain lip corners, inspect the exact mouth image with faces_inspect and use face_lips_set with a normalized tight lip box relative to that single full-head image (not the whole atlas). Never guess coordinates, use it on rejected articulation, or lower validation thresholds. Both tools resume preparation and reject stale revisions/sources. Wait for the job, inspect faces_status again and play_reload after success. Do not repeat an unchanged failed repair; inspect the new stage and use its matching tool. For source-pose or final-patch calibration failures, face_calibration_inspect lists pending targets and shows their exact close-up. Use face_calibration_set with its target, revision, imageHash, tight lip box and visible-cavity open flag. This preserves accepted artwork and resumes the remaining checks. If an inspected mouth remains genuinely ambiguous, stop and explain that specific uncertainty rather than inventing coordinates. Image, 2D rig and talk-sprite faces share audio timing. 3D models articulate when model3d.mouth is true: method morphs uses authored facial poses, painted-surfaces animates calibrated face paint on the actual mesh. Check the reported view coverage and spritePoses pending count; a body skeleton alone does not articulate. Do not claim native mouth geometry for the painted-surface method; a 3D figure that walks straddle-legged with arms held out to the sides (like a zombie) or hands sticking out gets cast_relax (local Blender, free; blender_get says if it is there, else tell the person to install Blender and turn it on in Setup); a walk that tears (holes in clothes, a leg, boot or arm coming off, a lantern left hanging in the air) gets cast_rewalk, not cast_redo; a character with no 3D model the person explicitly wants animated in 3D (a creature, cast_status body upright false, or an NPC with no lines) gets a model with cast_animate, never a cast_redo that makes it stand on two legs: a creature hops, squashes and blinks as one body, then play_reload; a hero painted sitting, lying or with furniture grown into the 3D model (from a photo where they sat or lay) gets cast_redo with looks that describe only the person standing (clothes, trousers, shoes; no pose, bed or "not visible"), then the model again; inventory icons have icons_status, icon_redo (a hint in the person's words, cut or paint, another take) and icon_fix_clear, never a swapped file in items/. Voices are cast from the gender and age Spark sees in each figure: adults get stock voices, children, teens and the old get a voice made for them by ElevenLabs Voice Design (voice "design"). When one sounds wrong, voice_list with slug and key lists the voices that fit that character first; voice_set with voice "design" and a design description in English remakes a designed voice (no word "child" or age in years there: ElevenLabs refuses it). Never give a child an adult's voice or raise an adult voice's pitch to fake one. A character whose voice is "pending-design" in cast_status waits for Voice Design (it was refused or the service was busy): run_step voices or voice_set designs it again; never give them a stock adult instead. Lines with numbers or sums ("10+10=20") are read and checked as words by themselves; voiceFailures lists only lines the story still has. For one missing or rejected line, use voice_retry with slug, key and the exact text from voiceFailures: it keeps the selected voice and every other recording, gives rejected takes fresh seeds, and resumes saved audio after a transcription outage. Do not change voice_set merely to retry a line. Sounds have sound_status (each bed, loop, footstep, one-shot and music: made, or rejected and why) and sound_redo (ids, an optional hint in English, kept across rebuilds): a sound rejected as "mostly hiss" or "nearly silent" stays silent in the game until sound_redo, never an edited sound.json. cast_status says how the game moves each character (moves), why there is no 2D walk (rigIssue) and which walk directions left the pose (spriteIssues): those get sprites_redo, which paints new sheets without repainting the character; a character with no model after flat reconstructions walks with its 2D rig or slides as a cut-out, and cast_animate tries the model again. When the hero starts in front of an NPC or a thing and hides it, hero_start_set moves where they begin; npc_move changes the NPC standing position in the layout. Author a story-triggered trip with story_redo. A still frame in place_motion is a fault only when inTheGame lists water, mist or weather for that place.
|
|
@@ -51,7 +51,7 @@ export function studioTool(request: ApprovalRequest): boolean {
|
|
|
51
51
|
const detail = (request.detail ?? {}) as { subject?: { kind?: string } };
|
|
52
52
|
if (['shell', 'process', 'fileAccess'].includes(String(detail.subject?.kind))) return false;
|
|
53
53
|
const text = JSON.stringify(detail);
|
|
54
|
-
return /"
|
|
54
|
+
return /"mcp__?diorama__?[a-z_]+"/.test(text) || /"(server|serverName|mcpServer)"\s*:\s*"diorama"/.test(text);
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
/**
|
|
@@ -71,9 +71,9 @@ export function readsOnly(request: ApprovalRequest): boolean {
|
|
|
71
71
|
|
|
72
72
|
type Call = { turn?: number; name: string; args: Record<string, unknown>; done: boolean; ok?: boolean; summary?: string };
|
|
73
73
|
|
|
74
|
-
/** A Studio tool's own name, however the agent spells it (mcp__diorama__job_wait, diorama.job_wait). */
|
|
74
|
+
/** A Studio tool's own name, however the agent spells it (mcp__diorama__job_wait, Antigravity's mcp_diorama_job_wait, diorama.job_wait). */
|
|
75
75
|
export function toolName(name: string): string | undefined {
|
|
76
|
-
return /^(?:
|
|
76
|
+
return /^(?:mcp__?diorama__?|diorama[.:_/]+)(\w+)$/.exec(name)?.[1];
|
|
77
77
|
}
|
|
78
78
|
|
|
79
79
|
/** A tool's result in one line for the panel: a job by its name and state, else the result itself, short. */
|
|
@@ -225,7 +225,7 @@ class Talk {
|
|
|
225
225
|
// A turn's entries only grow, so a turn and a place in it name an entry.
|
|
226
226
|
if (entry.kind === 'tool' && !this.tools.has(`${turn.id}:${i}`)) {
|
|
227
227
|
this.tools.add(`${turn.id}:${i}`);
|
|
228
|
-
this.publish('tool', { name: entry.name.
|
|
228
|
+
this.publish('tool', { name: toolName(entry.name) ?? entry.name });
|
|
229
229
|
// The shell sees files, not the pictures the Studio's tools show: after a few shell calls in a turn,
|
|
230
230
|
// the agent is reminded once which tools look for it. It is still let through.
|
|
231
231
|
if (shellTool(entry.name) && ++this.shell === NUDGE_AFTER) {
|
|
@@ -302,6 +302,7 @@ export const AGENTS = {
|
|
|
302
302
|
muse: { label: 'Muse Code', make: (command?: string) => museLive({ command: command ?? process.env.DIORAMA_MUSE_COMMAND ?? 'muse' }) },
|
|
303
303
|
claude: { label: 'Claude Code', make: () => claude() },
|
|
304
304
|
codex: { label: 'Codex', make: () => preapproved(codexLive()) },
|
|
305
|
+
antigravity: { label: 'Antigravity', make: () => antigravity({ command: process.env.DIORAMA_AGY_COMMAND ?? 'agy' }) },
|
|
305
306
|
} satisfies Record<string, { label: string; make(command?: string): CodingAgent }>;
|
|
306
307
|
export type AgentName = keyof typeof AGENTS;
|
|
307
308
|
export const isAgentName = (name: string): name is AgentName => Object.hasOwn(AGENTS, name);
|
package/app/src/studio/repair.ts
CHANGED
|
@@ -19,7 +19,7 @@ export function repairPrompt(job: Job): string {
|
|
|
19
19
|
if (job.unplayable?.length) {
|
|
20
20
|
const places = job.unplayable.map((u) => `- ${u.place}: ${u.errors.join('; ') || 'see place_status'}`).join('\n');
|
|
21
21
|
return `[Studio] The build "${job.label}" ended with places that are not playable, and its free fixes did not help:\n${places}\n`
|
|
22
|
-
+ 'Repair them as your brief says: place_status and place_view first, then the playbook\'s fix tools in order,
|
|
22
|
+
+ 'Repair them as your brief says: place_status and place_view first, then the playbook\'s fix tools in order, after three failed fixes of a place take a different path rather than repeating one, '
|
|
23
23
|
+ 'and ask me before the last resort. Then tell me in two sentences what you fixed and what is left for me.' + animation;
|
|
24
24
|
}
|
|
25
25
|
const tail = job.tail.slice(-15).map((l) => ` ${l}`).join('\n');
|
package/app/web/living.ts
CHANGED
|
@@ -402,3 +402,39 @@ export class Clouds {
|
|
|
402
402
|
ctx.drawImage(canvas, this.box[0] + sx, this.box[1] + sy);
|
|
403
403
|
}
|
|
404
404
|
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* Small things painted falling (code rain, snow, petals): the glyphs cut out of the backdrop fall in columns,
|
|
408
|
+
* each column at its own pace, wrapping round inside their painted region (a soft mask) so they keep falling.
|
|
409
|
+
*/
|
|
410
|
+
export class Stream {
|
|
411
|
+
private readonly canvas: HTMLCanvasElement;
|
|
412
|
+
/** Column width, px: about one glyph, so a letter falls whole. */
|
|
413
|
+
private static readonly COLUMN = 22;
|
|
414
|
+
|
|
415
|
+
constructor(readonly img: HTMLImageElement, readonly region: HTMLImageElement, readonly box: Box, readonly k: number) {
|
|
416
|
+
this.canvas = canvasOf(box[2] - box[0], box[3] - box[1]);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
draw(ctx: CanvasRenderingContext2D, t: number, shift: Shift) {
|
|
420
|
+
const { img, region, canvas } = this;
|
|
421
|
+
const [w, h] = [canvas.width, canvas.height];
|
|
422
|
+
const c = canvas.getContext('2d')!;
|
|
423
|
+
c.globalCompositeOperation = 'source-over';
|
|
424
|
+
c.clearRect(0, 0, w, h);
|
|
425
|
+
const cw = Stream.COLUMN;
|
|
426
|
+
for (let x = 0, i = 0; x < w; x += cw, i++) {
|
|
427
|
+
const speed = 28 + 46 * rand(i, 31 + this.k);
|
|
428
|
+
const y = (t * speed + rand(i, 32 + this.k) * h) % h;
|
|
429
|
+
const sw = Math.min(cw, w - x);
|
|
430
|
+
c.drawImage(img, x, 0, sw, h, x, y, sw, h);
|
|
431
|
+
c.drawImage(img, x, 0, sw, h, x, y - h, sw, h);
|
|
432
|
+
}
|
|
433
|
+
// Only inside the painted rain, fading at its edges.
|
|
434
|
+
c.globalCompositeOperation = 'destination-in';
|
|
435
|
+
c.drawImage(region, 0, 0, w, h);
|
|
436
|
+
c.globalCompositeOperation = 'source-over';
|
|
437
|
+
const [sx, sy] = shift(this.box[3]);
|
|
438
|
+
ctx.drawImage(canvas, this.box[0] + sx, this.box[1] + sy);
|
|
439
|
+
}
|
|
440
|
+
}
|
package/app/web/main.ts
CHANGED
|
@@ -20,7 +20,7 @@ import { spokenLine, type SoundData } from '../src/play/sound.ts';
|
|
|
20
20
|
import type { Figure3D, ModelInfo } from './hero3d.ts';
|
|
21
21
|
import type { Weather } from '../src/play/weather.ts';
|
|
22
22
|
import { WeatherFx } from './weather.ts';
|
|
23
|
-
import { Clouds, Critter, drawSwell, Fall, Surf } from './living.ts';
|
|
23
|
+
import { Clouds, Critter, drawSwell, Fall, Stream, Surf } from './living.ts';
|
|
24
24
|
import { stormOf } from '../src/play/living.ts';
|
|
25
25
|
import type { Reactions } from '../src/play/reactions.ts';
|
|
26
26
|
import { ReactionFx } from './reactions.ts';
|
|
@@ -103,6 +103,8 @@ type Place = {
|
|
|
103
103
|
surf?: Surf;
|
|
104
104
|
/** Painted waterfalls pouring, painted animals swimming and flying, cloud bands drifting (see living.ts). */
|
|
105
105
|
falls?: Fall[];
|
|
106
|
+
/** Small things painted falling (code rain, snow, petals), column by column (see living.ts). */
|
|
107
|
+
streams?: Stream[];
|
|
106
108
|
critters?: Critter[];
|
|
107
109
|
clouds?: Clouds;
|
|
108
110
|
far?: { sky?: HTMLImageElement; flats: Array<{ img: HTMLImageElement; box: [number, number, number, number]; d: number }> };
|
|
@@ -492,12 +494,16 @@ class Game {
|
|
|
492
494
|
}
|
|
493
495
|
|
|
494
496
|
/** The waterfalls, animals and cloud bands a place's layers set moving. */
|
|
495
|
-
private async living(layers: Layers): Promise<Pick<Place, 'falls' | 'critters' | 'clouds'>> {
|
|
497
|
+
private async living(layers: Layers): Promise<Pick<Place, 'falls' | 'streams' | 'critters' | 'clouds'>> {
|
|
496
498
|
const img = (p: string) => loadImage(files(p)).catch(() => undefined);
|
|
497
499
|
const falls = (await Promise.all((layers.falls ?? []).map(async (f) => {
|
|
498
500
|
const i = await img(f.file);
|
|
499
501
|
return i && new Fall(i, f.box);
|
|
500
502
|
}))).filter((f): f is Fall => !!f);
|
|
503
|
+
const streams = (await Promise.all((layers.streams ?? []).map(async (f, k) => {
|
|
504
|
+
const [i, r] = await Promise.all([img(f.file), img(f.region)]);
|
|
505
|
+
return i && r && new Stream(i, r, f.box, k);
|
|
506
|
+
}))).filter((f): f is Stream => !!f);
|
|
501
507
|
const critters = (await Promise.all((layers.critters ?? []).map(async (c, k) => {
|
|
502
508
|
const i = await img(c.file);
|
|
503
509
|
return i && new Critter(i, c.box, c.medium, c.facing, k);
|
|
@@ -508,7 +514,7 @@ class Game {
|
|
|
508
514
|
const [clip, keep, ...bands] = await Promise.all([img(cl.clip), cl.keep ? img(cl.keep) : Promise.resolve(undefined), ...cl.bands.map((b) => img(b.file))]);
|
|
509
515
|
if (clip && bands.every(Boolean)) clouds = new Clouds(cl.bands.map((b, i) => ({ img: bands[i]!, box: b.box, speed: b.speed })), clip, keep, cl.box);
|
|
510
516
|
}
|
|
511
|
-
return { falls, critters, clouds };
|
|
517
|
+
return { falls, streams, critters, clouds };
|
|
512
518
|
}
|
|
513
519
|
|
|
514
520
|
/** A place, loaded once: a second ask while it loads (the preload, an NPC's walk) waits on the same files. */
|
|
@@ -1274,13 +1280,39 @@ class Game {
|
|
|
1274
1280
|
thingAt(x: number, y: number): Hot | undefined {
|
|
1275
1281
|
const g = this.place.g;
|
|
1276
1282
|
const gone = this.gone();
|
|
1283
|
+
const [sx0, sy0] = [x, y];
|
|
1277
1284
|
for (let i = this.drawn.length - 1; i >= 0; i--) {
|
|
1278
1285
|
const [x0, y0, x1, y1] = this.drawn[i].rect;
|
|
1279
1286
|
// The rect includes the shadow; trim it and the transparent margin at the sides.
|
|
1280
1287
|
const inset = (x1 - x0) * 0.12;
|
|
1281
|
-
if (x >= x0 + inset && x <= x1 - inset && y >= y0 && y <= y1 - 4)
|
|
1288
|
+
if (!(x >= x0 + inset && x <= x1 - inset && y >= y0 && y <= y1 - 4)) continue;
|
|
1289
|
+
// A person's rect is wider than the person: where nobody was drawn over a painted thing (a potion at a
|
|
1290
|
+
// scientist's feet), the thing is what is under the pointer.
|
|
1291
|
+
const under = this.drawnOver(sx0, sy0) === false ? this.paintedAt(sx0, sy0, gone) : undefined;
|
|
1292
|
+
return under?.mask ? under : this.drawn[i].hot;
|
|
1282
1293
|
}
|
|
1283
|
-
|
|
1294
|
+
return this.paintedAt(sx0, sy0, gone);
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
/**
|
|
1298
|
+
* Whether anyone was drawn over the frame point in the last frame: the canvas there differs from the place as
|
|
1299
|
+
* drawn without anyone (the depth snapshot). Undefined without a snapshot.
|
|
1300
|
+
*/
|
|
1301
|
+
private drawnOver(x: number, y: number): boolean | undefined {
|
|
1302
|
+
if (!this.snapped || !this.snap) return undefined;
|
|
1303
|
+
const { width: W, height: H } = this.place.g.frame;
|
|
1304
|
+
const z = this.zoom();
|
|
1305
|
+
const [cx, cy] = [Math.floor(((x - (this.viewX || W / 2)) * z + W / 2) * (canvas.width / W)), Math.floor(((y - (this.viewY || H / 2)) * z + H / 2) * (canvas.height / H))];
|
|
1306
|
+
if (cx < 0 || cy < 0 || cx >= canvas.width || cy >= canvas.height) return undefined;
|
|
1307
|
+
const a = ctx.getImageData(cx, cy, 1, 1).data;
|
|
1308
|
+
const b = this.snap.getContext('2d')!.getImageData(cx, cy, 1, 1).data;
|
|
1309
|
+
return Math.abs(a[0] - b[0]) + Math.abs(a[1] - b[1]) + Math.abs(a[2] - b[2]) > 24;
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
/** The painted thing (not a person) under a frame point, by its own mask where it has one. */
|
|
1313
|
+
private paintedAt(sx0: number, sy0: number, gone: Set<string>): Hot | undefined {
|
|
1314
|
+
const g = this.place.g;
|
|
1315
|
+
let [x, y] = [sx0, sy0];
|
|
1284
1316
|
for (const hot of this.hots()) {
|
|
1285
1317
|
if (this.drawn.some((d) => d.hot === hot)) continue;
|
|
1286
1318
|
if (gone.has(hot.thing.key)) continue;
|
|
@@ -1963,6 +1995,7 @@ class Game {
|
|
|
1963
1995
|
this.place.clouds?.draw(ctx, this.time, this.place.weather?.w.wind ?? 0, (y) => shiftAt(g, p, y));
|
|
1964
1996
|
this.drawWater(p);
|
|
1965
1997
|
for (const f of this.place.falls ?? []) f.draw(ctx, this.time, (y) => shiftAt(g, p, y));
|
|
1998
|
+
for (const f of this.place.streams ?? []) f.draw(ctx, this.time, (y) => shiftAt(g, p, y));
|
|
1966
1999
|
for (const c of this.place.critters ?? []) c.draw(ctx, this.time, (y) => shiftAt(g, p, y));
|
|
1967
2000
|
this.drawFloats(p);
|
|
1968
2001
|
this.drawSmoke(p);
|
|
@@ -3065,7 +3098,9 @@ class Game {
|
|
|
3065
3098
|
const y0 = Math.max(top, cut.y0);
|
|
3066
3099
|
const y1 = Math.min(bottom, cut.y0 + cut.canvas.height);
|
|
3067
3100
|
if (y1 <= y0) continue;
|
|
3068
|
-
|
|
3101
|
+
// Where the feet are, as seen from the card: a figure's box need not be centred on them.
|
|
3102
|
+
const feet = (feetX ?? (rect[0] + rect[2]) / 2) - sx;
|
|
3103
|
+
for (const [a, b] of hiddenRuns(cut.occ, left, right, feetY, support, feet)) {
|
|
3069
3104
|
const xa = Math.max(a, cut.x0);
|
|
3070
3105
|
const xb = Math.min(b, cut.x0 + cut.canvas.width);
|
|
3071
3106
|
if (xb > xa) this.drawCard(cut, xa, xb, y0, y1);
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Releasing
|
|
2
|
+
|
|
3
|
+
`CHANGELOG.md` decides every npm release of `@gtanczyk/diorama`; nobody runs `npm version` by hand.
|
|
4
|
+
|
|
5
|
+
1. With a change to shipped code (`app/bin`, `app/src`, `app/studio`, `app/web`), add one line under
|
|
6
|
+
`## Unreleased`, written for someone updating Diorama:
|
|
7
|
+
- `### Breaking`: a command, flag or world file layout changed incompatibly (minor before 1.0, major after)
|
|
8
|
+
- `### Added`: something new you can do (minor)
|
|
9
|
+
- `### Fixed`: something that was wrong now works (patch)
|
|
10
|
+
- `### Internal`: refactors, tests, tooling; never releases
|
|
11
|
+
2. Push to main. `release-pr.yaml` checks the changelog, cuts the next version on branch `release/next`
|
|
12
|
+
and opens (or refreshes) the PR **`release: vX.Y.Z`**. Later pushes with more lines update the same PR.
|
|
13
|
+
3. Merge the PR. `publish.yaml` sees `package.json` change, publishes to npm (trusted publishing, no
|
|
14
|
+
token) and creates the `vX.Y.Z` tag and a GitHub Release with that section as notes.
|
|
15
|
+
|
|
16
|
+
Hold a release by leaving the PR open. Set the repository variable `RELEASE_AUTOMERGE=true` to merge it
|
|
17
|
+
by itself, and the secret `RELEASE_TOKEN` (a PAT) if CI must run on the release PR. A version npm already holds is stepped past by patch and never published twice; Actions → Publish to npm → Run workflow
|
|
18
|
+
publishes the version in `package.json` by hand.
|
|
19
|
+
|
|
20
|
+
Locally: `node app/scripts/release.mjs next` (the next version, or none), `notes 0.2.0`, `check`
|
|
21
|
+
(changelog well formed and agreeing with package.json; `--files a,b` also asks for a changelog line
|
|
22
|
+
when those files ship).
|
package/docs/scenes.md
CHANGED
|
@@ -55,7 +55,7 @@ To change it, tell the Studio agent ("no rain in the hut", "it should pour", "wa
|
|
|
55
55
|
|
|
56
56
|
## Checking a world
|
|
57
57
|
|
|
58
|
-
`diorama build` ends with `verify`, which writes `verify.json` per place and prints which places are playable and what was flagged on the way. Places are built side by side and independently, and no place waits for another: each goes through its own steps and is told playable (`place.verdict`) as soon as its verify and free repairs are done, while the rest are still being made. The world's own steps start as soon as what they read is there (`app/src/world/schedule.ts`): the cast is painted from world.json while the places are, voices once the story is, an item's icon once its place is cut out, sprites once every place has its walking routes, sound once every place is painted. A place whose step breaks is set aside with why, and the others go on to the end, characters and sound included. Each model has a cap on requests open at once so the overlap does not run into rate limits (`DIORAMA_LANES="muse=8,spark=12,sam=16"`, the defaults). The report gives each error its playbook (`app/src/scene/playbook.ts`): where to look, the fix tools to try in order, and the costly last resort (a camera too close, `SCALE_NEAR` or a floor cut off once derive shut the front rows for it, is rerun derive first, then relayout and repaint); `place_status` hands the same to the Studio agent, which repairs one place at a time with the fix tools. The `masks` step also checks that each mask holds its thing: one Spark look per place at the painting with every mask box numbered (`app/src/scene/maskcheck.ts`), since a box and its mask can agree and both sit on the neighbour. When Spark points to where the thing really is, that is `MASK_WRONG` in verify (an error for any thing, props too). When it finds the thing nowhere and what it sees in the box is another kind of thing (hay where a horse should be), the thing was never painted: `THING_NOT_PAINTED`, an error whose play is to paint it in. A doubt about details only (a bronze bell for a brass one) is the flag `MASK_DOUBT`. Spark is asked to judge the kind of thing, not its details, and a box someone fixed is taken as given. Before anyone is asked, the build makes the cheap fixes itself (`app/src/scene/autofix.ts`): a gap of up to three 60 px cells with nothing solid in it, or up to six along stairs or a gangway (a walk-on thing) whose mask reaches both floors, is bridged with a floor brush stroke (two passes, as one bridge can bring the next patch within reach; stairs that lead somewhere else, with water between the floors, bridge nothing: that place needs a floor in the layout or a repaint, so it goes to the agent), a stand spot off the floor moves to the nearest reachable point (both without a model call), and a `MASK_WRONG` thing gets the box Spark saw it in (one SAM call). A card flagged `CARD_CUT` gets the cut part brushed onto its mask (a lantern on a boat's gunwale), so the card carries it and the backdrop is made again without it (once per cut). A `THING_NOT_PAINTED` thing left after them gets one Muse edit at its layout box (once per thing, kept as a repaint correction) and the place is rebuilt from paint. They are ordinary corrections, so `corrections_get` lists them and `corrections_clear` takes them back; `build.json` keeps them under `repairs`, and `--repair off` skips them. What the free fixes leave, the Studio hands to the agent at once: a build that ends with places unplayable wakes it with the places and their errors (`budget_set autoRepair:false` turns that off). It repairs with the fix tools in the playbook's order; after three fixes of a place in a row that leave it unplayable the fix tools refuse it and it tells you what it tried
|
|
58
|
+
`diorama build` ends with `verify`, which writes `verify.json` per place and prints which places are playable and what was flagged on the way. Places are built side by side and independently, and no place waits for another: each goes through its own steps and is told playable (`place.verdict`) as soon as its verify and free repairs are done, while the rest are still being made. The world's own steps start as soon as what they read is there (`app/src/world/schedule.ts`): the cast is painted from world.json while the places are, voices once the story is, an item's icon once its place is cut out, sprites once every place has its walking routes, sound once every place is painted. A place whose step breaks is set aside with why, and the others go on to the end, characters and sound included. Each model has a cap on requests open at once so the overlap does not run into rate limits (`DIORAMA_LANES="muse=8,spark=12,sam=16"`, the defaults). The report gives each error its playbook (`app/src/scene/playbook.ts`): where to look, the fix tools to try in order, and the costly last resort (a camera too close, `SCALE_NEAR` or a floor cut off once derive shut the front rows for it, is rerun derive first, then relayout and repaint); `place_status` hands the same to the Studio agent, which repairs one place at a time with the fix tools. The `masks` step also checks that each mask holds its thing: one Spark look per place at the painting with every mask box numbered (`app/src/scene/maskcheck.ts`), since a box and its mask can agree and both sit on the neighbour. When Spark points to where the thing really is, that is `MASK_WRONG` in verify (an error for any thing, props too). When it finds the thing nowhere and what it sees in the box is another kind of thing (hay where a horse should be), the thing was never painted: `THING_NOT_PAINTED`, an error whose play is to paint it in. A doubt about details only (a bronze bell for a brass one) is the flag `MASK_DOUBT`. Spark is asked to judge the kind of thing, not its details, and a box someone fixed is taken as given. Before anyone is asked, the build makes the cheap fixes itself (`app/src/scene/autofix.ts`): a gap of up to three 60 px cells with nothing solid in it, or up to six along stairs or a gangway (a walk-on thing) whose mask reaches both floors, is bridged with a floor brush stroke (two passes, as one bridge can bring the next patch within reach; stairs that lead somewhere else, with water between the floors, bridge nothing: that place needs a floor in the layout or a repaint, so it goes to the agent), a stand spot off the floor moves to the nearest reachable point (both without a model call), and a `MASK_WRONG` thing gets the box Spark saw it in (one SAM call). A card flagged `CARD_CUT` gets the cut part brushed onto its mask (a lantern on a boat's gunwale), so the card carries it and the backdrop is made again without it (once per cut). A `THING_NOT_PAINTED` thing left after them gets one Muse edit at its layout box (once per thing, kept as a repaint correction) and the place is rebuilt from paint. They are ordinary corrections, so `corrections_get` lists them and `corrections_clear` takes them back; `build.json` keeps them under `repairs`, and `--repair off` skips them. What the free fixes leave, the Studio hands to the agent at once: a build that ends with places unplayable wakes it with the places and their errors (`budget_set autoRepair:false` turns that off). It repairs with the fix tools in the playbook's order; after three fixes of a place in a row that leave it unplayable the fix tools refuse an exact repeat of a fix that failed and steer it to a different path (a mask brushed by hand, floor cells, taking a thing off), and after six they keep repaints and relayouts, which cost a Muse call, for you to approve; when the new path fails too it tells you briefly what it tried. `place_view` is how the agent looks, as a picture it sees (layer `walk`, `masks` or `painting`, all on the 60 px grid), so it has no need to dig through files; after three shell calls in a turn it is reminded of that once. Layer `masks` tints each thing's mask against the box it should be in and flags a mask that cut out a neighbour. Layer `walk` shows why a place cannot be walked: reached floor green, unreached red, each gap as a dashed line naming the 60 px cells to bridge (the ones `floor_cells` takes) and the things in it, and stand spots off the floor (`places/<place>/diagnose.png`; the agent gets it as an image). Every step also leaves a picture to look at: `masks/overlay.png` (what SAM found) and `walk.png` (where the hero can walk, footprints, floor lines and his size at a few depths).
|
|
59
59
|
|
|
60
60
|
`diorama explore <slug>` puts the hero on every part of each floor in the real play page and checks each frame; it needs Playwright with Chromium (`diorama browsers` downloads it once). `--fix` closes the floor where he looked wrong.
|
|
61
61
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gtanczyk/diorama",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Turn a story prompt into playable painted point-and-click scenes with Meta Muse and SAM 3.1",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"@meta-sam/parser": "^0.1.0",
|
|
55
55
|
"@preact/signals": "^2.11.3",
|
|
56
56
|
"esbuild": "^0.25.0",
|
|
57
|
-
"genaicode": "^2.
|
|
57
|
+
"genaicode": "^2.28.1",
|
|
58
58
|
"onnxruntime-web": "^1.31.1",
|
|
59
59
|
"playwright": "^1.63.0",
|
|
60
60
|
"preact": "^10.29.8",
|