@gtanczyk/diorama 0.0.0-stage → 0.1.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/LICENSE +202 -0
- package/README.md +157 -2
- package/app/bin/diorama.mjs +13 -0
- package/app/src/actions/budget.ts +152 -0
- package/app/src/actions/build.ts +158 -0
- package/app/src/actions/cast.ts +248 -0
- package/app/src/actions/depth.ts +95 -0
- package/app/src/actions/extend.ts +36 -0
- package/app/src/actions/faces.ts +160 -0
- package/app/src/actions/feedback.ts +87 -0
- package/app/src/actions/fix.ts +382 -0
- package/app/src/actions/history.ts +93 -0
- package/app/src/actions/idea-preview.ts +65 -0
- package/app/src/actions/idea-review.ts +102 -0
- package/app/src/actions/idea-sketch.ts +172 -0
- package/app/src/actions/index.ts +31 -0
- package/app/src/actions/items.ts +94 -0
- package/app/src/actions/kids.ts +215 -0
- package/app/src/actions/models.ts +64 -0
- package/app/src/actions/motion.ts +119 -0
- package/app/src/actions/pictures.ts +49 -0
- package/app/src/actions/reactions.ts +36 -0
- package/app/src/actions/registry.ts +101 -0
- package/app/src/actions/share.ts +32 -0
- package/app/src/actions/sound.ts +73 -0
- package/app/src/actions/start.ts +154 -0
- package/app/src/actions/step-work.ts +150 -0
- package/app/src/actions/story.ts +101 -0
- package/app/src/actions/studio.ts +133 -0
- package/app/src/actions/style.ts +72 -0
- package/app/src/actions/video.ts +46 -0
- package/app/src/actions/view.ts +109 -0
- package/app/src/actions/weather.ts +38 -0
- package/app/src/actions/world.ts +289 -0
- package/app/src/commands/action.ts +39 -0
- package/app/src/commands/backdrop.ts +188 -0
- package/app/src/commands/browsers.ts +21 -0
- package/app/src/commands/build.ts +503 -0
- package/app/src/commands/cast.ts +142 -0
- package/app/src/commands/clip.ts +431 -0
- package/app/src/commands/derive.ts +167 -0
- package/app/src/commands/doctor.ts +74 -0
- package/app/src/commands/explore.ts +123 -0
- package/app/src/commands/export.ts +142 -0
- package/app/src/commands/extend.ts +75 -0
- package/app/src/commands/faces.ts +80 -0
- package/app/src/commands/icons.ts +69 -0
- package/app/src/commands/index.ts +36 -0
- package/app/src/commands/layout.ts +31 -0
- package/app/src/commands/masks.ts +117 -0
- package/app/src/commands/mcp.ts +52 -0
- package/app/src/commands/model.ts +284 -0
- package/app/src/commands/paint.ts +182 -0
- package/app/src/commands/plates.ts +97 -0
- package/app/src/commands/play.ts +192 -0
- package/app/src/commands/playthrough.ts +111 -0
- package/app/src/commands/publish.ts +78 -0
- package/app/src/commands/reactions.ts +40 -0
- package/app/src/commands/reel.ts +46 -0
- package/app/src/commands/setup.ts +85 -0
- package/app/src/commands/sound.ts +90 -0
- package/app/src/commands/sprites.ts +232 -0
- package/app/src/commands/story.ts +37 -0
- package/app/src/commands/studio.ts +65 -0
- package/app/src/commands/style.ts +87 -0
- package/app/src/commands/verify.ts +45 -0
- package/app/src/commands/voice-retry.ts +14 -0
- package/app/src/commands/voices.ts +145 -0
- package/app/src/commands/weather.ts +52 -0
- package/app/src/commands/world.ts +104 -0
- package/app/src/film/clip.ts +386 -0
- package/app/src/film/reel.ts +126 -0
- package/app/src/film/screencast.ts +123 -0
- package/app/src/image/mask.ts +29 -0
- package/app/src/image/overlay.ts +38 -0
- package/app/src/image/raster.ts +224 -0
- package/app/src/main.ts +28 -0
- package/app/src/meta/choice.ts +27 -0
- package/app/src/meta/http.ts +99 -0
- package/app/src/meta/memo.ts +91 -0
- package/app/src/meta/muse.ts +70 -0
- package/app/src/meta/openrouter.ts +93 -0
- package/app/src/meta/prices.ts +25 -0
- package/app/src/meta/sam.ts +83 -0
- package/app/src/meta/schema.ts +63 -0
- package/app/src/meta/spark.ts +220 -0
- package/app/src/meta/step.ts +14 -0
- package/app/src/meta/voice-file.ts +78 -0
- package/app/src/meta/voice.ts +103 -0
- package/app/src/model/animation.ts +38 -0
- package/app/src/model/blender/relax.py +284 -0
- package/app/src/model/blender.ts +115 -0
- package/app/src/model/build.ts +50 -0
- package/app/src/model/face.ts +21 -0
- package/app/src/model/fit.ts +181 -0
- package/app/src/model/glb.ts +241 -0
- package/app/src/model/plan.ts +151 -0
- package/app/src/model/quality.ts +48 -0
- package/app/src/model/space.ts +168 -0
- package/app/src/model/sprites.ts +293 -0
- package/app/src/model/views.ts +163 -0
- package/app/src/play/creature.ts +43 -0
- package/app/src/play/depth-occlusion.ts +150 -0
- package/app/src/play/exit-outline.ts +48 -0
- package/app/src/play/face3d.ts +34 -0
- package/app/src/play/focus.ts +78 -0
- package/app/src/play/geometry.ts +272 -0
- package/app/src/play/gestures.ts +23 -0
- package/app/src/play/glow.ts +187 -0
- package/app/src/play/lipsync.ts +69 -0
- package/app/src/play/living.ts +83 -0
- package/app/src/play/outline-path.ts +130 -0
- package/app/src/play/parallax.ts +200 -0
- package/app/src/play/path.ts +262 -0
- package/app/src/play/preview.ts +71 -0
- package/app/src/play/reactions.ts +92 -0
- package/app/src/play/rig.ts +172 -0
- package/app/src/play/shadow.ts +253 -0
- package/app/src/play/sound.ts +51 -0
- package/app/src/play/weather.ts +58 -0
- package/app/src/qa/explore.ts +136 -0
- package/app/src/qa/probe.ts +139 -0
- package/app/src/qa/route.ts +118 -0
- package/app/src/registry.ts +23 -0
- package/app/src/scene/animation-source.ts +36 -0
- package/app/src/scene/assets.ts +37 -0
- package/app/src/scene/autofix.ts +133 -0
- package/app/src/scene/backdrop.ts +326 -0
- package/app/src/scene/blockin.ts +189 -0
- package/app/src/scene/brief.ts +74 -0
- package/app/src/scene/carry.ts +177 -0
- package/app/src/scene/cast-fixes.ts +31 -0
- package/app/src/scene/cast.ts +192 -0
- package/app/src/scene/corrections.ts +164 -0
- package/app/src/scene/critic.ts +169 -0
- package/app/src/scene/depthmap.ts +149 -0
- package/app/src/scene/derive.ts +475 -0
- package/app/src/scene/diagnose.ts +132 -0
- package/app/src/scene/face-calibration.ts +65 -0
- package/app/src/scene/face-registration.ts +21 -0
- package/app/src/scene/face-work.ts +71 -0
- package/app/src/scene/faces-data.ts +48 -0
- package/app/src/scene/faces-settings.ts +20 -0
- package/app/src/scene/faces.ts +435 -0
- package/app/src/scene/glows.ts +153 -0
- package/app/src/scene/held.ts +55 -0
- package/app/src/scene/icons.ts +185 -0
- package/app/src/scene/layers.ts +822 -0
- package/app/src/scene/layout.ts +216 -0
- package/app/src/scene/locate.ts +118 -0
- package/app/src/scene/maskcheck.ts +43 -0
- package/app/src/scene/masks.ts +392 -0
- package/app/src/scene/occluders.ts +123 -0
- package/app/src/scene/occlusion-check.ts +138 -0
- package/app/src/scene/paint-layout.ts +135 -0
- package/app/src/scene/paint.ts +81 -0
- package/app/src/scene/photo.ts +123 -0
- package/app/src/scene/plates.ts +256 -0
- package/app/src/scene/playbook.ts +121 -0
- package/app/src/scene/preview.ts +23 -0
- package/app/src/scene/reactions.ts +118 -0
- package/app/src/scene/restyle.ts +53 -0
- package/app/src/scene/rig-check.ts +68 -0
- package/app/src/scene/rig.ts +375 -0
- package/app/src/scene/scale.ts +148 -0
- package/app/src/scene/shadows.ts +207 -0
- package/app/src/scene/stairs.ts +183 -0
- package/app/src/scene/status.ts +32 -0
- package/app/src/scene/verify.ts +156 -0
- package/app/src/scene/walk.ts +326 -0
- package/app/src/scene/warp.ts +417 -0
- package/app/src/scene/weather.ts +118 -0
- package/app/src/scene/words.ts +65 -0
- package/app/src/secrets.ts +219 -0
- package/app/src/setup/checks.ts +110 -0
- package/app/src/sound/cost.ts +74 -0
- package/app/src/sound/data.ts +89 -0
- package/app/src/sound/fixes.ts +23 -0
- package/app/src/sound/issues.ts +21 -0
- package/app/src/sound/language.ts +11 -0
- package/app/src/sound/lipsync-make.ts +98 -0
- package/app/src/sound/lipsync.ts +24 -0
- package/app/src/sound/make.ts +229 -0
- package/app/src/sound/musicgen.py +20 -0
- package/app/src/sound/pcm.ts +280 -0
- package/app/src/sound/plan.ts +154 -0
- package/app/src/sound/retry.ts +43 -0
- package/app/src/sound/settings.ts +28 -0
- package/app/src/sound/sources.ts +81 -0
- package/app/src/sound/spoken.ts +70 -0
- package/app/src/sound/voices.ts +579 -0
- package/app/src/speech/transcribe.ts +38 -0
- package/app/src/speech/types.ts +18 -0
- package/app/src/story/check.ts +230 -0
- package/app/src/story/design.ts +108 -0
- package/app/src/story/engine.ts +233 -0
- package/app/src/story/fixes.ts +54 -0
- package/app/src/story/npc-guidance.ts +7 -0
- package/app/src/story/phrases.ts +4 -0
- package/app/src/story/schema.ts +92 -0
- package/app/src/studio/agent.ts +554 -0
- package/app/src/studio/discovery.ts +28 -0
- package/app/src/studio/events.ts +48 -0
- package/app/src/studio/history.ts +124 -0
- package/app/src/studio/jobs.ts +291 -0
- package/app/src/studio/locks.ts +33 -0
- package/app/src/studio/mcp.ts +86 -0
- package/app/src/studio/operation.ts +18 -0
- package/app/src/studio/repair.ts +31 -0
- package/app/src/studio/server.ts +421 -0
- package/app/src/studio/trouble.ts +48 -0
- package/app/src/world/design.ts +72 -0
- package/app/src/world/drawings.ts +198 -0
- package/app/src/world/extend-stage.ts +109 -0
- package/app/src/world/extend.ts +123 -0
- package/app/src/world/extension-progress.ts +10 -0
- package/app/src/world/files.ts +62 -0
- package/app/src/world/idea-review.ts +32 -0
- package/app/src/world/photo-limits.ts +2 -0
- package/app/src/world/pool.ts +41 -0
- package/app/src/world/schedule.ts +64 -0
- package/app/src/world/schema.ts +74 -0
- package/app/src/world/sketch.ts +81 -0
- package/app/src/world/style-plan.ts +90 -0
- package/app/src/world/style-reference.ts +16 -0
- package/app/src/world/styled.ts +51 -0
- package/app/src/world/told.ts +105 -0
- package/app/src/world/video-limits.ts +3 -0
- package/app/src/world/video.ts +98 -0
- package/app/studio/activity.tsx +124 -0
- package/app/studio/agent.tsx +384 -0
- package/app/studio/api.ts +111 -0
- package/app/studio/atelier.tsx +234 -0
- package/app/studio/audio-missing.tsx +25 -0
- package/app/studio/audio-setup.tsx +33 -0
- package/app/studio/blender-setup.tsx +30 -0
- package/app/studio/build.tsx +677 -0
- package/app/studio/camera.tsx +59 -0
- package/app/studio/cast-work.tsx +207 -0
- package/app/studio/cast.tsx +178 -0
- package/app/studio/concept-edit.tsx +47 -0
- package/app/studio/creator-idea.tsx +46 -0
- package/app/studio/custom-look.tsx +36 -0
- package/app/studio/faces-setup.tsx +28 -0
- package/app/studio/favicon.svg +11 -0
- package/app/studio/figure.tsx +314 -0
- package/app/studio/fonts/OFL.txt +93 -0
- package/app/studio/fonts/fonts.css +7 -0
- package/app/studio/fonts/fraunces-italic-latin-ext.woff2 +0 -0
- package/app/studio/fonts/fraunces-italic-latin.woff2 +0 -0
- package/app/studio/fonts/fraunces-normal-latin-ext.woff2 +0 -0
- package/app/studio/fonts/fraunces-normal-latin.woff2 +0 -0
- package/app/studio/fonts/instrument-sans-normal-latin-ext.woff2 +0 -0
- package/app/studio/fonts/instrument-sans-normal-latin.woff2 +0 -0
- package/app/studio/icons.tsx +42 -0
- package/app/studio/idea-card.tsx +42 -0
- package/app/studio/idea-review.tsx +181 -0
- package/app/studio/idea-storyboard.tsx +134 -0
- package/app/studio/index.html +15 -0
- package/app/studio/job-words.ts +33 -0
- package/app/studio/lightbox.tsx +49 -0
- package/app/studio/main.tsx +1328 -0
- package/app/studio/nav.ts +14 -0
- package/app/studio/place.tsx +264 -0
- package/app/studio/review-sketch.tsx +47 -0
- package/app/studio/say-it.tsx +49 -0
- package/app/studio/step-work.tsx +134 -0
- package/app/studio/story.tsx +394 -0
- package/app/studio/style-menu.tsx +81 -0
- package/app/studio/style.css +1390 -0
- package/app/studio/sure.tsx +54 -0
- package/app/studio/tip.tsx +74 -0
- package/app/studio/tsconfig.json +5 -0
- package/app/studio/video.tsx +73 -0
- package/app/studio/world.tsx +188 -0
- package/app/tsconfig.json +13 -0
- package/app/web/audio.ts +315 -0
- package/app/web/face.ts +131 -0
- package/app/web/face3d.ts +35 -0
- package/app/web/glows.ts +387 -0
- package/app/web/hero3d.ts +728 -0
- package/app/web/index.html +118 -0
- package/app/web/living.ts +404 -0
- package/app/web/main.ts +3751 -0
- package/app/web/pad.ts +174 -0
- package/app/web/reactions.ts +79 -0
- package/app/web/sheet.ts +36 -0
- package/app/web/synth.ts +140 -0
- package/app/web/tsconfig.json +5 -0
- package/app/web/ui.ts +263 -0
- package/app/web/weather.ts +306 -0
- package/app/web/words.ts +12 -0
- package/docs/characters.md +58 -0
- package/docs/npc-motion.md +69 -0
- package/docs/scenes.md +75 -0
- package/docs/setup.md +60 -0
- package/docs/sound.md +23 -0
- package/docs/studio.md +62 -0
- package/package.json +70 -4
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// Where the page is, and whether it follows the agent: shared by the atelier's rail, the bell and the world.
|
|
2
|
+
import { signal } from '@preact/signals';
|
|
3
|
+
|
|
4
|
+
export const route = signal(location.pathname);
|
|
5
|
+
export const followAgent = signal(true);
|
|
6
|
+
/** The world the agent moved the view away from, so the person can go back. */
|
|
7
|
+
export const movedFrom = signal<string | null>(null);
|
|
8
|
+
|
|
9
|
+
export function go(path: string) {
|
|
10
|
+
if (path === route.value) return;
|
|
11
|
+
history.pushState(path === '/setup' ? { setupFrom: route.value } : null, '', path);
|
|
12
|
+
route.value = path;
|
|
13
|
+
}
|
|
14
|
+
addEventListener('popstate', () => (route.value = location.pathname));
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
// The place up close: the painting (or its masks or walk review), and tools to fix what the pipeline got
|
|
2
|
+
// wrong. Every tool is an action that writes corrections.json and rebuilds the place, so a fix survives
|
|
3
|
+
// the next build and the agent can do the same with words.
|
|
4
|
+
import { useEffect, useRef, useState } from 'preact/hooks';
|
|
5
|
+
import { call, fileUrl } from './api.ts';
|
|
6
|
+
import { named } from './build.tsx';
|
|
7
|
+
|
|
8
|
+
const W = 1200;
|
|
9
|
+
const H = 896;
|
|
10
|
+
const KINDS = ['standing', 'wall', 'on-top', 'walk-on', 'flat'] as const;
|
|
11
|
+
|
|
12
|
+
type Pic = { file: string; v: number | null } | null;
|
|
13
|
+
type ThingRow = { key: string; kind: string; phrase: string; source: string; coarse: boolean; brushed?: boolean; stand: [number, number] | null; removable?: boolean; why?: string };
|
|
14
|
+
type FloorRow = { key: string; source: string; coarse: boolean; brushed?: boolean };
|
|
15
|
+
type Stroke = { mode: 'add' | 'erase'; radius: number; points: [number, number][] };
|
|
16
|
+
type Fixes = {
|
|
17
|
+
things: Record<string, { phrase?: string; box?: number[]; kind?: string; strokes: Stroke[]; stand?: [number, number]; removed?: boolean }>;
|
|
18
|
+
added?: Record<string, { looks: string }>;
|
|
19
|
+
floors: Record<string, { add: string[]; remove: string[]; strokes: Stroke[] }>;
|
|
20
|
+
repaint: Array<{ region: number[]; fix: string }>;
|
|
21
|
+
};
|
|
22
|
+
export type PlaceStatus = {
|
|
23
|
+
playable: boolean | null;
|
|
24
|
+
errors: Array<{ code: string; key?: string; detail: string; play?: { look: string; tools: string[]; last?: string } }>;
|
|
25
|
+
flags: Array<{ code: string; key?: string; detail: string }>;
|
|
26
|
+
place: string;
|
|
27
|
+
pictures: { painting: Pic; masks: Pic; walk: Pic };
|
|
28
|
+
things: ThingRow[];
|
|
29
|
+
floors: FloorRow[];
|
|
30
|
+
flagged: string[];
|
|
31
|
+
start: [number, number] | null;
|
|
32
|
+
corrections: Fixes;
|
|
33
|
+
npcs: { key: string; box: Rect; at: [number, number] }[];
|
|
34
|
+
};
|
|
35
|
+
type Tool = 'look' | 'note' | 'box' | 'repaint' | 'brush' | 'stand' | 'move';
|
|
36
|
+
type Rect = [number, number, number, number];
|
|
37
|
+
|
|
38
|
+
const TOOLS: Array<[Tool, string, string]> = [
|
|
39
|
+
['look', 'Look', 'Just look, clicks change nothing'],
|
|
40
|
+
['note', 'Note', 'Click a spot and say what is wrong; the note waits for you or the agent. Free'],
|
|
41
|
+
['box', 'Box', 'Drag a box around where a thing really is, so SAM finds it there. 1 SAM call'],
|
|
42
|
+
['brush', 'Brush', 'Paint onto or rub out of a thing\'s or a floor\'s mask. Free, saved answers are reused'],
|
|
43
|
+
['stand', 'Stand', 'Click where the hero should stand to use a thing. Free'],
|
|
44
|
+
['move', 'Move NPC', 'Click where an NPC should stand (its feet); it is sized for the depth there. Free, no repaint'],
|
|
45
|
+
['repaint', 'Repaint', 'Drag over a region and say how Muse should paint it. 1 Muse Image edit'],
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
const LAYER_TIPS: Record<string, string> = {
|
|
49
|
+
painting: 'The painting Muse made',
|
|
50
|
+
masks: 'What SAM cut out: each thing and floor in its own colour',
|
|
51
|
+
walk: 'Where the hero can walk, and how big they are at each spot',
|
|
52
|
+
};
|
|
53
|
+
const KIND_TIPS: Record<string, string> = {
|
|
54
|
+
standing: 'Standing: people walk around and behind it. Changing the kind is free',
|
|
55
|
+
wall: 'Wall: part of the backdrop. Changing the kind is free',
|
|
56
|
+
'on-top': 'On top of something else. Changing the kind is free',
|
|
57
|
+
'walk-on': 'Walk-on: people walk over it. Changing the kind is free',
|
|
58
|
+
flat: 'Flat: lies on the floor, never in front of anyone. Changing the kind is free',
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const norm = ([a, b, c, d]: Rect): Rect => [Math.round(Math.min(a, c)), Math.round(Math.min(b, d)), Math.round(Math.max(a, c)), Math.round(Math.max(b, d))];
|
|
62
|
+
|
|
63
|
+
export function PlaceEditor({ slug, place, version, onError, focus }: { slug: string; place: string; version: number; onError: (m: string) => void; focus?: string }) {
|
|
64
|
+
const [st, setSt] = useState<PlaceStatus | null>(null);
|
|
65
|
+
// Opened on one thing (its Details on the stage): its mask shows and its row is in view.
|
|
66
|
+
const [layer, setLayer] = useState<'painting' | 'masks' | 'walk'>(focus ? 'masks' : 'painting');
|
|
67
|
+
const [tool, setTool] = useState<Tool>('look');
|
|
68
|
+
const [target, setTarget] = useState('');
|
|
69
|
+
const [mode, setMode] = useState<'add' | 'erase'>('add');
|
|
70
|
+
const [radius, setRadius] = useState(14);
|
|
71
|
+
const [strokes, setStrokes] = useState<Stroke[]>([]);
|
|
72
|
+
const [drag, setDrag] = useState<Rect | null>(null);
|
|
73
|
+
const [point, setPoint] = useState<[number, number] | null>(null);
|
|
74
|
+
const [text, setText] = useState('');
|
|
75
|
+
const [busy, setBusy] = useState(false);
|
|
76
|
+
const svg = useRef<SVGSVGElement>(null);
|
|
77
|
+
const down = useRef(false);
|
|
78
|
+
|
|
79
|
+
const load = () => call<PlaceStatus>('place_status', { slug, place }).then(setSt).catch((e) => onError(e.message));
|
|
80
|
+
useEffect(() => void load(), [slug, place, version]);
|
|
81
|
+
useEffect(() => {
|
|
82
|
+
setDrag(null);
|
|
83
|
+
setPoint(null);
|
|
84
|
+
setStrokes([]);
|
|
85
|
+
setText('');
|
|
86
|
+
}, [tool, place]);
|
|
87
|
+
|
|
88
|
+
const things = st?.things ?? [];
|
|
89
|
+
useEffect(() => {
|
|
90
|
+
if (focus && st) document.getElementById(`thing-${focus}`)?.scrollIntoView({ block: 'center' });
|
|
91
|
+
}, [focus, !!st]);
|
|
92
|
+
const floors = st?.floors ?? [];
|
|
93
|
+
const isFloor = floors.some((f) => f.key === target);
|
|
94
|
+
const npcs = st?.npcs ?? [];
|
|
95
|
+
const targets = tool === 'brush' ? [...things.map((t) => t.key), ...floors.map((f) => f.key)] : tool === 'move' ? npcs.map((n) => n.key) : things.map((t) => t.key);
|
|
96
|
+
useEffect(() => {
|
|
97
|
+
if (targets.length && !targets.includes(target)) setTarget(targets[0]);
|
|
98
|
+
}, [tool, st]);
|
|
99
|
+
|
|
100
|
+
const at = (e: PointerEvent): [number, number] => {
|
|
101
|
+
const r = svg.current!.getBoundingClientRect();
|
|
102
|
+
return [Math.round(((e.clientX - r.left) / r.width) * W), Math.round(((e.clientY - r.top) / r.height) * H)];
|
|
103
|
+
};
|
|
104
|
+
const onDown = (e: PointerEvent) => {
|
|
105
|
+
if (tool === 'look') return;
|
|
106
|
+
(e.currentTarget as Element).setPointerCapture(e.pointerId);
|
|
107
|
+
const p = at(e);
|
|
108
|
+
down.current = true;
|
|
109
|
+
if (tool === 'box' || tool === 'repaint') setDrag([p[0], p[1], p[0], p[1]]);
|
|
110
|
+
if (tool === 'brush') setStrokes((s) => [...s, { mode, radius, points: [p] }]);
|
|
111
|
+
if (tool === 'note' || tool === 'stand' || tool === 'move') setPoint(p);
|
|
112
|
+
};
|
|
113
|
+
const onMove = (e: PointerEvent) => {
|
|
114
|
+
if (!down.current) return;
|
|
115
|
+
const p = at(e);
|
|
116
|
+
if (tool === 'box' || tool === 'repaint') setDrag((d) => d && [d[0], d[1], p[0], p[1]]);
|
|
117
|
+
if (tool === 'brush') setStrokes((s) => s.map((k, i) => (i === s.length - 1 ? { ...k, points: [...k.points, p] } : k)));
|
|
118
|
+
};
|
|
119
|
+
const onUp = () => (down.current = false);
|
|
120
|
+
|
|
121
|
+
const act = async (name: string, input: object, after?: () => void) => {
|
|
122
|
+
setBusy(true);
|
|
123
|
+
try {
|
|
124
|
+
await call(name, { slug, place, ...input });
|
|
125
|
+
after?.();
|
|
126
|
+
setDrag(null);
|
|
127
|
+
setPoint(null);
|
|
128
|
+
setStrokes([]);
|
|
129
|
+
setText('');
|
|
130
|
+
void load();
|
|
131
|
+
} catch (e) {
|
|
132
|
+
onError((e as Error).message);
|
|
133
|
+
}
|
|
134
|
+
setBusy(false);
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const pic = st?.pictures[layer] ?? st?.pictures.painting;
|
|
138
|
+
const box = drag && norm(drag);
|
|
139
|
+
const big = box && box[2] - box[0] > 8 && box[3] - box[1] > 8;
|
|
140
|
+
const fixes = st?.corrections;
|
|
141
|
+
const fixList = fixes ? [
|
|
142
|
+
...Object.entries(fixes.added ?? {}).map(([k, a]) => ({ key: k, what: `added: "${a.looks}"` })),
|
|
143
|
+
...Object.entries(fixes.things).map(([k, t]) => ({ key: k, what: [t.removed && 'taken off the list', t.phrase && `"${t.phrase}"`, t.kind, t.box && 'box', t.strokes.length && `${t.strokes.length} strokes`, t.stand && 'stand'].filter(Boolean).join(', ') })),
|
|
144
|
+
...Object.entries(fixes.floors).map(([k, f]) => ({ key: k, what: [f.add.length && `+${f.add.length} cells`, f.remove.length && `-${f.remove.length} cells`, f.strokes.length && `${f.strokes.length} strokes`].filter(Boolean).join(', ') })),
|
|
145
|
+
] : [];
|
|
146
|
+
|
|
147
|
+
return (
|
|
148
|
+
<section class="editor">
|
|
149
|
+
<section class="checks" aria-label="Place checks">
|
|
150
|
+
{st && <p class="detail">{st.playable === true ? 'Verified playable' : st.playable === false ? 'Needs a fix' : 'Not verified yet'}</p>}
|
|
151
|
+
{(st?.errors ?? []).map((f) => <div><p class="error"><code>{f.code}</code>{f.key ? ` ${f.key}` : ''}: {f.detail}</p>{f.play && <details><summary>How to fix it</summary><p>{f.play.look}</p><p>{f.play.tools.join(' · ')}</p>{f.play.last && <p>Last resort: {f.play.last}</p>}</details>}</div>)}
|
|
152
|
+
{(st?.flags ?? []).map((f) => <p class="flag"><code>{f.code}</code>{f.key ? ` ${f.key}` : ''}: {f.detail}</p>)}
|
|
153
|
+
</section>
|
|
154
|
+
<div class="row">
|
|
155
|
+
<h2>{place}</h2>
|
|
156
|
+
<span class="grow" />
|
|
157
|
+
<div class="seg" role="group" aria-label="Picture">
|
|
158
|
+
{(['painting', 'masks', 'walk'] as const).map((l) => <button type="button" title={named(LAYER_TIPS[l])} class={layer === l ? 'on' : ''} disabled={!st?.pictures[l]} onClick={() => setLayer(l)}>{l}</button>)}
|
|
159
|
+
</div>
|
|
160
|
+
</div>
|
|
161
|
+
<div class="seg tools" role="group" aria-label="Tool">
|
|
162
|
+
{TOOLS.map(([t, label, hint]) => <button type="button" title={named(hint)} class={tool === t ? 'on' : ''} onClick={() => setTool(t)}>{label}</button>)}
|
|
163
|
+
</div>
|
|
164
|
+
{tool !== 'look' && <p class="detail">{named(TOOLS.find(([t]) => t === tool)![2])}.</p>}
|
|
165
|
+
{(tool === 'box' || tool === 'brush' || tool === 'stand' || tool === 'move') && (
|
|
166
|
+
<div class="row">
|
|
167
|
+
<label title={tool === 'brush' ? 'The thing or floor whose mask the brush paints' : tool === 'move' ? 'The NPC to move' : 'The thing this tool fixes'}>{tool === 'brush' ? 'Mask of' : tool === 'move' ? 'NPC' : 'Thing'} <select value={target} onChange={(e) => setTarget(e.currentTarget.value)}>
|
|
168
|
+
{tool === 'brush' && floors.length > 0 && <optgroup label="Things">{things.map((t) => <option value={t.key}>{t.key}</option>)}</optgroup>}
|
|
169
|
+
{tool === 'brush' && floors.length > 0 ? <optgroup label="Floors">{floors.map((f) => <option value={f.key}>{f.key}</option>)}</optgroup> : targets.map((k) => <option value={k}>{k}</option>)}
|
|
170
|
+
</select></label>
|
|
171
|
+
{tool === 'brush' && <>
|
|
172
|
+
<div class="seg" role="group" aria-label="Brush"><button type="button" class={mode === 'add' ? 'on' : ''} title="Strokes add to the mask" onClick={() => setMode('add')}>Add</button><button type="button" class={mode === 'erase' ? 'on' : ''} title="Strokes rub out of the mask" onClick={() => setMode('erase')}>Erase</button></div>
|
|
173
|
+
<label title="How wide the brush is">Size <input type="range" min={3} max={60} value={radius} onInput={(e) => setRadius(Number(e.currentTarget.value))} /></label>
|
|
174
|
+
</>}
|
|
175
|
+
</div>
|
|
176
|
+
)}
|
|
177
|
+
<div class={`stage tool-${tool}`}>
|
|
178
|
+
{pic ? <img src={fileUrl(slug, pic.file, pic.v)} alt={`${place}, ${layer}`} draggable={false} /> : <div class="noimg">no picture yet</div>}
|
|
179
|
+
<svg ref={svg} viewBox={`0 0 ${W} ${H}`} onPointerDown={onDown} onPointerMove={onMove} onPointerUp={onUp} onPointerCancel={onUp}>
|
|
180
|
+
{things.map((t) => t.stand && <g><circle cx={t.stand[0]} cy={t.stand[1]} r={7} class="stand" /></g>)}
|
|
181
|
+
{tool === 'move' && npcs.map((n) => <rect x={n.box[0]} y={n.box[1]} width={n.box[2] - n.box[0]} height={n.box[3] - n.box[1]} class={n.key === target ? 'sel npc on' : 'sel npc'} />)}
|
|
182
|
+
{box && <rect x={box[0]} y={box[1]} width={box[2] - box[0]} height={box[3] - box[1]} class={tool === 'repaint' ? 'sel repaint' : 'sel'} />}
|
|
183
|
+
{strokes.map((s) => <polyline points={s.points.map((p) => p.join(',')).join(' ')} class={`stroke ${s.mode}`} stroke-width={s.radius * 2} />)}
|
|
184
|
+
{point && <circle cx={point[0]} cy={point[1]} r={10} class="pin" />}
|
|
185
|
+
</svg>
|
|
186
|
+
</div>
|
|
187
|
+
{tool === 'box' && big && <div class="row"><span class="detail">{target} is in [{box!.join(', ')}]</span><span class="grow" /><button type="button" class="primary small" disabled={busy || !target} title="SAM looks for the thing inside this box only, then the place is checked again. 1 SAM call" onClick={() => act('mask_fix', { thing: target, box })}>Find it here</button></div>}
|
|
188
|
+
{tool === 'stand' && point && <div class="row"><span class="detail">Stand at {point.join(', ')} to use {target}</span><span class="grow" /><button type="button" class="primary small" disabled={busy || !target} title="The hero walks here to use it. Kept across rebuilds. Free" onClick={() => act('stand_set', { thing: target, at: point })}>Set stand spot</button></div>}
|
|
189
|
+
{tool === 'move' && npcs.length === 0 && <p class="detail">No one stands in this place.</p>}
|
|
190
|
+
{tool === 'move' && point && <div class="row"><span class="detail">{target} stands with feet at {point.join(', ')}</span><span class="grow" /><button type="button" class="primary small" disabled={busy || !target} title="The NPC stands here, sized for the depth. Kept across rebuilds. Free, no repaint" onClick={() => act('npc_move', { npc: target, at: point })}>Move here</button></div>}
|
|
191
|
+
{tool === 'brush' && strokes.length > 0 && <div class="row"><span class="detail">{strokes.length} stroke{strokes.length > 1 ? 's' : ''} on {target}</span><span class="grow" /><button type="button" class="ghost small" title="Take back the last stroke" onClick={() => setStrokes((s) => s.slice(0, -1))}>Undo stroke</button><button type="button" class="primary small" disabled={busy || !target} title="Keep the strokes and rebuild this place from its masks. Kept across rebuilds. Free, saved answers are reused" onClick={() => act('mask_brush', { layer: isFloor ? 'floor' : 'thing', key: target, strokes })}>Apply and rebuild</button></div>}
|
|
192
|
+
{((tool === 'note' && point) || (tool === 'repaint' && big)) && (
|
|
193
|
+
<form class="say" onSubmit={(e) => { e.preventDefault(); if (tool === 'note') void act('feedback_add', { point, text: text.trim() }); else void act('repaint_region', { region: box, fix: text.trim() }); }}>
|
|
194
|
+
<textarea rows={2} placeholder={tool === 'note' ? 'What is wrong here?' : named('How should Muse paint this region?')} value={text} onInput={(e) => setText(e.currentTarget.value)} />
|
|
195
|
+
<div class="row"><span class="grow" /><button type="submit" class="primary small" disabled={busy || text.trim().length < 3} title={tool === 'note' ? 'Keep the note on this spot, for you or the agent. Free' : named('Muse repaints only this region your way, then the place is measured again. 1 Muse Image edit')}>{tool === 'note' ? 'Leave note' : named('Repaint (1 Muse edit)')}</button></div>
|
|
196
|
+
</form>
|
|
197
|
+
)}
|
|
198
|
+
{things.length > 0 && (
|
|
199
|
+
<table class="things">
|
|
200
|
+
<thead><tr><th>Thing</th><th>Kind</th><th>SAM looks for</th><th>Mask</th><th /></tr></thead>
|
|
201
|
+
<tbody>
|
|
202
|
+
{things.map((t) => <ThingLine key={t.key} t={t} focused={t.key === focus} busy={busy} fix={(input) => act('mask_fix', { thing: t.key, ...input })} remove={(paintOut) => act('thing_remove', { thing: t.key, paintOut })} />)}
|
|
203
|
+
<AddThing busy={busy} add={(looks, kind) => act('thing_add', { looks, ...(kind && { kind }) })} />
|
|
204
|
+
</tbody>
|
|
205
|
+
</table>
|
|
206
|
+
)}
|
|
207
|
+
{floors.some((f) => f.coarse) && <p class="flag">Coarse floors: {floors.filter((f) => f.coarse).map((f) => f.key).join(', ')}. Brush them, or ask the agent to fix their cells.</p>}
|
|
208
|
+
{(fixList.length > 0 || (fixes?.repaint.length ?? 0) > 0) && (
|
|
209
|
+
<div class="fixes">
|
|
210
|
+
<div class="row"><strong>Your fixes</strong><span class="detail">kept across rebuilds</span><span class="grow" /><button type="button" class="ghost small" disabled={busy} title={named((fixes?.repaint.length ?? 0) > 0 ? 'Take back every fix of this place. It is painted again: 1 Muse Image call' : 'Take back every fix of this place and rebuild it from its masks. Free')} onClick={() => act('corrections_clear', {})}>Clear all</button></div>
|
|
211
|
+
<ul>
|
|
212
|
+
{fixList.map((f) => <li><code>{f.key}</code> {f.what}<button type="button" class="ghost small" disabled={busy} title={`Take back the fixes of ${f.key} and rebuild this place from its masks. Free`} onClick={() => act('corrections_clear', { key: f.key })}>Clear</button></li>)}
|
|
213
|
+
{fixes!.repaint.map((r) => <li>repaint [{r.region.join(', ')}]: {r.fix}</li>)}
|
|
214
|
+
</ul>
|
|
215
|
+
</div>
|
|
216
|
+
)}
|
|
217
|
+
</section>
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** The last row of the things: something painted that should be a thing of its own. */
|
|
222
|
+
function AddThing({ busy, add }: { busy: boolean; add: (looks: string, kind: string) => void }) {
|
|
223
|
+
const [looks, setLooks] = useState('');
|
|
224
|
+
const [kind, setKind] = useState('');
|
|
225
|
+
const ok = looks.trim().length >= 3;
|
|
226
|
+
return (
|
|
227
|
+
<tr class="add-thing">
|
|
228
|
+
<td colSpan={2}><select value={kind} disabled={busy} title="How it meets the floor; Spark decides when left empty" onChange={(e) => setKind(e.currentTarget.value)}><option value="">kind: Spark decides</option>{KINDS.map((k) => <option value={k}>{k}</option>)}</select></td>
|
|
229
|
+
<td><form class="row" onSubmit={(e) => (e.preventDefault(), ok && (add(looks.trim(), kind), setLooks('')))}><input value={looks} placeholder="Add a thing that is painted here, e.g. the red bench" onInput={(e) => setLooks(e.currentTarget.value)} />{ok && <button type="submit" class="primary small" disabled={busy} title="It becomes a thing of its own: Spark finds it, SAM cuts it out, the place is checked again. 1 Spark and 1 SAM call">Add</button>}</form></td>
|
|
230
|
+
<td colSpan={2} />
|
|
231
|
+
</tr>
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function ThingLine({ t, focused, busy, fix, remove }: { t: ThingRow; focused?: boolean; busy: boolean; fix: (input: object) => void; remove: (paintOut: boolean) => void }) {
|
|
236
|
+
const [asking, setAsking] = useState(false);
|
|
237
|
+
const [phrase, setPhrase] = useState(t.phrase);
|
|
238
|
+
useEffect(() => setPhrase(t.phrase), [t.phrase]);
|
|
239
|
+
return (
|
|
240
|
+
<>
|
|
241
|
+
<tr id={`thing-${t.key}`} class={focused ? 'focus' : undefined}>
|
|
242
|
+
<td><code>{t.key}</code></td>
|
|
243
|
+
<td><select value={t.kind} disabled={busy} title={KIND_TIPS[t.kind] ?? 'What kind of thing it is'} onChange={(e) => fix({ kind: e.currentTarget.value })}>{KINDS.map((k) => <option value={k}>{k}</option>)}</select></td>
|
|
244
|
+
<td><form class="row" onSubmit={(e) => (e.preventDefault(), phrase.trim() !== t.phrase && fix({ phrase: phrase.trim() }))}><input value={phrase} title="The words SAM searches the painting for. Change them and press Search" onInput={(e) => setPhrase(e.currentTarget.value)} />{phrase.trim() !== t.phrase && phrase.trim().length > 1 && <button type="submit" class="primary small" disabled={busy} title="SAM looks for the thing by these words. 1 SAM call">Search</button>}</form></td>
|
|
245
|
+
<td><span class={`badge ${t.coarse ? 'fail' : 'ok'}`} title={t.coarse ? 'SAM did not find it: the mask is only a box' : undefined}>{t.source}{t.brushed ? ' + brush' : ''}</span></td>
|
|
246
|
+
<td class="remove">
|
|
247
|
+
<button type="button" class={`ghost small${asking ? ' on' : ''}`} disabled={busy || t.removable === false} title={t.removable === false ? `Can't remove: ${t.why}` : 'Take it off the list'} aria-label={`Remove ${t.key}`} onClick={() => setAsking(!asking)}>×</button>
|
|
248
|
+
</td>
|
|
249
|
+
</tr>
|
|
250
|
+
{asking && (
|
|
251
|
+
<tr class="confirm">
|
|
252
|
+
<td colSpan={5}>
|
|
253
|
+
<span class="row">
|
|
254
|
+
<span class="detail">Take <code>{t.key}</code> off the list:</span>
|
|
255
|
+
<button type="button" class="ghost small" disabled={busy} title="It stays in the picture, but is no longer a thing of its own. Free" onClick={() => (setAsking(false), remove(false))}>Keep it painted</button>
|
|
256
|
+
<button type="button" class="ghost small danger" disabled={busy} title={named('Muse paints it out of the picture, then the place is checked again. 1 Muse Image edit')} onClick={() => (setAsking(false), remove(true))}>Paint it out</button>
|
|
257
|
+
<button type="button" class="ghost small" onClick={() => setAsking(false)}>Cancel</button>
|
|
258
|
+
</span>
|
|
259
|
+
</td>
|
|
260
|
+
</tr>
|
|
261
|
+
)}
|
|
262
|
+
</>
|
|
263
|
+
);
|
|
264
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { useId } from 'preact/hooks';
|
|
2
|
+
|
|
3
|
+
/** A small paper theatre while the model works, not a preview of the user's game. */
|
|
4
|
+
export function ReviewSketch({ compact = false }: { compact?: boolean }) {
|
|
5
|
+
const clip = useId();
|
|
6
|
+
return <span class={`review-sketch${compact ? ' compact' : ''}`} aria-hidden="true">
|
|
7
|
+
<svg viewBox="0 0 440 320" fill="none">
|
|
8
|
+
<defs><clipPath id={clip}><path d="M130 256V131a90 90 0 0 1 180 0v125Z" /></clipPath></defs>
|
|
9
|
+
<ellipse cx="224" cy="284" rx="139" ry="11" fill="#746448" opacity=".07" />
|
|
10
|
+
<g class="sketch-leaf sketch-leaf-left">
|
|
11
|
+
<path d="m57 99 94-17 26 161-94 17Z" fill="#e4d9c5" />
|
|
12
|
+
<path d="m53 93 94-17 26 161-94 17Z" fill="#fbf7ed" stroke="#d8cbb5" />
|
|
13
|
+
<path d="m85 207 12-69 21-5 21 63M89 180l39-7m-23-42-3-17m-12 9 23-4" stroke="#716e58" stroke-width="1.5" stroke-linecap="round" class="sketch-ink" pathLength="1" />
|
|
14
|
+
<circle cx="106" cy="112" r="11" stroke="#716e58" stroke-width="1.5" />
|
|
15
|
+
<path d="m90 228 48-9m-45 15 28-5" stroke="#d8cbb5" stroke-width="2" stroke-linecap="round" />
|
|
16
|
+
</g>
|
|
17
|
+
<g class="sketch-leaf sketch-leaf-right">
|
|
18
|
+
<path d="m287 96 100 19-28 145-100-19Z" fill="#e4d9c5" />
|
|
19
|
+
<path d="m291 89 100 19-28 145-100-19Z" fill="#fbf7ed" stroke="#d8cbb5" />
|
|
20
|
+
<circle cx="345" cy="137" r="17" fill="#d4a659" opacity=".6" />
|
|
21
|
+
<path d="m290 193 29-29 15 20 15-7 16 30m-69-58 12 3m-25 62 63 12m-66-4 44 9" stroke="#8e9986" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
|
|
22
|
+
</g>
|
|
23
|
+
<g class="sketch-theatre">
|
|
24
|
+
<path d="M116 274V131a104 104 0 0 1 208 0v143Z" fill="#dacbb3" opacity=".5" transform="translate(3 5)" />
|
|
25
|
+
<path d="M116 274V131a104 104 0 0 1 208 0v143Z" fill="#fbf7ed" stroke="#cdbc9f" />
|
|
26
|
+
<g clip-path={`url(#${clip})`}>
|
|
27
|
+
<path d="M130 40h180v216H130Z" fill="#dce3df" />
|
|
28
|
+
<circle class="sketch-sun" cx="258" cy="107" r="24" fill="#c88b39" opacity=".8" />
|
|
29
|
+
<path d="M114 199q50-61 105-15t105-22v101H114Z" fill="#a7b3a0" />
|
|
30
|
+
<path d="M114 231q71-66 133-17t80-7v62H114Z" fill="#768778" />
|
|
31
|
+
<path d="M159 258q90-50 51-67-18-11 7-19" stroke="#efe6cf" stroke-width="15" />
|
|
32
|
+
<path d="M146 228q46-30 82-12m13 13 49 18" stroke="#526654" opacity=".35" stroke-linecap="round" />
|
|
33
|
+
<path d="M166 191v-48l20-15 20 15v45m-29 0v-26h13v26m-29-43 25-22 26 22" stroke="#4e584b" stroke-width="1.6" stroke-linejoin="round" stroke-linecap="round" class="sketch-ink" pathLength="1" />
|
|
34
|
+
<path d="m275 222-1-55m-17 25q-8-16 5-23-1-22 15-23 22 2 14 23 15 10 3 25-20 14-37-2Z" stroke="#4e584b" stroke-width="1.6" stroke-linejoin="round" class="sketch-ink sketch-ink-late" pathLength="1" />
|
|
35
|
+
</g>
|
|
36
|
+
<path d="M130 256V131a90 90 0 0 1 180 0v125Z" stroke="#cabba3" />
|
|
37
|
+
<path d="M201 265h38" stroke="#cdbc9f" stroke-linecap="round" />
|
|
38
|
+
</g>
|
|
39
|
+
<g class="sketch-pencil" transform="translate(307 238) rotate(32)">
|
|
40
|
+
<path d="M0-59h7v68H0Z" fill="#bd7b2b" /><path d="M0-59h2v68H0Z" fill="#e1b571" />
|
|
41
|
+
<path d="m0 9 3.5 13L7 9Z" fill="#dfc8a2" /><path d="m2 17 1.5 5L5 17Z" fill="#484336" />
|
|
42
|
+
<path d="M0-61q3.5-4 7 0v5H0Z" fill="#7c8370" />
|
|
43
|
+
</g>
|
|
44
|
+
<path d="m350 58 1 12m-6-6 12-1m-281 2 2 9m-6-4 10-1" stroke="#c39a5e" stroke-width="1.5" stroke-linecap="round" opacity=".7" />
|
|
45
|
+
</svg>
|
|
46
|
+
</span>;
|
|
47
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// Speaking instead of typing: the browser records, Muse Voice Transcribe (idea_hear) writes it down.
|
|
2
|
+
import { useEffect, useState } from 'preact/hooks';
|
|
3
|
+
import { call } from './api.ts';
|
|
4
|
+
import { base64 } from './camera.tsx';
|
|
5
|
+
import { Icon } from './icons.tsx';
|
|
6
|
+
|
|
7
|
+
/** The mic: press to speak, press again; what was said joins the text (the idea, or the message to the agent). */
|
|
8
|
+
export function SayIt({ onText, onError, tip = 'Say the idea instead of typing it. Muse Voice Transcribe writes it here: 1 call', small }: { onText: (text: string) => void; onError: (m: string) => void; tip?: string; small?: boolean }) {
|
|
9
|
+
const [rec, setRec] = useState<MediaRecorder | null>(null);
|
|
10
|
+
const [hearing, setHearing] = useState(false);
|
|
11
|
+
const [seconds, setSeconds] = useState(0);
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
if (!rec) return;
|
|
14
|
+
const started = Date.now();
|
|
15
|
+
const timer = setInterval(() => setSeconds(Math.round((Date.now() - started) / 1000)), 250);
|
|
16
|
+
return () => clearInterval(timer);
|
|
17
|
+
}, [rec]);
|
|
18
|
+
const start = async () => {
|
|
19
|
+
try {
|
|
20
|
+
const stream = await navigator.mediaDevices.getUserMedia({ audio: { echoCancellation: true, noiseSuppression: true } });
|
|
21
|
+
const type = ['audio/webm;codecs=opus', 'audio/mp4', 'audio/webm'].find((t) => MediaRecorder.isTypeSupported(t)) ?? '';
|
|
22
|
+
const r = new MediaRecorder(stream, type ? { mimeType: type } : {});
|
|
23
|
+
const chunks: Blob[] = [];
|
|
24
|
+
r.ondataavailable = (e) => e.data.size && chunks.push(e.data);
|
|
25
|
+
r.onstop = async () => {
|
|
26
|
+
stream.getTracks().forEach((t) => t.stop());
|
|
27
|
+
setHearing(true);
|
|
28
|
+
try {
|
|
29
|
+
const said = await call<{ text: string }>('idea_hear', { name: `idea.${r.mimeType.includes('mp4') ? 'm4a' : 'webm'}`, data: await base64(new Blob(chunks, { type: r.mimeType })) });
|
|
30
|
+
if (said.text) onText(said.text);
|
|
31
|
+
} catch (e) {
|
|
32
|
+
onError((e as Error).message);
|
|
33
|
+
} finally {
|
|
34
|
+
setHearing(false);
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
r.start(1000);
|
|
38
|
+
setSeconds(0);
|
|
39
|
+
setRec(r);
|
|
40
|
+
} catch (e) {
|
|
41
|
+
onError(`No microphone: ${(e as Error).message}`);
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
return (
|
|
45
|
+
<button type="button" class={`ghost ${small ? 'small' : 'bare'}${rec ? ' rec' : ''}`} disabled={hearing} title={rec ? 'Stop: what you said is written in' : tip} onClick={rec ? () => (rec.stop(), setRec(null)) : start}>
|
|
46
|
+
{hearing ? 'Hearing…' : rec ? `■ Stop · ${seconds} s` : <><Icon name="mic" /> Say it</>}
|
|
47
|
+
</button>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
// A world-wide build step at work, live: what it works from and what it made so far, card by card. Item icons show
|
|
2
|
+
// where SAM cuts each item from (the painting around it, the mask lit) and the icon once it lands; plates show what
|
|
3
|
+
// goes and the place without it; weather and reactions show each place and what was read for it; sounds list every
|
|
4
|
+
// sound to listen to. The build bar opens it from that step's segment.
|
|
5
|
+
import { useEffect, useState } from 'preact/hooks';
|
|
6
|
+
import { call, fileUrl } from './api.ts';
|
|
7
|
+
import { named } from './build.tsx';
|
|
8
|
+
import { nice } from './world.tsx';
|
|
9
|
+
|
|
10
|
+
type Pic = { file: string; v: number | null };
|
|
11
|
+
type Shot = { painting: Pic; box?: [number, number, number, number]; mask?: Pic; over?: Pic };
|
|
12
|
+
type WorkCard = { key: string; title: string; place?: string; from: Shot | null; made: Shot | Pic | null; words: string[]; at: number | null };
|
|
13
|
+
type Sound = { id: string; kind: string; prompt: string; state: string; file?: string; seconds?: number; rejected?: string };
|
|
14
|
+
|
|
15
|
+
/** The steps this view shows; the cast steps have their own (cast-work.tsx). */
|
|
16
|
+
export const WORK_STEPS = ['icons', 'plates', 'weather', 'reactions', 'sound'];
|
|
17
|
+
const WHAT: Record<string, string> = { icons: 'Item icons', plates: 'Change plates', weather: 'Weather', reactions: 'Reactions', sound: 'Sounds' };
|
|
18
|
+
const FROM: Record<string, string> = { icons: 'cut from', plates: 'what changes', weather: 'reads', reactions: 'reads' };
|
|
19
|
+
const MADE: Record<string, string> = { icons: 'icon', plates: 'after', weather: 'weather', reactions: 'reactions' };
|
|
20
|
+
const WAIT: Record<string, string> = {
|
|
21
|
+
icons: 'Reading the story\'s items…', plates: 'Reading what the hero takes and what changes…', weather: 'Reading the places…', reactions: 'Reading the places…', sound: 'Reading the sound plan…',
|
|
22
|
+
};
|
|
23
|
+
const NONE: Record<string, string> = {
|
|
24
|
+
icons: 'The story has no items to pick up, so no icons.', plates: 'Nothing is taken or changed in any place, so no plates.',
|
|
25
|
+
weather: 'No places painted yet.', reactions: 'No places painted yet.', sound: 'No sound plan yet: the sound step writes it.',
|
|
26
|
+
};
|
|
27
|
+
const FRAME = { width: 1200, height: 896 };
|
|
28
|
+
|
|
29
|
+
const clock = (s: number) => `${Math.floor(Math.max(0, s) / 60)}:${String(Math.round(Math.max(0, s)) % 60).padStart(2, '0')}`;
|
|
30
|
+
|
|
31
|
+
/** A place's painting cropped to a box, the SAM mask over it (the rest dimmed), or a plate laid over it. */
|
|
32
|
+
function Crop({ slug, shot, alt, title }: { slug: string; shot: Shot; alt: string; title: string }) {
|
|
33
|
+
const [x0, y0, x1, y1] = shot.box ?? [0, 0, 1, 1];
|
|
34
|
+
const [bw, bh] = [x1 - x0, y1 - y0];
|
|
35
|
+
const place = { width: `${100 / bw}%`, left: `${(-x0 / bw) * 100}%`, top: `${(-y0 / bh) * 100}%` };
|
|
36
|
+
const big = fileUrl(slug, (shot.over ?? shot.painting).file, (shot.over ?? shot.painting).v);
|
|
37
|
+
return (
|
|
38
|
+
<div class="sw-crop" style={{ aspectRatio: `${bw * FRAME.width} / ${bh * FRAME.height}` }} data-lightbox={shot.over ? undefined : big} title={title}>
|
|
39
|
+
<img class={shot.mask ? 'dim' : ''} style={place} src={fileUrl(slug, shot.painting.file, shot.painting.v)} alt={alt} />
|
|
40
|
+
{shot.over && <img style={place} src={fileUrl(slug, shot.over.file, shot.over.v)} alt="" />}
|
|
41
|
+
{/* What SAM cut out, lit: the painting again, seen only through its mask. */}
|
|
42
|
+
{shot.mask && <img class="cut" style={{ ...place, maskImage: `url("${fileUrl(slug, shot.mask.file, shot.mask.v)}")` }} src={fileUrl(slug, shot.painting.file, shot.painting.v)} alt="" />}
|
|
43
|
+
</div>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const isShot = (x: Shot | Pic): x is Shot => 'painting' in x;
|
|
48
|
+
|
|
49
|
+
export function StepWorkStage({ slug, step, running, failed, since, took, version }: {
|
|
50
|
+
slug: string; step: string; running: boolean; failed?: string; since?: number; took: number; version: number;
|
|
51
|
+
}) {
|
|
52
|
+
const [cards, setCards] = useState<WorkCard[] | null>(null);
|
|
53
|
+
const [sounds, setSounds] = useState<Sound[] | null>(null);
|
|
54
|
+
useEffect(() => {
|
|
55
|
+
if (step === 'sound') void call<Sound[]>('sound_status', { slug }).then(setSounds).catch(() => setSounds([]));
|
|
56
|
+
else void call<WorkCard[]>('step_work', { slug, step }).then(setCards).catch(() => setCards([]));
|
|
57
|
+
}, [slug, step, version]);
|
|
58
|
+
const [now, setNow] = useState(Date.now());
|
|
59
|
+
useEffect(() => {
|
|
60
|
+
if (!running) return;
|
|
61
|
+
const t = setInterval(() => setNow(Date.now()), 1000);
|
|
62
|
+
return () => clearInterval(t);
|
|
63
|
+
}, [running]);
|
|
64
|
+
const elapsed = since ? (now - since) / 1000 : 0;
|
|
65
|
+
const left = took - elapsed;
|
|
66
|
+
// While the step runs, a result older than its start is the earlier build's: it is being made again.
|
|
67
|
+
const fresh = (at: number | null) => !!at && (!running || !since || at >= since);
|
|
68
|
+
const list = step === 'sound' ? sounds : cards;
|
|
69
|
+
const doneCount = step === 'sound' ? (sounds ?? []).filter((s) => s.state === 'made').length : (cards ?? []).filter((c) => c.made && fresh(c.at)).length;
|
|
70
|
+
|
|
71
|
+
return (
|
|
72
|
+
<div class="cast-work step-work" data-lightbox-group>
|
|
73
|
+
<div class="cw-head">
|
|
74
|
+
<strong>{WHAT[step] ?? step}</strong>
|
|
75
|
+
<span class="detail">
|
|
76
|
+
{running ? (since ? `${clock(elapsed)} so far · ${left > 5 ? `about ${clock(left)} left` : 'a little longer than usual'}` : 'starting…') : failed ? `stopped: ${failed}` : 'done'}
|
|
77
|
+
{list && list.length > 0 ? ` · ${doneCount} of ${list.length} made` : ''}
|
|
78
|
+
</span>
|
|
79
|
+
</div>
|
|
80
|
+
{list === null && <div class="cw-wait"><span>{WAIT[step] ?? 'Reading…'}</span></div>}
|
|
81
|
+
{list !== null && list.length === 0 && <div class="cw-wait"><span>{NONE[step] ?? 'Nothing here.'}</span></div>}
|
|
82
|
+
{step === 'sound' && sounds && sounds.length > 0 && (
|
|
83
|
+
<ul class="sw-sounds">
|
|
84
|
+
{sounds.map((s) => (
|
|
85
|
+
<li class={s.state === 'made' ? 'ok' : s.state === 'rejected' ? 'fail' : ''}>
|
|
86
|
+
<span class="kind">{s.kind}</span>
|
|
87
|
+
<span class="what" title={s.prompt}><strong>{nice(s.id.replace(/^[a-z]+:/, ''))}</strong> <span class="detail">{s.prompt}</span></span>
|
|
88
|
+
{s.state === 'made' && s.file
|
|
89
|
+
? <audio controls preload="none" src={fileUrl(slug, s.file, version)} title={`${s.seconds ?? ''} s. Free to listen`} />
|
|
90
|
+
: <span class="detail">{s.state === 'rejected' ? `✕ ${s.rejected}` : running ? '○ waiting its turn' : `○ ${s.state}`}</span>}
|
|
91
|
+
</li>
|
|
92
|
+
))}
|
|
93
|
+
</ul>
|
|
94
|
+
)}
|
|
95
|
+
{step !== 'sound' && cards && cards.length > 0 && (
|
|
96
|
+
<ul class="sw-cards">
|
|
97
|
+
{cards.map((c) => {
|
|
98
|
+
const made = c.made && fresh(c.at) ? c.made : null;
|
|
99
|
+
// Weather and reactions: the words are what was read, so the earlier build's wait for this run's.
|
|
100
|
+
const words = (step === 'weather' || step === 'reactions') && !made ? [] : c.words;
|
|
101
|
+
return (
|
|
102
|
+
<li class={made ? 'ok' : ''}>
|
|
103
|
+
<div class="pair">
|
|
104
|
+
<figure>
|
|
105
|
+
{c.from
|
|
106
|
+
? <Crop slug={slug} shot={c.from} alt={c.title} title={`${c.title}: ${step === 'icons' ? 'the painting where it is picked up; SAM\'s mask lit, the rest dimmed' : 'what the step works from'}. Click: large`} />
|
|
107
|
+
: <div class="sw-none">{named(step === 'icons' ? 'Muse Image paints it' : 'not painted yet')}</div>}
|
|
108
|
+
<figcaption>{FROM[step] ?? 'from'}</figcaption>
|
|
109
|
+
</figure>
|
|
110
|
+
{(step === 'icons' || step === 'plates') && (
|
|
111
|
+
<figure>
|
|
112
|
+
{made
|
|
113
|
+
? isShot(made)
|
|
114
|
+
? <Crop slug={slug} shot={made} alt={c.title} title={`${c.title}: ${MADE[step]}`} />
|
|
115
|
+
: <img class="sw-icon" src={fileUrl(slug, made.file, made.v)} data-lightbox={fileUrl(slug, made.file, made.v)} title={`${c.title}: the icon in the hero's bag. Click: large`} alt={c.title} />
|
|
116
|
+
: <div class={`to-come${failed || !running ? ' still' : ''}`} title={running ? 'Being made now' : 'Not made'} />}
|
|
117
|
+
<figcaption>{MADE[step]}</figcaption>
|
|
118
|
+
</figure>
|
|
119
|
+
)}
|
|
120
|
+
</div>
|
|
121
|
+
<div class="words">
|
|
122
|
+
<strong>{step === 'weather' || step === 'reactions' ? nice(c.title) : c.title}</strong>
|
|
123
|
+
{words.length
|
|
124
|
+
? words.map((w) => <span class="detail">{named(w)}</span>)
|
|
125
|
+
: <span class="detail">{running ? '○ waiting its turn' : '○ not made'}</span>}
|
|
126
|
+
</div>
|
|
127
|
+
</li>
|
|
128
|
+
);
|
|
129
|
+
})}
|
|
130
|
+
</ul>
|
|
131
|
+
)}
|
|
132
|
+
</div>
|
|
133
|
+
);
|
|
134
|
+
}
|