@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.
- package/LICENSE +202 -0
- package/README.md +157 -2
- package/app/bin/diorama.mjs +13 -0
- package/app/src/actions/budget.ts +160 -0
- package/app/src/actions/build.ts +158 -0
- package/app/src/actions/cast.ts +267 -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 +32 -0
- package/app/src/actions/items.ts +94 -0
- package/app/src/actions/kids.ts +215 -0
- package/app/src/actions/language.ts +52 -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 +290 -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 +161 -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 +158 -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 +37 -0
- package/app/src/commands/language.ts +87 -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 +195 -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 +100 -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 +105 -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 +33 -0
- package/app/src/scene/cast.ts +199 -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 +109 -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 +379 -0
- package/app/src/scene/scale.ts +148 -0
- package/app/src/scene/shadows.ts +215 -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 +419 -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 +555 -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 +423 -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/language.ts +116 -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 +679 -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 +1329 -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 +3769 -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,145 @@
|
|
|
1
|
+
import { refreshExistingLipSync } from '../sound/lipsync-make.ts';
|
|
2
|
+
// diorama voices <slug>: every line of the story spoken. Spark casts the voices (kept in voices.json; voice_set in the Studio recasts one),
|
|
3
|
+
// ElevenLabs reads, Muse Voice Transcribe checks each take says its line.
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
import { inputHash } from '../meta/memo.ts';
|
|
6
|
+
import { defineCommand, emit } from '../registry.ts';
|
|
7
|
+
import { creditGuard, soundQuality } from '../sound/cost.ts';
|
|
8
|
+
import { elevenKey } from '../sound/sources.ts';
|
|
9
|
+
import { castVoices, DESIGNED, PENDING_DESIGN, freeVoiceSlot, voiceCandidates, DesignedVoices, designDescriptions, designVoice, needsDesign, stockVoices, storyLines, voiceLine, Voices, VOICE_PIPELINE, type Cast } from '../sound/voices.ts';
|
|
10
|
+
import { fixedLooks, readCastFixes } from '../scene/cast-fixes.ts';
|
|
11
|
+
import { Story } from '../story/schema.ts';
|
|
12
|
+
import { WorldFiles } from '../world/files.ts';
|
|
13
|
+
import { resumePendingVoices } from '../sound/voices.ts';
|
|
14
|
+
|
|
15
|
+
defineCommand({
|
|
16
|
+
name: 'voices',
|
|
17
|
+
usage: 'voices <slug> cast a voice per character (stock, or designed by ElevenLabs Voice Design for children, teens and the old) and speak every story line, checked by Muse Voice Transcribe; conversations, intro and endings [--all: every line; always for a world for kids, whose players cannot read] [--recast] [--draft: Flash and 2 takes; default final uses Multilingual v2 and up to 3 takes]',
|
|
18
|
+
async run(args) {
|
|
19
|
+
// Keep this generation at one quality even if Setup changes while it runs.
|
|
20
|
+
process.env.DIORAMA_SOUND = soundQuality(args);
|
|
21
|
+
const recast = args.includes('--recast');
|
|
22
|
+
const [slug] = args.filter((a) => !a.startsWith('--'));
|
|
23
|
+
if (!slug) throw new Error('usage: diorama voices <slug>');
|
|
24
|
+
const files = new WorldFiles(slug);
|
|
25
|
+
const world = files.world();
|
|
26
|
+
if (!fs.existsSync(files.path('story.json'))) return void console.log('no story.json: nothing to say');
|
|
27
|
+
if (!elevenKey()) return void console.log('no ELEVENLABS_API_KEY: lines stay unspoken');
|
|
28
|
+
const story = Story.parse(files.readJson(files.path('story.json')));
|
|
29
|
+
const guard = await creditGuard(elevenKey(), 'voices');
|
|
30
|
+
const lines = storyLines(world, story, args.includes('--all') || world.audience === 'kids');
|
|
31
|
+
const outFile = files.path('voices.json');
|
|
32
|
+
const old = fs.existsSync(outFile) ? Voices.parse(files.readJson(outFile)) : undefined;
|
|
33
|
+
let cast: Cast;
|
|
34
|
+
// A cast is kept only when it was made from profiles (who Spark saw in each figure) and every child, teen and old
|
|
35
|
+
// speaker has a designed voice; older casts are redone.
|
|
36
|
+
const fixes = readCastFixes(files);
|
|
37
|
+
const figure = (key: string) => (fs.existsSync(files.path('cast', `${key}.png`)) ? fs.readFileSync(files.path('cast', `${key}.png`)) : undefined);
|
|
38
|
+
const people = [world.hero, ...world.places.flatMap((p) => p.things).filter((t) => t.role === 'npc')];
|
|
39
|
+
const speakers = people.map((p) => ({ ...fixedLooks(files, { key: p.key, looks: p.looks }), figure: figure(p.key) }));
|
|
40
|
+
const castingFrom = inputHash(JSON.stringify(world), JSON.stringify(lines), ...speakers.flatMap((p) => [p.key, p.looks ?? '', p.figure ?? Buffer.alloc(0)]));
|
|
41
|
+
const current = (k: string) => { const c = old?.cast[k]; return old?.pipeline === VOICE_PIPELINE && old.castingFrom === castingFrom && c?.profile?.language === (world.language ?? 'English') && (!needsDesign(c.profile) || !!c.design || c.voice === PENDING_DESIGN || !!fixes[k]?.voice); };
|
|
42
|
+
if (old && !recast && lines.every((l) => current(l.speaker))) cast = await resumePendingVoices(files.memo, old.cast, lines);
|
|
43
|
+
else {
|
|
44
|
+
cast = await castVoices(files.memo, world, lines, await stockVoices(), speakers);
|
|
45
|
+
}
|
|
46
|
+
// The voices the person picked (cast/fixes.json) replace Spark's casting.
|
|
47
|
+
for (const [key, fix] of Object.entries(fixes)) {
|
|
48
|
+
if (!fix.voice || !cast[key]) continue;
|
|
49
|
+
const { design: wish, ...voice } = fix.voice;
|
|
50
|
+
const entry = { ...cast[key], ...voice };
|
|
51
|
+
if (voice.voice === DESIGNED) {
|
|
52
|
+
// A designed voice: from the person's description, or from the profile; designed again only when that changes.
|
|
53
|
+
const description = wish ?? (cast[key].profile ? designDescriptions(cast[key].profile!)[0] : 'A clear warm voice.');
|
|
54
|
+
const says = lines.filter((l) => l.speaker === key).map((l) => l.text);
|
|
55
|
+
try {
|
|
56
|
+
entry.design = cast[key].design?.request === description ? cast[key].design : await designVoice(files.memo, cast[key].profile ?? { gender: 'any', age: 'middle_aged', sound: '', language: world.language ?? 'English' }, says, description);
|
|
57
|
+
entry.pitch = 0;
|
|
58
|
+
} catch (error) {
|
|
59
|
+
// The voice Spark cast stays: the lines are still read. Studio shows the note on the character's card.
|
|
60
|
+
console.log(` ${key}: the designed voice could not be made (${error instanceof Error ? error.message : error}); the cast voice stays`);
|
|
61
|
+
cast = { ...cast, [key]: { ...cast[key], warnings: [...(cast[key].warnings ?? []), 'The designed voice could not be made this time; the cast voice reads the lines. Try the design again later.'] } };
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
} else delete entry.design;
|
|
65
|
+
cast = { ...cast, [key]: entry };
|
|
66
|
+
}
|
|
67
|
+
for (const [who, v] of Object.entries(cast)) console.log(` ${who.padEnd(16)} ${v.profile ? `${v.profile.age} ${v.profile.gender}`.padEnd(18) : ''} ${v.design ? 'designed' : v.voice}${v.pitch ? ` +${v.pitch} st` : ''} ${v.why}`);
|
|
68
|
+
|
|
69
|
+
// Lines no longer in the story go; the rest are rewritten (from the cache when nothing about them changed).
|
|
70
|
+
// Keep existing files until replacements pass validation; a failed run must not delete paid recordings.
|
|
71
|
+
// No free voice slot on the account: a designed voice cannot be created, so the nearest stock voice stands in.
|
|
72
|
+
if (Object.values(cast).some((c) => c.design) && !(await freeVoiceSlot())) {
|
|
73
|
+
const stock = await stockVoices();
|
|
74
|
+
for (const [k, c] of Object.entries(cast)) {
|
|
75
|
+
if (!c.design || !c.profile) continue;
|
|
76
|
+
const v = voiceCandidates(stock, c.profile)[0] ?? stock[0];
|
|
77
|
+
if (!v) throw new Error(`no stock voice available for ${k}`);
|
|
78
|
+
console.log(` ${k}: no free voice slot on the ElevenLabs account for a designed voice; ${v.name} stands in`);
|
|
79
|
+
cast = { ...cast, [k]: { ...c, voice: v.voice_id, design: undefined, pitch: 0, warnings: ['No free Voice Design slot; a stock actor stands in. Audition this voice.'] } };
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
const done: Voices['lines'] = new Array(lines.length);
|
|
83
|
+
const failures: Voices['failures'] = [];
|
|
84
|
+
let completed = 0;
|
|
85
|
+
const progress = () => emit('work.progress', { kind: 'voices', completed, total: lines.length, failed: failures.length });
|
|
86
|
+
progress();
|
|
87
|
+
// Designed voices live on the account only while their character's lines are read, one character at a time,
|
|
88
|
+
// so a world needs a single free voice slot.
|
|
89
|
+
const designed = new DesignedVoices(`diorama ${slug}`);
|
|
90
|
+
const index = lines.map((_, i) => i);
|
|
91
|
+
const groups = [index.filter((i) => !cast[lines[i].speaker].design), ...Object.keys(cast).filter((k) => cast[k].design).map((k) => index.filter((i) => lines[i].speaker === k))];
|
|
92
|
+
try {
|
|
93
|
+
for (const group of groups) {
|
|
94
|
+
let next = 0;
|
|
95
|
+
await Promise.all(Array.from({ length: 4 }, async () => {
|
|
96
|
+
while (next < group.length) await read(group[next++]);
|
|
97
|
+
}));
|
|
98
|
+
// A line the transcriber dropped (its stream failed, not the take) is listened to again once, after the
|
|
99
|
+
// group: the take is paid and cached, so this asks nothing of ElevenLabs. 42 lines of one world failed so.
|
|
100
|
+
const dropped = group.filter((i) => failures.some((f) => f.speaker === lines[i].speaker && f.text === lines[i].text && /^transcribe /.test(f.reason)));
|
|
101
|
+
if (dropped.length) console.log(` listening again to ${dropped.length} line(s) the transcriber dropped`);
|
|
102
|
+
for (const i of dropped) {
|
|
103
|
+
failures.splice(failures.findIndex((f) => f.speaker === lines[i].speaker && f.text === lines[i].text), 1);
|
|
104
|
+
completed--;
|
|
105
|
+
await read(i);
|
|
106
|
+
}
|
|
107
|
+
await designed.close();
|
|
108
|
+
}
|
|
109
|
+
} finally {
|
|
110
|
+
await designed.close();
|
|
111
|
+
}
|
|
112
|
+
async function read(i: number) {
|
|
113
|
+
// A child, teen or old speaker whose designed voice is not made yet waits: no stock adult reads for them, and
|
|
114
|
+
// no request goes out with a voice id that does not exist.
|
|
115
|
+
const voice = cast[lines[i].speaker];
|
|
116
|
+
if (voice.voice === PENDING_DESIGN) {
|
|
117
|
+
failures.push({ speaker: lines[i].speaker, text: lines[i].text, reason: `waiting for a designed voice: ${voice.pending ?? 'Voice Design pending'}` });
|
|
118
|
+
completed++;
|
|
119
|
+
progress();
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
try {
|
|
123
|
+
done[i] = await voiceLine(files.memo, lines[i], cast[lines[i].speaker], (id, mp3) => {
|
|
124
|
+
files.write(files.path('sound', 'voice', `${id}.mp3`), mp3);
|
|
125
|
+
return `sound/voice/${id}.mp3`;
|
|
126
|
+
}, designed, { retryRejected: old?.failures.some((f) => f.speaker === lines[i].speaker && f.text === lines[i].text && f.reason.startsWith('speech rejected')) });
|
|
127
|
+
const l = done[i];
|
|
128
|
+
console.log(` ${!l.flags.some((f) => f.startsWith('heard')) ? 'ok ' : 'FLAG'} ${l.speaker.padEnd(14)} ${l.seconds.toFixed(1).padStart(5)} s ${l.text.slice(0, 60)}${l.flags.length ? ` (${l.flags.join('; ')}; heard "${l.heard.slice(0, 60)}")` : ''}`);
|
|
129
|
+
} catch (error) {
|
|
130
|
+
failures.push({ speaker: lines[i].speaker, text: lines[i].text, reason: error instanceof Error ? error.message : String(error) });
|
|
131
|
+
console.log(` FAIL ${lines[i].speaker}: ${lines[i].text.slice(0, 50)}: ${error instanceof Error ? error.message : error}`);
|
|
132
|
+
} finally {
|
|
133
|
+
completed++;
|
|
134
|
+
progress();
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
const spoken = done.filter(Boolean);
|
|
138
|
+
files.write(outFile, { pipeline: VOICE_PIPELINE, castingFrom, cast, lines: spoken, failures });
|
|
139
|
+
await refreshExistingLipSync(files);
|
|
140
|
+
const s = files.memo.sent;
|
|
141
|
+
console.log(`voices ${slug}: ${spoken.length}/${lines.length} lines, ${spoken.filter((l) => l.flags.some((f) => f.startsWith('heard'))).length} flagged · sent: spark ${s.spark}, voice ${s.voice} · ${await guard.report()} · ${outFile}`);
|
|
142
|
+
// A line without a voice still shows as text: the build goes on and Studio shows it with a retry.
|
|
143
|
+
if (failures.length) console.log(`warning: ${failures.length} spoken lines stay text only (rejected audio is not used); see voices.json failures`);
|
|
144
|
+
},
|
|
145
|
+
});
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// diorama weather <slug> [place...]: weather.json per place, what falls there and where water drips, for the
|
|
2
|
+
// page to animate (rain or snow over the frame, rings on water and puddles, splashes, drips off the eaves).
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import sharp from 'sharp';
|
|
5
|
+
import { maskFromPng, maskToPng } from '../image/mask.ts';
|
|
6
|
+
import { Geometry } from '../play/geometry.ts';
|
|
7
|
+
import { decodeDepth } from '../play/shadow.ts';
|
|
8
|
+
import { WeatherWish, type Weather } from '../play/weather.ts';
|
|
9
|
+
import { defineCommand } from '../registry.ts';
|
|
10
|
+
import { dryOf, readWeather } from '../scene/weather.ts';
|
|
11
|
+
import { WorldFiles } from '../world/files.ts';
|
|
12
|
+
|
|
13
|
+
defineCommand({
|
|
14
|
+
name: 'weather',
|
|
15
|
+
usage: 'weather <slug> [place...] read what falls on each place (rain, snow) and which edges drip, for the page to animate',
|
|
16
|
+
async run(args) {
|
|
17
|
+
const [slug, ...only] = args;
|
|
18
|
+
if (!slug) throw new Error('usage: diorama weather <slug> [place...]');
|
|
19
|
+
const files = new WorldFiles(slug);
|
|
20
|
+
const world = files.world();
|
|
21
|
+
const places = world.places.filter((p) => only.length === 0 || only.includes(p.key));
|
|
22
|
+
await Promise.all(places.map(async (place) => {
|
|
23
|
+
const at = (...parts: string[]) => files.place(place.key, ...parts);
|
|
24
|
+
fs.rmSync(at('weather.json'), { force: true });
|
|
25
|
+
fs.rmSync(at('weather'), { recursive: true, force: true });
|
|
26
|
+
if (!fs.existsSync(at('geometry.json')) || !fs.existsSync(at('painting.png'))) {
|
|
27
|
+
console.log(` ${place.key}: no geometry.json or painting.png, no weather`);
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
const g = Geometry.parse(files.readJson(at('geometry.json')));
|
|
31
|
+
const painting = fs.readFileSync(at('painting.png'));
|
|
32
|
+
let depth;
|
|
33
|
+
if (g.depth && fs.existsSync(at(g.depth))) {
|
|
34
|
+
const { data, info } = await sharp(fs.readFileSync(at(g.depth))).ensureAlpha().raw().toBuffer({ resolveWithObject: true });
|
|
35
|
+
depth = decodeDepth(data, info.width, info.height);
|
|
36
|
+
}
|
|
37
|
+
const maskFiles = [
|
|
38
|
+
...(fs.existsSync(at('masks', 'floors')) ? fs.readdirSync(at('masks', 'floors')).filter((f) => f.endsWith('.png')).map((f) => at('masks', 'floors', f)) : []),
|
|
39
|
+
...[...new Set(g.occluders.map((o) => o.mask).filter((m): m is string => !!m))].map((m) => at(m)),
|
|
40
|
+
].filter((f) => fs.existsSync(f));
|
|
41
|
+
const dry = dryOf(g.frame.width, g.frame.height, await Promise.all(maskFiles.map(async (f) => maskFromPng(fs.readFileSync(f)))));
|
|
42
|
+
const wish = WeatherWish.parse(fs.existsSync(at('weather-wish.json')) ? files.readJson(at('weather-wish.json')) : {});
|
|
43
|
+
const { weather, ripples } = await readWeather(files.memo, place, painting, g, depth, dry, wish);
|
|
44
|
+
if (ripples) files.write(at('weather', 'ripples.png'), await maskToPng(ripples));
|
|
45
|
+
const out: Weather = { ...weather, ...(ripples && { ripples: 'weather/ripples.png' }) };
|
|
46
|
+
files.write(at('weather.json'), out);
|
|
47
|
+
const drips = weather.drips.map((d) => `${d.phrase} (${d.from.length})`).join(', ');
|
|
48
|
+
console.log(`${place.key}: ${weather.fall === 'none' ? 'nothing falls' : `${weather.fall} ${weather.strength}, wind ${weather.wind}`}${weather.fog ? `, fog ${weather.fog}` : ''}${ripples ? ', rings on water and puddles' : ''}${drips ? `, drips from ${drips}` : ''}`);
|
|
49
|
+
}));
|
|
50
|
+
console.log(`calls sent: spark ${files.memo.sent.spark}, sam ${files.memo.sent.sam}`);
|
|
51
|
+
},
|
|
52
|
+
});
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
// diorama world <slug> "<idea>": the first step of every build.
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import { defineCommand, takeOption } from '../registry.ts';
|
|
4
|
+
import { describePerson, describePhoto, framePhoto, normalisePhoto } from '../scene/photo.ts';
|
|
5
|
+
import { designWorld, type Extra } from '../world/design.ts';
|
|
6
|
+
import { drawingFiles, drawingsBrief, drawnIssues, fitPlaces, keepDrawings, KIDS_STYLE, oneHero, readDrawings, readKinds } from '../world/drawings.ts';
|
|
7
|
+
import { WorldFiles } from '../world/files.ts';
|
|
8
|
+
import { hearAll, Idea, ideaFromTold, keepTold } from '../world/told.ts';
|
|
9
|
+
import { CreativeChoices } from '../world/idea-review.ts';
|
|
10
|
+
|
|
11
|
+
defineCommand({
|
|
12
|
+
name: 'world',
|
|
13
|
+
usage: 'world <slug> "<idea>" design a world (world.json) with Muse Spark [--places N] [--photo room.jpg: a real room is the first place] [--hero-photo me.jpg: a real person is the hero] '
|
|
14
|
+
+ '[--drawings DIR: photos of children\'s drawings become the hero, NPCs, items and places] [--told DIR: recordings of them telling the story; without an idea, the idea is written from them] [--kids: a game for 5-7 year olds; on with drawings or recordings] [--language Polish: the game\'s text and voices; with recordings, the language spoken]',
|
|
15
|
+
async run(args) {
|
|
16
|
+
const places = Number(takeOption(args, 'places', '4'));
|
|
17
|
+
const photoFile = takeOption(args, 'photo', '');
|
|
18
|
+
const heroFile = takeOption(args, 'hero-photo', '');
|
|
19
|
+
const drawingsFrom = takeOption(args, 'drawings', '');
|
|
20
|
+
const toldFrom = takeOption(args, 'told', '');
|
|
21
|
+
let language: string | undefined = takeOption(args, 'language', '') || undefined;
|
|
22
|
+
const kidsFlag = args.indexOf('--kids');
|
|
23
|
+
if (kidsFlag >= 0) args.splice(kidsFlag, 1);
|
|
24
|
+
const [slug, given] = args;
|
|
25
|
+
if (!slug) throw new Error('usage: diorama world <slug> "<idea>" [--places N]');
|
|
26
|
+
const files = new WorldFiles(slug);
|
|
27
|
+
await designInitialWorld(files, { places, photoFile, heroFile, drawingsFrom, toldFrom, language, kidsFlag: kidsFlag >= 0, given });
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
/** The same first stage is used by the optional preview, without starting later build steps. */
|
|
32
|
+
export async function designInitialWorld(files: WorldFiles, { places, photoFile = '', heroFile = '', drawingsFrom = '', toldFrom = '', language, kidsFlag = false, given }: { places: number; photoFile?: string; heroFile?: string; drawingsFrom?: string; toldFrom?: string; language?: string; kidsFlag?: boolean; given?: string }) {
|
|
33
|
+
const briefFile = files.path('creative-brief.json');
|
|
34
|
+
const choices = fs.existsSync(briefFile) ? CreativeChoices.parse((files.readJson(briefFile) as { choices: unknown }).choices) : undefined;
|
|
35
|
+
const chosenStyle = choices?.style;
|
|
36
|
+
// Drawings and recordings already kept with the world (the Studio uploads them there) count too.
|
|
37
|
+
if (drawingsFrom) console.log(`drawings kept: ${(await keepDrawings(files, drawingsFrom)).join(', ')}`);
|
|
38
|
+
if (toldFrom) console.log(`recordings kept: ${keepTold(files, toldFrom).join(', ')}`);
|
|
39
|
+
const said = await hearAll(files);
|
|
40
|
+
for (const s of said) console.log(`heard ${s.clip}: ${s.text}`);
|
|
41
|
+
// A drawn place over the places asked for goes into a place as a prop: places are what makes a build long.
|
|
42
|
+
const drawn = fitPlaces(await readDrawings(files.memo, files), places);
|
|
43
|
+
for (const d of drawn) console.log(`drawing ${d.file}: ${d.kind} ${d.key}, ${d.looks}`);
|
|
44
|
+
const kids = kidsFlag || drawn.length > 0 || said.length > 0;
|
|
45
|
+
|
|
46
|
+
let idea = given;
|
|
47
|
+
// The story says who leads, unless the grown-up already picked a drawing as the hero.
|
|
48
|
+
const heroOf = (told: Idea) => {
|
|
49
|
+
if (told.hero && !drawn.some((d) => readKinds(files)[d.file] === 'hero')) oneHero(drawn, told.hero);
|
|
50
|
+
};
|
|
51
|
+
if (idea) {
|
|
52
|
+
// An idea written from what the children said (the Studio's kids_idea), maybe edited since: its language and hero stand.
|
|
53
|
+
const told = Idea.safeParse(fs.existsSync(files.path('told', 'idea.json')) ? JSON.parse(fs.readFileSync(files.path('told', 'idea.json'), 'utf8')) : null);
|
|
54
|
+
if (told.success) (language ??= told.data.language, heroOf(told.data));
|
|
55
|
+
} else if (said.length || drawn.length) {
|
|
56
|
+
// What the children said and drew is the idea.
|
|
57
|
+
const told = await ideaFromTold(files.memo, said.map((s) => s.text), drawn);
|
|
58
|
+
idea = told.idea;
|
|
59
|
+
language ??= told.language;
|
|
60
|
+
heroOf(told);
|
|
61
|
+
files.write(files.path('told', 'idea.json'), told);
|
|
62
|
+
console.log(`idea from what they said (${told.language}): ${told.title}. ${told.idea}`);
|
|
63
|
+
}
|
|
64
|
+
if (!idea) throw new Error('usage: diorama world <slug> "<idea>" (or --told DIR with recordings of the story)');
|
|
65
|
+
files.write(files.path('idea.txt'), `${idea}\n`);
|
|
66
|
+
let room;
|
|
67
|
+
if (photoFile) {
|
|
68
|
+
const { image, crop } = await framePhoto(fs.readFileSync(photoFile));
|
|
69
|
+
room = await describePhoto(files.memo, image, idea);
|
|
70
|
+
files.write(files.place(room.key, 'photo.png'), image);
|
|
71
|
+
files.write(files.place(room.key, 'photo.json'), crop);
|
|
72
|
+
console.log(`photo: ${room.key}, ${room.things.map((t) => t.key).join(', ')} · crop ${JSON.stringify(crop.crop)}`);
|
|
73
|
+
}
|
|
74
|
+
let hero;
|
|
75
|
+
if (heroFile) {
|
|
76
|
+
const photo = await normalisePhoto(fs.readFileSync(heroFile));
|
|
77
|
+
hero = await describePerson(files.memo, photo);
|
|
78
|
+
files.write(files.path('cast', `${hero.key}-photo.png`), photo);
|
|
79
|
+
console.log(`hero from photo: ${hero.key}, ${hero.looks}`);
|
|
80
|
+
}
|
|
81
|
+
const other = language && !/^english$/i.test(language) ? language : undefined;
|
|
82
|
+
// A clarification can choose a different lead. Do not let the default first drawing override it.
|
|
83
|
+
const characters = drawn.filter(d => d.kind === 'hero' || d.kind === 'npc').map(d => d.key);
|
|
84
|
+
const chooseHero = !!(choices?.answers.length || choices?.note) && characters.length > 1 && !drawn.some(d => readKinds(files)[d.file] === 'hero');
|
|
85
|
+
const asks = [
|
|
86
|
+
drawn.length ? drawingsBrief(drawn, chosenStyle ?? KIDS_STYLE, chooseHero) : '',
|
|
87
|
+
chosenStyle ? `The creator explicitly chose this visual style: ${chosenStyle}. Preserve recognizable shapes and identities from all source drawings.` : '',
|
|
88
|
+
other ? `Write the title and pitch in ${other}, the language of the players; everything else (style, shows, mood, looks, keys) stays in English for the painters.` : '',
|
|
89
|
+
// The game shows a name over each speaker: in the players' language, as they call them.
|
|
90
|
+
other || kids ? `Add "names": {"<key>": "name"} for the hero and every npc: a short name ${other ? `in ${other} ` : ''}the players would call them by (the name the children gave them, if they gave one; else a short friendly name a child would give).${drawn.length ? ` Name every drawn item and place too, as a child would say it${other ? ` in ${other}` : ''} ("the blue hat", "the mushroom forest").` : ''}` : '',
|
|
91
|
+
].filter(Boolean);
|
|
92
|
+
const extra: Extra | undefined = asks.length ? { brief: asks.join('\n\n'), issues: (w) => {
|
|
93
|
+
if (!chooseHero) return drawnIssues(w, drawn);
|
|
94
|
+
if (!characters.includes(w.hero.key)) return [`Choose the hero from the drawn characters: ${characters.join(', ')}`];
|
|
95
|
+
return drawnIssues(w, oneHero(drawn.map(d => ({ ...d })), w.hero.key));
|
|
96
|
+
} } : undefined;
|
|
97
|
+
const designed = await designWorld(files.memo, hero ? `${idea}\nThe hero is ${hero.looks}` : idea, places, room, extra);
|
|
98
|
+
if (chooseHero) oneHero(drawn, designed.hero.key);
|
|
99
|
+
const world = { ...designed, ...(drawn.length && { style: KIDS_STYLE }), ...(chosenStyle && { style: chosenStyle }), ...(hero && { hero }), ...(other && { language: other }), ...(kids && { audience: 'kids' as const }) };
|
|
100
|
+
files.write(files.path('world.json'), world);
|
|
101
|
+
if (drawingFiles(files).length) files.write(files.path('drawings.json'), drawn);
|
|
102
|
+
console.log(`${world.title}: ${world.places.map((p) => p.key).join(' · ')}`);
|
|
103
|
+
console.log(`spark calls sent: ${files.memo.sent.spark}`);
|
|
104
|
+
}
|