@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,234 @@
1
+ // The atelier around every world: the mark, the worlds rail (each world's painting, a ring while it builds, a
2
+ // red dot when it waits for you) and the bell that gathers what is building and what needs you. One studio-wide
3
+ // feed keeps them live; the worlds come from world_list, the same action the agent and the CLI use.
4
+ import { signal } from '@preact/signals';
5
+ import { useEffect, useState } from 'preact/hooks';
6
+ import { call, fileUrl, follow } from './api.ts';
7
+ import { go, route } from './nav.ts';
8
+ import { doneWords, jobWords, stepWords } from './job-words.ts';
9
+
10
+ /** The Diorama mark: an arched opening onto painted layers, a moon and the ground you walk on. */
11
+ export function Logo({ size = 28 }: { size?: number }) {
12
+ return (
13
+ <svg class="logo" width={size} height={size} viewBox="0 0 32 32" aria-hidden="true">
14
+ <defs><clipPath id="dio-arch"><path d="M7.4 28 V14 A8.6 8.6 0 0 1 24.6 14 V28Z" /></clipPath></defs>
15
+ <path d="M5.2 28.8 V14 A10.8 10.8 0 0 1 26.8 14 V28.8Z" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linejoin="round" />
16
+ <g clip-path="url(#dio-arch)">
17
+ <circle cx="19.5" cy="12" r="2.8" fill="var(--amber, #c27a2c)" />
18
+ <path d="M6 22 C10 18 14 18 18 21 C21 23 24 22 27 19.5 V29 H6Z" fill="currentColor" opacity=".32" />
19
+ <path d="M6 26 C11 22.5 16 22.8 21 25 C23 26 25 25.6 27 25 V29 H6Z" fill="currentColor" />
20
+ </g>
21
+ </svg>
22
+ );
23
+ }
24
+
25
+ export type WorldSummary = { slug: string; title: string; pitch: string | null; issue: string | null; places: number; playable: number; verified: number; steps: string[]; cover: { file: string; v: number | null } | null; workshop?: boolean };
26
+ /** A world's job right now: its steps, the ones done, the one running; failed when the last job failed. */
27
+ type Busy = { label: string; plan: string[]; done: string[]; step?: string; by: string };
28
+ type Ended = { label: string; ok: boolean; at: number; error?: string };
29
+
30
+ export const worlds = signal<WorldSummary[] | null>(null);
31
+ export const busy = signal<Record<string, Busy>>({});
32
+ const ended = signal<Record<string, Ended>>({});
33
+ const seen = signal<Record<string, number>>(readSeen());
34
+
35
+ function readSeen(): Record<string, number> {
36
+ try {
37
+ return JSON.parse(localStorage.getItem('diorama.seen') ?? '{}') as Record<string, number>;
38
+ } catch {
39
+ return {};
40
+ }
41
+ }
42
+ /** Bell notes put away by hand, per world: the note's words. The same note stays away; a new one (other words) shows again. */
43
+ const dismissed = signal<Record<string, string>>(readDismissed());
44
+ function readDismissed(): Record<string, string> {
45
+ try {
46
+ return JSON.parse(localStorage.getItem('diorama.dismissed') ?? '{}') as Record<string, string>;
47
+ } catch {
48
+ return {};
49
+ }
50
+ }
51
+ function dismiss(slug: string, note: string) {
52
+ dismissed.value = { ...dismissed.value, [slug]: note };
53
+ try {
54
+ localStorage.setItem('diorama.dismissed', JSON.stringify(dismissed.value));
55
+ } catch {
56
+ // A private window keeps it for this visit only.
57
+ }
58
+ }
59
+
60
+ function markSeen(slug: string) {
61
+ seen.value = { ...seen.value, [slug]: Date.now() };
62
+ try {
63
+ localStorage.setItem('diorama.seen', JSON.stringify(seen.value));
64
+ } catch {
65
+ // A private window keeps it for this visit only.
66
+ }
67
+ }
68
+
69
+ let revision = 0;
70
+ const refresh = async () => {
71
+ const current = ++revision;
72
+ try {
73
+ const list = await call<WorldSummary[]>('world_list');
74
+ if (current === revision) worlds.value = list;
75
+ } catch {}
76
+ };
77
+
78
+ /** Forget a confirmed deletion immediately, including any list response already in flight. */
79
+ export function forgetWorld(slug: string) {
80
+ revision++;
81
+ worlds.value = worlds.value?.filter((w) => w.slug !== slug) ?? null;
82
+ const nextBusy = { ...busy.value }, nextEnded = { ...ended.value };
83
+ delete nextBusy[slug];
84
+ delete nextEnded[slug];
85
+ busy.value = nextBusy;
86
+ ended.value = nextEnded;
87
+ }
88
+
89
+ /** Starts the studio-wide feed once: jobs start and end, steps finish; the worlds list is read again after. */
90
+ export function useAtelierFeed() {
91
+ useEffect(() => {
92
+ void refresh();
93
+ let timer: ReturnType<typeof setTimeout> | undefined;
94
+ const soon = () => (clearTimeout(timer), (timer = setTimeout(refresh, 600)));
95
+ const stop = follow(null, (e) => {
96
+ const slug = String(e.world ?? '');
97
+ if (!slug) return;
98
+ const job = e.job as { label?: string; by?: string; state?: string; error?: string; args?: string[] } | undefined;
99
+ const b = busy.value[slug];
100
+ switch (e.type) {
101
+ case 'world.deleted':
102
+ forgetWorld(slug);
103
+ return soon();
104
+ case 'job.start':
105
+ busy.value = { ...busy.value, [slug]: { label: job?.label ?? 'build', plan: [], done: [], by: job?.by ?? 'you' } };
106
+ return soon();
107
+ case 'build.start':
108
+ if (b) busy.value = { ...busy.value, [slug]: { ...b, plan: e.steps as string[] } };
109
+ return;
110
+ case 'step.start':
111
+ if (b) busy.value = { ...busy.value, [slug]: { ...b, step: String(e.step) } };
112
+ return;
113
+ case 'step.end':
114
+ if (b) busy.value = { ...busy.value, [slug]: { ...b, done: [...b.done, String(e.step)] } };
115
+ return soon();
116
+ case 'job.end': {
117
+ const next = { ...busy.value };
118
+ delete next[slug];
119
+ busy.value = next;
120
+ ended.value = { ...ended.value, [slug]: { label: job?.label ?? 'job', ok: job?.state === 'done', at: Date.parse(e.at), error: job?.error } };
121
+ return soon();
122
+ }
123
+ }
124
+ });
125
+ return () => (stop(), clearTimeout(timer));
126
+ }, []);
127
+ }
128
+
129
+ const progress = (b: Busy) => (b.plan.length ? Math.min(0.97, (b.done.length + 0.4) / b.plan.length) : 0.05);
130
+ /** Waits for the person: the last job failed, or the world is built but some place is not playable. */
131
+ function needs(w: WorldSummary): string | null {
132
+ if (busy.value[w.slug]) return null;
133
+ const e = ended.value[w.slug];
134
+ if (e && !e.ok) return `${e.label} failed${e.error ? `: ${e.error}` : ''}`;
135
+ if (w.issue && w.issue !== 'no world.json yet') return w.issue;
136
+ if (w.places > 0 && w.steps.includes('verify') && w.playable < w.places) return `${w.places - w.playable} of ${w.places} places not playable yet`;
137
+ return null;
138
+ }
139
+
140
+ function Ring({ p }: { p: number }) {
141
+ const r = 9;
142
+ const c = 2 * Math.PI * r;
143
+ return (
144
+ <svg class="ring" width="24" height="24" viewBox="0 0 24 24" aria-label={`${Math.round(p * 100)}% built`}>
145
+ <circle cx="12" cy="12" r={r} class="track" />
146
+ <circle cx="12" cy="12" r={r} class="done" stroke-dasharray={`${c * p} ${c}`} transform="rotate(-90 12 12)" />
147
+ </svg>
148
+ );
149
+ }
150
+
151
+ const here = () => /^\/w\/([a-z][a-z0-9-]*)/.exec(route.value)?.[1] ?? null;
152
+
153
+ /** Every world as its painting, newest first; the one open is ringed in amber. */
154
+ export function Rail() {
155
+ const open = here();
156
+ useEffect(() => void (open && markSeen(open)), [open]);
157
+ return (
158
+ <nav class="rail" aria-label="Worlds">
159
+ <a href="/" class="mark-link" title="The atelier: describe a new world" onClick={(e) => (e.preventDefault(), go('/'))}><Logo size={30} /><span>Diorama</span></a>
160
+ <button type="button" class="new" title="A new world: describe it in a sentence" aria-label="New world" onClick={() => go('/')}>+</button>
161
+ <div class="rail-worlds">
162
+ {worlds.value?.filter((w) => !(w.workshop && w.issue === 'no world.json yet')).map((w) => {
163
+ const b = busy.value[w.slug];
164
+ const n = needs(w);
165
+ return (
166
+ <a href={`/w/${w.slug}`} class={`rail-world${open === w.slug ? ' on' : ''}`} title={`${w.title}${b ? ` · ${b.label}${b.step ? `: ${b.step}` : ''}` : n ? ` · needs you: ${n}` : ''}`} onClick={(e) => (e.preventDefault(), markSeen(w.slug), go(`/w/${w.slug}`))}>
167
+ <span class="th" style={w.cover ? { backgroundImage: `url("${fileUrl(w.slug, w.cover.file, w.cover.v)}")` } : undefined} />
168
+ {b ? <Ring p={progress(b)} /> : n ? <span class="dot-needs" aria-label="needs you">!</span> : null}
169
+ <span class="lb">{w.title}</span>
170
+ </a>
171
+ );
172
+ })}
173
+ </div>
174
+ </nav>
175
+ );
176
+ }
177
+
178
+ /** What a world's running job does, for the bell: who, what, which step now, how far. */
179
+ function busyWords(b: Busy): string {
180
+ const what = jobWords(b.label);
181
+ const now = b.step ? `, now ${stepWords(b.step)}` : '';
182
+ const far = b.plan.length ? ` · step ${Math.min(b.done.length + 1, b.plan.length)} of ${b.plan.length}` : '';
183
+ return `${b.by === 'agent' ? 'The agent is ' : ''}${b.by === 'agent' ? what : what.replace(/^./, (c) => c.toUpperCase())}${now}${far}`;
184
+ }
185
+
186
+ /** What is building, what waits for you, and what finished since you last looked: one click goes there. */
187
+ export function Bell() {
188
+ const [open, setOpen] = useState(false);
189
+ useEffect(() => {
190
+ if (!open) return;
191
+ const close = (e: Event) => !(e.target as HTMLElement).closest?.('.bell-wrap') && setOpen(false);
192
+ // Esc closes the menu only, not also the view under it.
193
+ const esc = (e: KeyboardEvent) => e.key === 'Escape' && (e.stopImmediatePropagation(), setOpen(false));
194
+ addEventListener('pointerdown', close);
195
+ addEventListener('keydown', esc, true);
196
+ return () => (removeEventListener('pointerdown', close), removeEventListener('keydown', esc, true));
197
+ }, [open]);
198
+ const list = worlds.value ?? [];
199
+ const items = [
200
+ ...list.flatMap((w) => (needs(w) && dismissed.value[w.slug] !== `needs:${needs(w)}` ? [{ w, kind: 'needs', text: needs(w)! }] : [])),
201
+ ...list.flatMap((w) => { const b = busy.value[w.slug]; return b ? [{ w, kind: 'busy', text: busyWords(b) }] : []; }),
202
+ ...list.flatMap((w) => { const e = ended.value[w.slug]; return e?.ok && !busy.value[w.slug] && e.at > (seen.value[w.slug] ?? 0) ? [{ w, kind: 'done', text: doneWords(e.label) }] : []; }),
203
+ ];
204
+ // A note that waits for you, or a finished job, can be put away; a build in progress is live and stays.
205
+ const put = (i: (typeof items)[number]) => (i.kind === 'done' ? markSeen(i.w.slug) : dismiss(i.w.slug, `needs:${i.text}`));
206
+ const away = items.filter((i) => i.kind !== 'busy');
207
+ const loud = items.filter((i) => i.kind !== 'busy').length;
208
+ return (
209
+ <div class="bell-wrap">
210
+ <button type="button" class={`bell${loud ? ' loud' : ''}`} aria-expanded={open} title="What is building and what waits for you" aria-label={`Notifications${items.length ? `: ${items.length}` : ''}`} onClick={() => setOpen(!open)}>
211
+ <svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" aria-hidden="true"><path d="M6 16V11a6 6 0 0 1 12 0v5l1.5 2h-15Z" /><path d="M10 20.5a2 2 0 0 0 4 0" /></svg>
212
+ {items.length > 0 && <b>{items.length}</b>}
213
+ </button>
214
+ {open && (
215
+ <div class="bell-pop" role="menu">
216
+ {items.length === 0 && <p class="detail">Nothing building, nothing waiting for you.</p>}
217
+ {items.map((i) => {
218
+ const { w, kind, text } = i;
219
+ return (
220
+ <div class="bell-row">
221
+ <a href={`/w/${w.slug}`} class={`bell-item ${kind}`} onClick={(e) => (e.preventDefault(), markSeen(w.slug), setOpen(false), go(`/w/${w.slug}`))}>
222
+ <span class="th" style={w.cover ? { backgroundImage: `url("${fileUrl(w.slug, w.cover.file, w.cover.v)}")` } : undefined} />
223
+ <span><strong>{w.title}</strong> {kind === 'needs' ? 'needs you' : kind === 'busy' ? 'is building' : 'is ready'}<small>{text}</small></span>
224
+ </a>
225
+ {kind !== 'busy' && <button type="button" class="ghost tiny bell-x" aria-label={`Dismiss: ${w.title}`} title={kind === 'needs' ? 'Dismiss. It comes back only if something new needs you' : 'Dismiss'} onClick={() => put(i)}>✕</button>}
226
+ </div>
227
+ );
228
+ })}
229
+ {away.length > 1 && <button type="button" class="ghost small bell-clear" onClick={() => away.forEach(put)}>Clear all</button>}
230
+ </div>
231
+ )}
232
+ </div>
233
+ );
234
+ }
@@ -0,0 +1,25 @@
1
+ import { audioIssues, type AudioMisses } from '../src/sound/issues.ts';
2
+
3
+ export function AudioMissing({ misses, retry }: { misses: AudioMisses; retry: () => void }) {
4
+ const groups = audioIssues(misses);
5
+ const waiting = groups.filter((g) => g.kind === 'validation');
6
+ const count = waiting.reduce((n, g) => n + g.count, 0);
7
+ const other = misses.lines.length - count;
8
+ const nice = (s: string) => s.replace(/-/g, ' ');
9
+ const labels = { validation: 'verification pending', design: 'voice creation pending', rejected: 'did not pass audio checks', unavailable: 'generation unavailable' };
10
+ return <>
11
+ {waiting.length > 0 && <p>Voice verification is pending for {waiting.length} {waiting.length === 1 ? 'voice' : 'voices'}, blocking {count} {count === 1 ? 'line' : 'lines'}. The verification service could not finish; this does not mean the voices sound bad.</p>}
12
+ {(other > 0 || misses.sounds.length > 0) && <p>{[
13
+ ...(other ? [`${other}${waiting.length ? ' other' : ''} spoken ${other === 1 ? 'line' : 'lines'}`] : []),
14
+ ...(misses.sounds.length ? [`${misses.sounds.length} ${misses.sounds.length === 1 ? 'sound' : 'sounds'}`] : []),
15
+ ].join(' and ')} {other + misses.sounds.length === 1 ? 'is' : 'are'} missing.</p>}
16
+ <p class="note">These audio gaps do not by themselves block play. Retry checks saved voice samples again and generates new audio only when needed.</p>
17
+ <details><summary>Audio details · {groups.length} {groups.length === 1 ? 'group' : 'groups'}</summary>
18
+ <ul>{groups.map((g) => <li key={`${g.kind}:${g.speaker ?? g.id}`}>
19
+ <strong>{nice(g.speaker ?? g.id!)}: {labels[g.kind]}{g.speaker ? ` (${g.count} ${g.count === 1 ? 'line' : 'lines'})` : ''}</strong>
20
+ <p class="detail" style={{ overflowWrap: 'anywhere' }}>{g.reason}</p>
21
+ </li>)}</ul>
22
+ </details>
23
+ <div class="btns"><button type="button" class="ghost small" onClick={retry}>Retry missing audio</button></div>
24
+ </>;
25
+ }
@@ -0,0 +1,33 @@
1
+ import { useEffect, useState } from 'preact/hooks';
2
+ import { call } from './api.ts';
3
+
4
+ type AudioSettings = { quality: 'final' | 'draft'; env: string | null };
5
+
6
+ export function AudioSetup() {
7
+ const [settings, setSettings] = useState<AudioSettings | null>(null);
8
+ const [busy, setBusy] = useState(false);
9
+ const [error, setError] = useState('');
10
+ const [saved, setSaved] = useState(false);
11
+ useEffect(() => { void call<AudioSettings>('audio_get').then(setSettings).catch((e) => setError(e.message)); }, []);
12
+ const choose = async (quality: string) => {
13
+ setBusy(true); setError(''); setSaved(false);
14
+ try { setSettings(await call<AudioSettings>('audio_set', { quality })); setSaved(true); }
15
+ catch (e) { setError((e as Error).message); }
16
+ finally { setBusy(false); }
17
+ };
18
+ return <section class="budget" aria-label="Audio settings">
19
+ <h2>Audio</h2>
20
+ <div class="prices"><label>Audio quality
21
+ <select value={settings?.quality ?? ''} disabled={!settings || busy || settings.env !== null} onChange={(e) => void choose(e.currentTarget.value)}>
22
+ {!settings && <option value="">Loading…</option>}
23
+ <option value="final">Final (default)</option>
24
+ <option value="draft">Draft (lower cost)</option>
25
+ </select>
26
+ </label></div>
27
+ <p class="note">Final uses richer voices, more attempts to get lines right and longer ambience. Draft uses fewer credits for quick previews.</p>
28
+ <p class="note">Applies to future audio generations in all worlds. To upgrade existing recordings, rerun Voices and Sound. This does not rewrite dialogue.</p>
29
+ {settings?.env !== null && settings?.env !== undefined && <p class="note">Set by DIORAMA_SOUND={settings.env}. Unset it and restart Studio to change this here.</p>}
30
+ <span role="status">{busy ? 'Saving…' : saved ? 'Saved' : ''}</span>
31
+ {error && <p class="error" role="alert">{error}</p>}
32
+ </section>;
33
+ }
@@ -0,0 +1,30 @@
1
+ import { useEffect, useState } from 'preact/hooks';
2
+ import { call } from './api.ts';
3
+
4
+ type BlenderState = { enabled: boolean; active: boolean; env: string | null; installed: { bin: string; version: string } | null };
5
+
6
+ /** Setup: the optional local Blender that gives 3D figures a calm rest pose, and its on/off switch. */
7
+ export function BlenderSetup() {
8
+ const [state, setState] = useState<BlenderState | null>(null);
9
+ const [busy, setBusy] = useState(false);
10
+ const [error, setError] = useState('');
11
+ useEffect(() => { void call<BlenderState>('blender_get').then(setState).catch((e) => setError(e.message)); }, []);
12
+ const toggle = async (enabled: boolean) => {
13
+ setBusy(true); setError('');
14
+ try { setState(await call<BlenderState>('blender_set', { enabled })); }
15
+ catch (e) { setError((e as Error).message); }
16
+ finally { setBusy(false); }
17
+ };
18
+ return <section class="budget blender" aria-label="Blender">
19
+ <h2>3D animation</h2>
20
+ <label class="switch-row" title="Free, runs on this computer. New 3D models get it automatically; existing ones with Relax in the cast (cast_relax)">
21
+ <span>Use Blender for natural walks</span>
22
+ <input type="checkbox" role="switch" checked={!!state?.enabled} disabled={!state || busy || state.env !== null} onChange={(e) => void toggle(e.currentTarget.checked)} />
23
+ </label>
24
+ <p class="note">{!state ? 'Looking for Blender…'
25
+ : state.installed ? `Blender ${state.installed.version} found${state.active ? ', in use' : ', not in use'}. It puts each 3D figure into a calm pose (arms down, feet under the hips) before it walks; without it they walk legs apart, arms out.`
26
+ : 'Not installed. On a Mac: brew install --cask blender, or blender.org. Then Check again. Without it 3D figures walk legs apart, arms out.'}</p>
27
+ {state?.env !== null && state?.env !== undefined && <p class="note">Set by DIORAMA_BLENDER={state.env}. Unset it and restart Studio to switch it here.</p>}
28
+ {error && <p class="error" role="alert">{error}</p>}
29
+ </section>;
30
+ }