@gtanczyk/diorama 0.0.0-stage → 0.2.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.
Files changed (302) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +157 -2
  3. package/app/bin/diorama.mjs +13 -0
  4. package/app/src/actions/budget.ts +160 -0
  5. package/app/src/actions/build.ts +158 -0
  6. package/app/src/actions/cast.ts +267 -0
  7. package/app/src/actions/depth.ts +95 -0
  8. package/app/src/actions/extend.ts +36 -0
  9. package/app/src/actions/faces.ts +160 -0
  10. package/app/src/actions/feedback.ts +87 -0
  11. package/app/src/actions/fix.ts +382 -0
  12. package/app/src/actions/history.ts +93 -0
  13. package/app/src/actions/idea-preview.ts +65 -0
  14. package/app/src/actions/idea-review.ts +102 -0
  15. package/app/src/actions/idea-sketch.ts +172 -0
  16. package/app/src/actions/index.ts +32 -0
  17. package/app/src/actions/items.ts +94 -0
  18. package/app/src/actions/kids.ts +215 -0
  19. package/app/src/actions/language.ts +52 -0
  20. package/app/src/actions/models.ts +64 -0
  21. package/app/src/actions/motion.ts +119 -0
  22. package/app/src/actions/pictures.ts +49 -0
  23. package/app/src/actions/reactions.ts +36 -0
  24. package/app/src/actions/registry.ts +101 -0
  25. package/app/src/actions/share.ts +32 -0
  26. package/app/src/actions/sound.ts +73 -0
  27. package/app/src/actions/start.ts +154 -0
  28. package/app/src/actions/step-work.ts +150 -0
  29. package/app/src/actions/story.ts +101 -0
  30. package/app/src/actions/studio.ts +133 -0
  31. package/app/src/actions/style.ts +72 -0
  32. package/app/src/actions/video.ts +46 -0
  33. package/app/src/actions/view.ts +109 -0
  34. package/app/src/actions/weather.ts +38 -0
  35. package/app/src/actions/world.ts +290 -0
  36. package/app/src/commands/action.ts +39 -0
  37. package/app/src/commands/backdrop.ts +188 -0
  38. package/app/src/commands/browsers.ts +21 -0
  39. package/app/src/commands/build.ts +503 -0
  40. package/app/src/commands/cast.ts +161 -0
  41. package/app/src/commands/clip.ts +431 -0
  42. package/app/src/commands/derive.ts +167 -0
  43. package/app/src/commands/doctor.ts +74 -0
  44. package/app/src/commands/explore.ts +123 -0
  45. package/app/src/commands/export.ts +158 -0
  46. package/app/src/commands/extend.ts +75 -0
  47. package/app/src/commands/faces.ts +80 -0
  48. package/app/src/commands/icons.ts +69 -0
  49. package/app/src/commands/index.ts +37 -0
  50. package/app/src/commands/language.ts +87 -0
  51. package/app/src/commands/layout.ts +31 -0
  52. package/app/src/commands/masks.ts +117 -0
  53. package/app/src/commands/mcp.ts +52 -0
  54. package/app/src/commands/model.ts +284 -0
  55. package/app/src/commands/paint.ts +182 -0
  56. package/app/src/commands/plates.ts +97 -0
  57. package/app/src/commands/play.ts +195 -0
  58. package/app/src/commands/playthrough.ts +111 -0
  59. package/app/src/commands/publish.ts +78 -0
  60. package/app/src/commands/reactions.ts +40 -0
  61. package/app/src/commands/reel.ts +46 -0
  62. package/app/src/commands/setup.ts +85 -0
  63. package/app/src/commands/sound.ts +100 -0
  64. package/app/src/commands/sprites.ts +232 -0
  65. package/app/src/commands/story.ts +37 -0
  66. package/app/src/commands/studio.ts +65 -0
  67. package/app/src/commands/style.ts +87 -0
  68. package/app/src/commands/verify.ts +45 -0
  69. package/app/src/commands/voice-retry.ts +14 -0
  70. package/app/src/commands/voices.ts +145 -0
  71. package/app/src/commands/weather.ts +52 -0
  72. package/app/src/commands/world.ts +104 -0
  73. package/app/src/film/clip.ts +386 -0
  74. package/app/src/film/reel.ts +126 -0
  75. package/app/src/film/screencast.ts +123 -0
  76. package/app/src/image/mask.ts +29 -0
  77. package/app/src/image/overlay.ts +38 -0
  78. package/app/src/image/raster.ts +224 -0
  79. package/app/src/main.ts +28 -0
  80. package/app/src/meta/choice.ts +27 -0
  81. package/app/src/meta/http.ts +99 -0
  82. package/app/src/meta/memo.ts +105 -0
  83. package/app/src/meta/muse.ts +70 -0
  84. package/app/src/meta/openrouter.ts +93 -0
  85. package/app/src/meta/prices.ts +25 -0
  86. package/app/src/meta/sam.ts +83 -0
  87. package/app/src/meta/schema.ts +63 -0
  88. package/app/src/meta/spark.ts +220 -0
  89. package/app/src/meta/step.ts +14 -0
  90. package/app/src/meta/voice-file.ts +78 -0
  91. package/app/src/meta/voice.ts +103 -0
  92. package/app/src/model/animation.ts +38 -0
  93. package/app/src/model/blender/relax.py +284 -0
  94. package/app/src/model/blender.ts +115 -0
  95. package/app/src/model/build.ts +50 -0
  96. package/app/src/model/face.ts +21 -0
  97. package/app/src/model/fit.ts +181 -0
  98. package/app/src/model/glb.ts +241 -0
  99. package/app/src/model/plan.ts +151 -0
  100. package/app/src/model/quality.ts +48 -0
  101. package/app/src/model/space.ts +168 -0
  102. package/app/src/model/sprites.ts +293 -0
  103. package/app/src/model/views.ts +163 -0
  104. package/app/src/play/creature.ts +43 -0
  105. package/app/src/play/depth-occlusion.ts +150 -0
  106. package/app/src/play/exit-outline.ts +48 -0
  107. package/app/src/play/face3d.ts +34 -0
  108. package/app/src/play/focus.ts +78 -0
  109. package/app/src/play/geometry.ts +272 -0
  110. package/app/src/play/gestures.ts +23 -0
  111. package/app/src/play/glow.ts +187 -0
  112. package/app/src/play/lipsync.ts +69 -0
  113. package/app/src/play/living.ts +83 -0
  114. package/app/src/play/outline-path.ts +130 -0
  115. package/app/src/play/parallax.ts +200 -0
  116. package/app/src/play/path.ts +262 -0
  117. package/app/src/play/preview.ts +71 -0
  118. package/app/src/play/reactions.ts +92 -0
  119. package/app/src/play/rig.ts +172 -0
  120. package/app/src/play/shadow.ts +253 -0
  121. package/app/src/play/sound.ts +51 -0
  122. package/app/src/play/weather.ts +58 -0
  123. package/app/src/qa/explore.ts +136 -0
  124. package/app/src/qa/probe.ts +139 -0
  125. package/app/src/qa/route.ts +118 -0
  126. package/app/src/registry.ts +23 -0
  127. package/app/src/scene/animation-source.ts +36 -0
  128. package/app/src/scene/assets.ts +37 -0
  129. package/app/src/scene/autofix.ts +133 -0
  130. package/app/src/scene/backdrop.ts +326 -0
  131. package/app/src/scene/blockin.ts +189 -0
  132. package/app/src/scene/brief.ts +74 -0
  133. package/app/src/scene/carry.ts +177 -0
  134. package/app/src/scene/cast-fixes.ts +33 -0
  135. package/app/src/scene/cast.ts +199 -0
  136. package/app/src/scene/corrections.ts +164 -0
  137. package/app/src/scene/critic.ts +169 -0
  138. package/app/src/scene/depthmap.ts +149 -0
  139. package/app/src/scene/derive.ts +475 -0
  140. package/app/src/scene/diagnose.ts +132 -0
  141. package/app/src/scene/face-calibration.ts +65 -0
  142. package/app/src/scene/face-registration.ts +21 -0
  143. package/app/src/scene/face-work.ts +71 -0
  144. package/app/src/scene/faces-data.ts +48 -0
  145. package/app/src/scene/faces-settings.ts +20 -0
  146. package/app/src/scene/faces.ts +435 -0
  147. package/app/src/scene/glows.ts +153 -0
  148. package/app/src/scene/held.ts +109 -0
  149. package/app/src/scene/icons.ts +185 -0
  150. package/app/src/scene/layers.ts +822 -0
  151. package/app/src/scene/layout.ts +216 -0
  152. package/app/src/scene/locate.ts +118 -0
  153. package/app/src/scene/maskcheck.ts +43 -0
  154. package/app/src/scene/masks.ts +392 -0
  155. package/app/src/scene/occluders.ts +123 -0
  156. package/app/src/scene/occlusion-check.ts +138 -0
  157. package/app/src/scene/paint-layout.ts +135 -0
  158. package/app/src/scene/paint.ts +81 -0
  159. package/app/src/scene/photo.ts +123 -0
  160. package/app/src/scene/plates.ts +256 -0
  161. package/app/src/scene/playbook.ts +121 -0
  162. package/app/src/scene/preview.ts +23 -0
  163. package/app/src/scene/reactions.ts +118 -0
  164. package/app/src/scene/restyle.ts +53 -0
  165. package/app/src/scene/rig-check.ts +68 -0
  166. package/app/src/scene/rig.ts +379 -0
  167. package/app/src/scene/scale.ts +148 -0
  168. package/app/src/scene/shadows.ts +215 -0
  169. package/app/src/scene/stairs.ts +183 -0
  170. package/app/src/scene/status.ts +32 -0
  171. package/app/src/scene/verify.ts +156 -0
  172. package/app/src/scene/walk.ts +326 -0
  173. package/app/src/scene/warp.ts +419 -0
  174. package/app/src/scene/weather.ts +118 -0
  175. package/app/src/scene/words.ts +65 -0
  176. package/app/src/secrets.ts +219 -0
  177. package/app/src/setup/checks.ts +110 -0
  178. package/app/src/sound/cost.ts +74 -0
  179. package/app/src/sound/data.ts +89 -0
  180. package/app/src/sound/fixes.ts +23 -0
  181. package/app/src/sound/issues.ts +21 -0
  182. package/app/src/sound/language.ts +11 -0
  183. package/app/src/sound/lipsync-make.ts +98 -0
  184. package/app/src/sound/lipsync.ts +24 -0
  185. package/app/src/sound/make.ts +229 -0
  186. package/app/src/sound/musicgen.py +20 -0
  187. package/app/src/sound/pcm.ts +280 -0
  188. package/app/src/sound/plan.ts +154 -0
  189. package/app/src/sound/retry.ts +43 -0
  190. package/app/src/sound/settings.ts +28 -0
  191. package/app/src/sound/sources.ts +81 -0
  192. package/app/src/sound/spoken.ts +70 -0
  193. package/app/src/sound/voices.ts +579 -0
  194. package/app/src/speech/transcribe.ts +38 -0
  195. package/app/src/speech/types.ts +18 -0
  196. package/app/src/story/check.ts +230 -0
  197. package/app/src/story/design.ts +108 -0
  198. package/app/src/story/engine.ts +233 -0
  199. package/app/src/story/fixes.ts +54 -0
  200. package/app/src/story/npc-guidance.ts +7 -0
  201. package/app/src/story/phrases.ts +4 -0
  202. package/app/src/story/schema.ts +92 -0
  203. package/app/src/studio/agent.ts +555 -0
  204. package/app/src/studio/discovery.ts +28 -0
  205. package/app/src/studio/events.ts +48 -0
  206. package/app/src/studio/history.ts +124 -0
  207. package/app/src/studio/jobs.ts +291 -0
  208. package/app/src/studio/locks.ts +33 -0
  209. package/app/src/studio/mcp.ts +86 -0
  210. package/app/src/studio/operation.ts +18 -0
  211. package/app/src/studio/repair.ts +31 -0
  212. package/app/src/studio/server.ts +423 -0
  213. package/app/src/studio/trouble.ts +48 -0
  214. package/app/src/world/design.ts +72 -0
  215. package/app/src/world/drawings.ts +198 -0
  216. package/app/src/world/extend-stage.ts +109 -0
  217. package/app/src/world/extend.ts +123 -0
  218. package/app/src/world/extension-progress.ts +10 -0
  219. package/app/src/world/files.ts +62 -0
  220. package/app/src/world/idea-review.ts +32 -0
  221. package/app/src/world/language.ts +116 -0
  222. package/app/src/world/photo-limits.ts +2 -0
  223. package/app/src/world/pool.ts +41 -0
  224. package/app/src/world/schedule.ts +64 -0
  225. package/app/src/world/schema.ts +74 -0
  226. package/app/src/world/sketch.ts +81 -0
  227. package/app/src/world/style-plan.ts +90 -0
  228. package/app/src/world/style-reference.ts +16 -0
  229. package/app/src/world/styled.ts +51 -0
  230. package/app/src/world/told.ts +105 -0
  231. package/app/src/world/video-limits.ts +3 -0
  232. package/app/src/world/video.ts +98 -0
  233. package/app/studio/activity.tsx +124 -0
  234. package/app/studio/agent.tsx +384 -0
  235. package/app/studio/api.ts +111 -0
  236. package/app/studio/atelier.tsx +234 -0
  237. package/app/studio/audio-missing.tsx +25 -0
  238. package/app/studio/audio-setup.tsx +33 -0
  239. package/app/studio/blender-setup.tsx +30 -0
  240. package/app/studio/build.tsx +679 -0
  241. package/app/studio/camera.tsx +59 -0
  242. package/app/studio/cast-work.tsx +207 -0
  243. package/app/studio/cast.tsx +178 -0
  244. package/app/studio/concept-edit.tsx +47 -0
  245. package/app/studio/creator-idea.tsx +46 -0
  246. package/app/studio/custom-look.tsx +36 -0
  247. package/app/studio/faces-setup.tsx +28 -0
  248. package/app/studio/favicon.svg +11 -0
  249. package/app/studio/figure.tsx +314 -0
  250. package/app/studio/fonts/OFL.txt +93 -0
  251. package/app/studio/fonts/fonts.css +7 -0
  252. package/app/studio/fonts/fraunces-italic-latin-ext.woff2 +0 -0
  253. package/app/studio/fonts/fraunces-italic-latin.woff2 +0 -0
  254. package/app/studio/fonts/fraunces-normal-latin-ext.woff2 +0 -0
  255. package/app/studio/fonts/fraunces-normal-latin.woff2 +0 -0
  256. package/app/studio/fonts/instrument-sans-normal-latin-ext.woff2 +0 -0
  257. package/app/studio/fonts/instrument-sans-normal-latin.woff2 +0 -0
  258. package/app/studio/icons.tsx +42 -0
  259. package/app/studio/idea-card.tsx +42 -0
  260. package/app/studio/idea-review.tsx +181 -0
  261. package/app/studio/idea-storyboard.tsx +134 -0
  262. package/app/studio/index.html +15 -0
  263. package/app/studio/job-words.ts +33 -0
  264. package/app/studio/lightbox.tsx +49 -0
  265. package/app/studio/main.tsx +1329 -0
  266. package/app/studio/nav.ts +14 -0
  267. package/app/studio/place.tsx +264 -0
  268. package/app/studio/review-sketch.tsx +47 -0
  269. package/app/studio/say-it.tsx +49 -0
  270. package/app/studio/step-work.tsx +134 -0
  271. package/app/studio/story.tsx +394 -0
  272. package/app/studio/style-menu.tsx +81 -0
  273. package/app/studio/style.css +1390 -0
  274. package/app/studio/sure.tsx +54 -0
  275. package/app/studio/tip.tsx +74 -0
  276. package/app/studio/tsconfig.json +5 -0
  277. package/app/studio/video.tsx +73 -0
  278. package/app/studio/world.tsx +188 -0
  279. package/app/tsconfig.json +13 -0
  280. package/app/web/audio.ts +315 -0
  281. package/app/web/face.ts +131 -0
  282. package/app/web/face3d.ts +35 -0
  283. package/app/web/glows.ts +387 -0
  284. package/app/web/hero3d.ts +728 -0
  285. package/app/web/index.html +118 -0
  286. package/app/web/living.ts +404 -0
  287. package/app/web/main.ts +3769 -0
  288. package/app/web/pad.ts +174 -0
  289. package/app/web/reactions.ts +79 -0
  290. package/app/web/sheet.ts +36 -0
  291. package/app/web/synth.ts +140 -0
  292. package/app/web/tsconfig.json +5 -0
  293. package/app/web/ui.ts +263 -0
  294. package/app/web/weather.ts +306 -0
  295. package/app/web/words.ts +12 -0
  296. package/docs/characters.md +58 -0
  297. package/docs/npc-motion.md +69 -0
  298. package/docs/scenes.md +75 -0
  299. package/docs/setup.md +60 -0
  300. package/docs/sound.md +23 -0
  301. package/docs/studio.md +62 -0
  302. package/package.json +70 -4
@@ -0,0 +1,80 @@
1
+ import fs from 'node:fs';
2
+ import { castLanes, mapLimit } from '../world/pool.ts';
3
+ import { CalibrationTarget, setFaceCalibration } from '../scene/face-calibration.ts';
4
+ import { defineCommand, takeOption } from '../registry.ts';
5
+ import { WorldFiles } from '../world/files.ts';
6
+ import { players } from '../world/drawings.ts';
7
+ import { prepareFace, repairFaceFrame, calibrateFaceFrame } from '../scene/faces.ts';
8
+ import { registeredFace } from '../scene/faces-data.ts';
9
+ import { FacePreparationError, LipBox, MouthName } from '../scene/face-work.ts';
10
+ import { prepareLipSync } from '../sound/lipsync-make.ts';
11
+
12
+ export function faceCharacters(files: WorldFiles): string[] {
13
+ const world = files.world();
14
+ return [...new Set([world.hero.key, ...world.places.flatMap(p => p.things.filter(t => t.role === 'npc').map(t => t.key)), ...players(files).map(p => p.key)])];
15
+ }
16
+
17
+ defineCommand({
18
+ name: 'faces',
19
+ usage: 'faces <slug> [key...] prepare speaking faces for paintings, 2D rigs and talk sprites, and cached audio lip-sync [--audio-only] [--image-only] [--force] [--take N] [--rhubarb PATH]',
20
+ async run(args) {
21
+ let take = Number(takeOption(args, 'take', '0'));
22
+ let repairWorkId: string | undefined;
23
+ const executable = takeOption(args, 'rhubarb', '');
24
+ const repairId=takeOption(args,'repair-frame',''), calibrateId=takeOption(args,'calibrate-frame','');
25
+ const revision=takeOption(args,'revision',''), mouth=takeOption(args,'mouth',''), hint=takeOption(args,'hint','');
26
+ const box=takeOption(args,'box','');
27
+ const calibration=takeOption(args,'calibration',''), imageHash=takeOption(args,'image-hash',''), open=takeOption(args,'open','');
28
+ const flag = (name: string) => { const i = args.indexOf(`--${name}`); return i >= 0 ? (args.splice(i, 1), true) : false; };
29
+ const audioOnly = flag('audio-only'), imageOnly = flag('image-only'), force = flag('force');
30
+ const [slug, ...only] = args;
31
+ if (!slug) throw new Error('usage: diorama faces <slug> [key...]');
32
+ const files = new WorldFiles(slug), keys = faceCharacters(files);
33
+ for (const key of only) if (!keys.includes(key)) throw new Error(`${slug} has no character ${key}`);
34
+ if(calibration) {
35
+ if(only.length!==1 || repairId || calibrateId || !['true','false'].includes(open))throw new Error('Choose one inspected calibration');
36
+ await setFaceCalibration(files,only[0],CalibrationTarget.parse(JSON.parse(calibration)),revision,imageHash,LipBox.parse(JSON.parse(box)),open==='true');
37
+ }
38
+ if(repairId || calibrateId) {
39
+ if(only.length!==1 || repairId && calibrateId) throw new Error('Choose one character and one face correction');
40
+ const work = repairId ? await repairFaceFrame(files,only[0],repairId,revision,MouthName.parse(mouth),hint)
41
+ : calibrateFaceFrame(files,only[0],calibrateId,revision,MouthName.parse(mouth),LipBox.parse(JSON.parse(box)));
42
+ take = work.take; repairWorkId = work.id;
43
+ }
44
+ const statusFile = files.path('face-preparation.json');
45
+ const status = fs.existsSync(statusFile) ? files.readJson(statusFile) as Record<string, { face?: string | null; audio?: string[] }> : {};
46
+ for (const key of keys) if (!status[key]) {
47
+ const attempt=files.path('cast',key,'face-attempt.json');
48
+ if(fs.existsSync(attempt)) {
49
+ const previous=files.readJson(attempt) as {state?:string;reason?:string};
50
+ status[key]=previous.state==='failed'&&!registeredFace(files,key)?.info.imagePose?.lips?{face:previous.reason??'Face preparation failed'}:previous.state?{face:null}:{};
51
+ }
52
+ }
53
+ const failures: string[] = [];
54
+ // Each character's face is its own work (its own files under cast/<key>): a few side by side.
55
+ if (!audioOnly) await mapLimit(keys.filter(k => !only.length || only.includes(k)), castLanes(), async (key) => {
56
+ try { await prepareFace(files, key, { take, force, imageOnly, repairWorkId }); (status[key] ??= {}).face = null; }
57
+ catch (e) {
58
+ const reason = e instanceof Error ? e.message : String(e);
59
+ files.write(files.path('cast', key, 'face-attempt.json'), { at: new Date().toISOString(), state: 'failed', take, reason });
60
+ console.log(`${key}: face preparation failed; existing face retained: ${reason}`);
61
+ // A retry (new talk sheets, a new take) that fails leaves the working face in place: the character
62
+ // still speaks with moving lips, so the build is not short of faces. Asked for by name, it is reported.
63
+ if (registeredFace(files, key)?.info.imagePose?.lips && !only.length) { (status[key] ??= {}).face = null; return; }
64
+ failures.push(`${key}: ${reason}`);
65
+ (status[key] ??= {}).face = reason;
66
+ }
67
+ });
68
+ files.write(statusFile, status);
69
+ if (fs.existsSync(files.path('voices.json'))) {
70
+ const audio = await prepareLipSync(files, only, executable || undefined);
71
+ for (const key of [...new Set([...keys, ...Object.keys(status)])].filter(k => !only.length || only.includes(k))) delete (status[key] ??= {}).audio;
72
+ for (const failure of audio.failures ?? []) {
73
+ failures.push(`${failure.speaker}: ${failure.reason}`);
74
+ ((status[failure.speaker] ??= {}).audio ??= []).push(failure.reason);
75
+ }
76
+ files.write(statusFile, status);
77
+ }
78
+ if (failures.length) throw new FacePreparationError(failures);
79
+ },
80
+ });
@@ -0,0 +1,69 @@
1
+ // diorama icons <slug> [item...]: items/<key>.png for every item in story.json, and items/items.json saying how each was made.
2
+ import fs from 'node:fs';
3
+ import sharp from 'sharp';
4
+ import { maskFromPng } from '../image/mask.ts';
5
+ import { maskBox } from '../image/raster.ts';
6
+ import { defineCommand } from '../registry.ts';
7
+ import { cutIcon, itemSources, ItemsInfo, MIN_CUT_PX, paintIcon, readIconFixes, redrawIcon } from '../scene/icons.ts';
8
+ import { Story } from '../story/schema.ts';
9
+ import { WorldFiles } from '../world/files.ts';
10
+
11
+ defineCommand({
12
+ name: 'icons',
13
+ usage: 'icons <slug> [item...] an inventory icon per story item: cut from the painting where the hero picks it up, else painted by Muse (items/fixes.json: how, hint, take)',
14
+ async run([slug, ...only]) {
15
+ if (!slug) throw new Error('usage: diorama icons <slug> [item...]');
16
+ const files = new WorldFiles(slug);
17
+ if (!fs.existsSync(files.path('story.json'))) {
18
+ console.log('no story.json: no items, no icons');
19
+ return;
20
+ }
21
+ const world = files.world();
22
+ const story = Story.parse(files.readJson(files.path('story.json')));
23
+ const sources = itemSources(story);
24
+ const fixes = readIconFixes(files);
25
+ const infoFile = files.path('items', 'items.json');
26
+ const info: ItemsInfo = fs.existsSync(infoFile) ? ItemsInfo.parse(files.readJson(infoFile)) : {};
27
+ // Items the story no longer has lose their icons.
28
+ for (const key of Object.keys(info)) if (!sources[key] && !story.items.some((i) => i.key === key)) delete info[key];
29
+ const items = story.items.filter((i) => only.length === 0 || only.includes(i.key));
30
+ const failed: string[] = [];
31
+ // An item without an icon shows its name in the bag, so one failure does not stop the build.
32
+ await Promise.all(items.map(async (item) => { try {
33
+ const from = sources[item.key];
34
+ const fix = fixes[item.key] ?? {};
35
+ let png: Buffer;
36
+ let how: ItemsInfo[string]['how'];
37
+ const maskFile = from && files.place(from.place, 'masks', `${from.thing}.png`);
38
+ if (from && maskFile && fs.existsSync(maskFile) && fix.how !== 'paint') {
39
+ const painting = fs.readFileSync(files.place(from.place, 'painting.png'));
40
+ const mask = await maskFromPng(fs.readFileSync(maskFile));
41
+ const box = maskBox(mask);
42
+ const side = box ? Math.max(box[2] - box[0], box[3] - box[1]) : 0;
43
+ // A hint from the person means the picture itself should change, so Muse redraws even a large cut-out.
44
+ if (fix.how === 'cut' || (side >= MIN_CUT_PX && !fix.hint)) {
45
+ png = await cutIcon(painting, mask);
46
+ how = 'cut';
47
+ } else {
48
+ // Too small to blow up, or the person asked for changes: Muse redraws the cut-out.
49
+ const crop = await sharp(await cutIcon(painting, mask, 12)).flatten({ background: '#f3ead8' }).png().toBuffer();
50
+ png = await redrawIcon(files.memo, world, item, crop, fix.hint, fix.take);
51
+ how = 'redraw';
52
+ }
53
+ } else {
54
+ if (from) console.log(` ${item.key}: ${from.place}/${from.thing} has no mask, Muse paints it`);
55
+ png = await paintIcon(files.memo, world, item, fix.hint, fix.take);
56
+ how = 'paint';
57
+ }
58
+ files.write(files.path('items', `${item.key}.png`), png);
59
+ info[item.key] = { icon: `items/${item.key}.png`, how, from };
60
+ console.log(`${item.key}: ${how}${from ? ` from ${from.place}/${from.thing}` : ''}`);
61
+ } catch (error) {
62
+ failed.push(item.key);
63
+ console.log(` ${item.key}: no icon (${error instanceof Error ? error.message : error}); the bag shows its name. icon_redo with a hint tries again`);
64
+ } }));
65
+ files.write(infoFile, info);
66
+ if (failed.length) console.log(`no icon for: ${failed.join(', ')}`);
67
+ console.log(`calls sent: muse ${files.memo.sent.muse}, sam ${files.memo.sent.sam}`);
68
+ },
69
+ });
@@ -0,0 +1,37 @@
1
+ // Command modules; each one calls defineCommand.
2
+ import './doctor.ts';
3
+ import './world.ts';
4
+ import './extend.ts';
5
+ import './story.ts';
6
+ import './playthrough.ts';
7
+ import './layout.ts';
8
+ import './paint.ts';
9
+ import './masks.ts';
10
+ import './plates.ts';
11
+ import './icons.ts';
12
+ import './derive.ts';
13
+ import './backdrop.ts';
14
+ import './weather.ts';
15
+ import './reactions.ts';
16
+ import './sound.ts';
17
+ import './voices.ts';
18
+ import './faces.ts';
19
+ import './voice-retry.ts';
20
+ import './verify.ts';
21
+ import './explore.ts';
22
+ import './build.ts';
23
+ import './cast.ts';
24
+ import './model.ts';
25
+ import './sprites.ts';
26
+ import './style.ts';
27
+ import './language.ts';
28
+ import './play.ts';
29
+ import './export.ts';
30
+ import './publish.ts';
31
+ import './reel.ts';
32
+ import './clip.ts';
33
+ import './setup.ts';
34
+ import './browsers.ts';
35
+ import './action.ts';
36
+ import './studio.ts';
37
+ import './mcp.ts';
@@ -0,0 +1,87 @@
1
+ // diorama language <slug> <Language>: the world told in another language. Title, pitch, names and every line of
2
+ // the story are translated in one Spark pass; the paintings, masks, sounds and puzzles stay as they are. Then the
3
+ // voices are cast again for the new language (no accent of the old one) and every spoken line is read again,
4
+ // and the talking faces follow the new takes.
5
+ import fs from 'node:fs';
6
+ import { defineCommand, registry } from '../registry.ts';
7
+ import { castFixesFile, readCastFixes } from '../scene/cast-fixes.ts';
8
+ import { SoundPlan } from '../sound/plan.ts';
9
+ import { readStoryFixes, StoryFixes, storyFixesFile, textAt } from '../story/fixes.ts';
10
+ import { Story } from '../story/schema.ts';
11
+ import { WorldFiles } from '../world/files.ts';
12
+ import { applyWorldTexts, languagePlan, planLine, relanguage, translateTexts, worldTexts } from '../world/language.ts';
13
+ import { placeSources, soundPlanFrom } from './sound.ts';
14
+
15
+ const run = (name: string, args: string[]) => registry.get(name)!.run(args);
16
+
17
+ defineCommand({
18
+ name: 'language',
19
+ usage: 'language <slug> <Language> tell the world in another language ("English", "Polish"): title, pitch, names and the story translated in one Spark pass, then the voices cast again for that language and every spoken line read again [--text-only: leave the voices for later] [--estimate: only say what it redoes and costs]',
20
+ async run(args) {
21
+ const estimate = args.includes('--estimate');
22
+ const textOnly = args.includes('--text-only');
23
+ const [slug, language] = args.filter((a) => !a.startsWith('--'));
24
+ if (!slug || !language) throw new Error('usage: diorama language <slug> <Language> [--text-only] [--estimate]');
25
+ const files = new WorldFiles(slug);
26
+ const world = files.world();
27
+ const storyFile = files.path('story.json');
28
+ const story = fs.existsSync(storyFile) ? Story.parse(files.readJson(storyFile)) : null;
29
+ const plan = languagePlan(files, world, story, language);
30
+ console.log(`language ${slug}: ${planLine(plan)}`);
31
+ if (estimate) return;
32
+
33
+ if (!plan.same) {
34
+ const texts = worldTexts(world, story);
35
+ const kids = world.audience === 'kids' || fs.existsSync(files.path('drawings'));
36
+ console.log(`translating ${Object.keys(texts).length} texts from ${plan.from} to ${plan.to}`);
37
+ const translated = await translateTexts(files.memo, texts, plan.from, plan.to, kids);
38
+ const now = applyWorldTexts(world, story, translated, plan.to);
39
+ // sound.json is planned from the world and story: the words changed, not what makes a sound, so the plan stays.
40
+ const soundFile = files.path('sound.json');
41
+ const sound = fs.existsSync(soundFile) ? SoundPlan.safeParse(files.readJson(soundFile)) : undefined;
42
+ const sources = sound?.success ? placeSources(files, world) : undefined;
43
+ const soundWasCurrent = sound?.success && sources && sound.data.from === soundPlanFrom(world, story, sources);
44
+
45
+ files.write(files.path('world.json'), { ...JSON.parse(fs.readFileSync(files.path('world.json'), 'utf8')), ...now.world, ...(now.world.language ? {} : { language: undefined }) });
46
+ if (now.story) {
47
+ files.write(storyFile, now.story);
48
+ // The person's reworded lines are kept across rebuilds: they are reworded in the new language too.
49
+ const fixes = readStoryFixes(files);
50
+ if (Object.keys(fixes.texts).length) {
51
+ for (const path of Object.keys(fixes.texts)) {
52
+ const text = textAt(now.story, path);
53
+ if (text) fixes.texts[path] = { ...fixes.texts[path], text };
54
+ }
55
+ files.write(storyFixesFile(files), StoryFixes.parse(fixes));
56
+ }
57
+ }
58
+ // A voice the person described for the old language ("Native Polish speaker") is asked for in the new one.
59
+ const castFixes = readCastFixes(files);
60
+ let castChanged = false;
61
+ for (const fix of Object.values(castFixes)) {
62
+ if (fix.voice?.design && fix.voice.design !== relanguage(fix.voice.design, plan.from, plan.to)) {
63
+ fix.voice.design = relanguage(fix.voice.design, plan.from, plan.to);
64
+ castChanged = true;
65
+ }
66
+ }
67
+ if (castChanged) files.write(castFixesFile(files), castFixes);
68
+ if (soundWasCurrent && sound?.success && sources) files.write(soundFile, { ...sound.data, from: soundPlanFrom(files.world(), now.story, sources) });
69
+ for (const id of ['world.title', 'world.pitch']) console.log(` ${id}: ${translated[id]}`);
70
+ for (const [id, text] of Object.entries(translated).filter(([id]) => id.startsWith('names.'))) console.log(` ${id.slice(6)}: ${texts[id]} -> ${text}`);
71
+ console.log(`world.json and story.json are in ${plan.to} now`);
72
+ }
73
+ if (textOnly) return void console.log('voices left for later: run_step voices, then faces');
74
+ if (!story) return;
75
+ console.log('\n# voices');
76
+ await run('voices', [slug]);
77
+ if (fs.existsSync(files.path('cast'))) {
78
+ console.log('\n# faces');
79
+ try {
80
+ await run('build', [slug, '--from', 'faces', '--to', 'faces']);
81
+ } catch (error) {
82
+ // Talking mouths are polish: the voices are in, the game plays without them.
83
+ console.log(`faces not made again (${error instanceof Error ? error.message : error}): run_step faces later`);
84
+ }
85
+ }
86
+ },
87
+ });
@@ -0,0 +1,31 @@
1
+ // diorama layout <slug> [place...]: layout.json, blockin.png (for Muse) and plan.png (for people) for each place.
2
+ import fs from 'node:fs';
3
+ import { renderBlockin, renderPlan } from '../scene/blockin.ts';
4
+ import { readCorrections } from '../scene/corrections.ts';
5
+ import { layOut } from '../scene/layout.ts';
6
+ import { defineCommand } from '../registry.ts';
7
+ import { WorldFiles } from '../world/files.ts';
8
+ import { approvedConcept } from '../world/sketch.ts';
9
+
10
+ defineCommand({
11
+ name: 'layout',
12
+ usage: 'layout <slug> [place...] lay out places (layout.json, blockin.png) with Muse Spark',
13
+ async run([slug, ...only]) {
14
+ if (!slug) throw new Error('usage: diorama layout <slug> [place...]');
15
+ const files = new WorldFiles(slug);
16
+ const world = files.world();
17
+ const places = world.places.filter((p) => only.length === 0 || only.includes(p.key));
18
+ await Promise.all(places.map(async (place) => {
19
+ // A place made from a photo is measured from it, and the photo stands in for the block-in.
20
+ const photoFile = files.place(place.key, 'photo.png');
21
+ const photo = fs.existsSync(photoFile) ? fs.readFileSync(photoFile) : undefined;
22
+ const layout = await layOut(files.memo, world, place, photo, readCorrections(files.place(place.key)).layout?.guidance, approvedConcept(files, 'place', place.key));
23
+ files.write(files.place(place.key, 'layout.json'), layout);
24
+ const blockin = photo ?? await renderBlockin(layout, place);
25
+ files.write(files.place(place.key, 'blockin.png'), blockin);
26
+ files.write(files.place(place.key, 'plan.png'), await renderPlan(layout, place, blockin, world.names));
27
+ console.log(`${place.key}: ${layout.floors.length} floor(s), ${layout.things.length} things, horizon ${layout.horizon}`);
28
+ }));
29
+ console.log(`spark calls sent: ${files.memo.sent.spark}`);
30
+ },
31
+ });
@@ -0,0 +1,117 @@
1
+ // diorama masks <slug> [place...]: masks/<thing>.png, masks/floors/<floor>.png and masks.json per place.
2
+ import fs from 'node:fs';
3
+ import { maskFromPng, maskToPng, type Mask } from '../image/mask.ts';
4
+ import { PALETTE, svgLabel, tintOver } from '../image/overlay.ts';
5
+ import { defineCommand } from '../registry.ts';
6
+ import { Brief } from '../scene/brief.ts';
7
+ import { carryFrom, ran, remakes, tilesLine } from '../scene/carry.ts';
8
+ import { brushed, correctCells, correctSpots, readCorrections } from '../scene/corrections.ts';
9
+ import { Layout } from '../scene/layout.ts';
10
+ import { locateThings, markFloors } from '../scene/locate.ts';
11
+ import { checkMasks, type MaskCheck } from '../scene/maskcheck.ts';
12
+ import { maskFloors, maskThings } from '../scene/masks.ts';
13
+ import { paintingLayoutHash } from '../scene/paint-layout.ts';
14
+ import { maskBox, type Rect } from '../image/raster.ts';
15
+ import { WorldFiles } from '../world/files.ts';
16
+
17
+ defineCommand({
18
+ name: 'masks',
19
+ usage: 'masks <slug> [place...] find each thing and floor in the painting with Spark and SAM 3.1',
20
+ async run([slug, ...only]) {
21
+ if (!slug) throw new Error('usage: diorama masks <slug> [place...]');
22
+ const files = new WorldFiles(slug);
23
+ const world = files.world();
24
+ const places = world.places.filter((p) => only.length === 0 || only.includes(p.key));
25
+ await Promise.all(places.map(async (place) => {
26
+ const at = (...parts: string[]) => files.place(place.key, ...parts);
27
+ fs.rmSync(at('verify.json'), { force: true });
28
+ const layout = Layout.parse(files.readJson(at('layout.json')));
29
+ const brief = Brief.parse(files.readJson(at('brief.json')));
30
+ const painting = fs.readFileSync(at('painting.png'));
31
+ // A rejected/stale checked painting must not be rescued by masks and invented floor strokes.
32
+ // Legacy worlds without a report remain editable; rebuilding from paint checks them too.
33
+ if (!fs.existsSync(at('photo.png')) && fs.existsSync(at('painting-layout.json'))) {
34
+ const report = files.readJson(at('painting-layout.json')) as { ok?: boolean; inputs?: string };
35
+ if (!report.ok || report.inputs !== paintingLayoutHash(place, layout, fs.readFileSync(at('blockin.png')), painting)) {
36
+ throw new Error(`PAINT_LAYOUT_MISMATCH ${place.key}: painting/layout changed or its composition check failed; rebuild this place from paint to inspect and repair it before masks`);
37
+ }
38
+ }
39
+ const flagged: string[] = [];
40
+ const log = (line: string) => {
41
+ flagged.push(line);
42
+ console.log(` ${place.key}: ${line}`);
43
+ };
44
+
45
+ // After a small repaint, what it did not reach is found again on the old painting: the memo has those answers.
46
+ const carry = await carryFrom(at(), 'masks', painting);
47
+ if (carry) console.log(` ${place.key}: ${tilesLine(carry)}; only what it reaches is asked again`);
48
+ const seen = carry?.old ?? painting;
49
+ const [found, marked] = await Promise.all([
50
+ locateThings(files.memo, place, brief, seen),
51
+ markFloors(files.memo, place, layout, brief, seen),
52
+ ]);
53
+ files.write(at('locate.json'), { things: found, floors: marked });
54
+ // The person's fixes go in before SAM is asked and their brush strokes after, every time.
55
+ const fixes = readCorrections(at());
56
+ const spots = correctSpots(found, fixes);
57
+ const cells = correctCells(marked, fixes);
58
+ const before = async (file: string): Promise<Mask | undefined> => (carry && fs.existsSync(at('masks', file)) ? maskFromPng(fs.readFileSync(at('masks', file))) : undefined);
59
+ // A thing or floor keeps its mask when the change does not reach it, or only grazes a big one.
60
+ const keptThing = new Set<string>();
61
+ const keptFloor = new Set<string>();
62
+ for (const t of place.things) {
63
+ const last = await before(`${t.key}.png`);
64
+ if (carry && last && !remakes(carry, last)) keptThing.add(t.key);
65
+ }
66
+ for (const f of layout.floors) {
67
+ const last = await before(`floors/${f.key}.png`);
68
+ if (carry && last && !remakes(carry, last)) keptFloor.add(f.key);
69
+ }
70
+ const split = <T>(all: T[], kept: (x: T) => boolean) => [all.filter(kept), all.filter((x) => !kept(x))];
71
+ const [oldThings, newThings] = split(place.things, (t) => keptThing.has(t.key));
72
+ const [oldFloors, newFloors] = split(layout.floors, (f) => keptFloor.has(f.key));
73
+ const [thingsOld, thingsNew, floorsOld, floorsNew] = await Promise.all([
74
+ oldThings.some((t) => t.role !== 'npc') ? maskThings(files.memo, { ...place, things: oldThings }, layout, spots, carry!.old, log) : [],
75
+ newThings.some((t) => t.role !== 'npc') ? maskThings(files.memo, { ...place, things: newThings }, layout, spots, painting, log) : [],
76
+ oldFloors.length ? maskFloors(files.memo, place, { ...layout, floors: oldFloors }, cells, carry!.old, log) : [],
77
+ newFloors.length ? maskFloors(files.memo, place, { ...layout, floors: newFloors }, cells, painting, log) : [],
78
+ ]);
79
+ const inOrder = <T extends { key: string }>(keys: string[], got: T[]) => keys.flatMap((k) => got.filter((g) => g.key === k));
80
+ const things = inOrder(place.things.map((t) => t.key), [...thingsOld, ...thingsNew]).map((t) => brushed(t, fixes.things[t.key]?.strokes));
81
+ const floors = inOrder(layout.floors.map((f) => f.key), [...floorsOld, ...floorsNew]).map((f) => brushed(f, fixes.floors[f.key]?.strokes));
82
+ if (carry) console.log(` ${place.key}: kept ${thingsOld.length}/${things.length} things and ${floorsOld.length}/${floors.length} floors as they were; masked again: ${[...thingsNew, ...floorsNew].map((m) => m.key).join(', ') || 'nothing'}`);
83
+ const lastChecks = carry && fs.existsSync(at('masks.json')) ? ((files.readJson(at('masks.json')) as { things?: Record<string, { box?: Rect; check?: MaskCheck }> }).things ?? {}) : {};
84
+
85
+ fs.rmSync(at('masks'), { recursive: true, force: true });
86
+ for (const t of things) files.write(at('masks', `${t.key}.png`), await maskToPng(t.mask));
87
+ for (const f of floors) files.write(at('masks', 'floors', `${f.key}.png`), await maskToPng(f.mask));
88
+ const layers = [...floors.map((f) => ({ key: f.key, mask: f.mask, alpha: 0.25 })), ...things.map((t) => ({ key: t.key, mask: t.mask, alpha: 0.45 }))];
89
+ const tints = layers.map((l, i) => ({ mask: l.mask, alpha: l.alpha, rgb: PALETTE[i % PALETTE.length] }));
90
+ const labels = [...floors.map((f) => ({ key: f.key, box: maskBox(f.mask) })), ...things.map((t) => ({ key: t.key, box: t.box as Rect | undefined }))]
91
+ .map((l, i) => (l.box ? svgLabel(l.box[0] + 4, l.box[1] + 18, l.key, PALETTE[i % PALETTE.length]) : ''));
92
+ files.write(at('masks', 'overlay.png'), await tintOver(painting, tints, labels.join('')));
93
+ // A corrected box guides SAM; it does not prove what SAM cut out. Kept masks retain their check.
94
+ let checks: Record<string, MaskCheck> = Object.fromEntries(things.flatMap((t) => (keptThing.has(t.key) && lastChecks[t.key]?.check && JSON.stringify(t.box) === JSON.stringify(lastChecks[t.key].box) ? [[t.key, lastChecks[t.key].check!]] : [])));
95
+ try {
96
+ const look = things.filter((t) => !checks[t.key]);
97
+ // Asked by what the thing is (the world's words), not by how the brief dressed it: a brief that described
98
+ // "the empty stall the cow uses" let a mask on an empty stall pass.
99
+ if (look.length) checks = { ...checks, ...await checkMasks(files.memo, place.key, painting, look.map((t) => ({ key: t.key, looks: place.things.find((x) => x.key === t.key)?.looks ?? brief.looks[t.key] ?? t.key, box: t.box as Rect | undefined }))) };
100
+ for (const [key, c] of Object.entries(checks)) if (!c.ok) log(`${key}: Spark sees ${c.seen ?? 'something else'} in its mask`);
101
+ } catch (error) {
102
+ throw new Error(`mask check failed: ${error instanceof Error ? error.message : error}`);
103
+ }
104
+ const round = (v: number) => Math.round(v * 100) / 100;
105
+ files.write(at('masks.json'), {
106
+ things: Object.fromEntries(things.map(({ key, mask: _, ...t }) => [key, { ...t, ...(checks[key] && { check: checks[key] }) }])),
107
+ floors: Object.fromEntries(floors.map(({ key, mask: _, iouCells, iouLayout, ...f }) => [key, { ...f, iouCells: round(iouCells), iouLayout: round(iouLayout) }])),
108
+ flagged,
109
+ });
110
+ // After a carry the old painting stays the one the answers belong to, so a rerun on this painting asks nothing new.
111
+ ran(at(), 'masks', carry?.old ?? painting);
112
+ const coarse = [...things, ...floors].filter((m) => m.coarse).map((m) => m.key);
113
+ console.log(`${place.key}: ${things.length} things, ${floors.length} floors${coarse.length ? `, coarse: ${coarse.join(', ')}` : ''}`);
114
+ }));
115
+ console.log(`calls sent: spark ${files.memo.sent.spark}, sam ${files.memo.sent.sam}`);
116
+ },
117
+ });
@@ -0,0 +1,52 @@
1
+ // diorama mcp: the Studio's actions as an MCP server over stdio, for Muse Code (or any MCP client) started
2
+ // by hand. With a Studio running for this folder it forwards every call there, so the page shows the agent's
3
+ // work live; without one it runs the actions itself.
4
+ import path from 'node:path';
5
+ import readline from 'node:readline';
6
+ import '../actions/index.ts';
7
+ import { defineCommand } from '../registry.ts';
8
+ import { Events } from '../studio/events.ts';
9
+ import { snapshotBeforeJob } from '../studio/history.ts';
10
+ import { Jobs } from '../studio/jobs.ts';
11
+ import { handleMcp, type Reply } from '../studio/mcp.ts';
12
+ import { readStudioFile } from '../studio/discovery.ts';
13
+
14
+ defineCommand({
15
+ name: 'mcp',
16
+ usage: 'mcp serve the Studio actions as MCP tools on stdio (forwards to a running diorama studio)',
17
+ async run() {
18
+ const studio = readStudioFile(process.cwd());
19
+ const events = new Events();
20
+ const root = path.resolve('worlds');
21
+ const ctx = { by: 'agent' as const, root, events, jobs: new Jobs(events, { beforeStart: snapshotBeforeJob(root, events) }) };
22
+ const out = (reply: Reply | Reply[] | null) => reply && process.stdout.write(`${JSON.stringify(reply)}\n`);
23
+ // Logs go to stderr: stdout carries only protocol messages.
24
+ console.error(studio ? `diorama mcp: forwarding to the Studio at ${studio.url}` : 'diorama mcp: no Studio running here, running actions in this process');
25
+ const lines = readline.createInterface({ input: process.stdin });
26
+ for await (const line of lines) {
27
+ if (!line.trim()) continue;
28
+ let message: unknown;
29
+ try {
30
+ message = JSON.parse(line);
31
+ } catch {
32
+ out({ jsonrpc: '2.0', id: null, error: { code: -32700, message: 'parse error' } });
33
+ continue;
34
+ }
35
+ if (studio) {
36
+ const res = await fetch(`${studio.url}mcp`, {
37
+ method: 'POST',
38
+ headers: { authorization: `Bearer ${studio.agentToken}`, 'content-type': 'application/json' },
39
+ body: line,
40
+ }).catch((error: Error) => ({ ok: false, status: 0, text: async () => error.message }));
41
+ if (res.status === 202) continue;
42
+ const text = await res.text();
43
+ if (res.ok) process.stdout.write(`${text}\n`);
44
+ else out({ jsonrpc: '2.0', id: (message as { id?: string | number }).id ?? null, error: { code: -32603, message: `the Studio answered ${res.status}: ${text.slice(0, 200)}` } });
45
+ continue;
46
+ }
47
+ if (Array.isArray(message)) out((await Promise.all(message.map((m) => handleMcp(m, ctx)))).filter((r): r is Reply => !!r));
48
+ else out(await handleMcp(message as Parameters<typeof handleMcp>[0], ctx));
49
+ }
50
+ ctx.jobs.stopAll();
51
+ },
52
+ });