@moood/render 0.1.3 → 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +82 -9
- package/cli.js +294 -28
- package/compose.js +71 -0
- package/engine/directions.js +3 -3
- package/engine/engine.js +1600 -150
- package/engine/engine.json +1 -1
- package/engine/gl.js +1092 -216
- package/engine/kit.js +50 -6
- package/engine/motion.html +47 -0
- package/engine/motion.js +258 -0
- package/engine/render.html +11 -3
- package/engine/scenestate.js +127 -15
- package/engine/select.js +4 -3
- package/engine/soma.bin +0 -0
- package/engine/soma.md +4 -0
- package/engine/speech.js +23 -19
- package/index.js +135 -26
- package/package.json +4 -2
- package/scene.js +28 -12
- package/video.js +66 -0
package/scene.js
CHANGED
|
@@ -1,19 +1,34 @@
|
|
|
1
1
|
// A scene as it lies in its world's repository, put together as moood does it (the scenes service's
|
|
2
2
|
// layout.py): scenes/<folder>/scene.json and its code beside it — elements/<id>.js (objects[].code),
|
|
3
|
-
// elements/<id>.script.js (objects[].script), script.js, lib.js, notes.md, widgets/<id>.html — and
|
|
4
|
-
// the modules it lists (modules/<name>.js, .data.json, in the world's repository).
|
|
3
|
+
// elements/<id>.script.js (objects[].script), script.js, lib.js, relief.js, notes.md, widgets/<id>.html — and
|
|
4
|
+
// the modules it lists (modules/<name>.js, .data.json, in the world's repository). As it is on disk,
|
|
5
|
+
// or as a commit of the repository has it (`ref`: a branch, a tag, a commit — the world's main, say).
|
|
6
|
+
import { execFileSync } from 'node:child_process';
|
|
5
7
|
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
|
|
6
8
|
import { join, relative, sep } from 'node:path';
|
|
7
9
|
|
|
8
|
-
const TOP = { script: 'script.js', lib: 'lib.js', notes: 'notes.md' };
|
|
10
|
+
const TOP = { script: 'script.js', lib: 'lib.js', relief: 'relief.js', notes: 'notes.md' };
|
|
9
11
|
const IN = { objects: { code: 'elements/{id}.js', script: 'elements/{id}.script.js' }, widgets: { html: 'widgets/{id}.html' } };
|
|
10
12
|
|
|
11
13
|
const walk = dir => readdirSync(dir).flatMap(f => (statSync(join(dir, f)).isDirectory() ? walk(join(dir, f)) : [join(dir, f)]));
|
|
14
|
+
const git = (world, ...args) => execFileSync('git', ['-C', world, ...args], { encoding: 'utf8', maxBuffer: 1 << 28, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
15
|
+
// a file at a commit (null: there is none) — its path from the world's folder, which may lie deeper in
|
|
16
|
+
// its repository (./: from there)
|
|
17
|
+
const shown = (world, ref, path) => { try { return git(world, 'show', `${ref}:./${path}`); } catch { return null; } };
|
|
12
18
|
|
|
13
|
-
// the spec a scene's folder makes (an error says what is wrong with it)
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
19
|
+
// the spec a scene's folder makes (an error says what is wrong with it); `ref` — as that commit of the
|
|
20
|
+
// world's repository (`world`) has it
|
|
21
|
+
export function readScene(folder, { world, ref } = {}) {
|
|
22
|
+
let files;
|
|
23
|
+
if (ref) {
|
|
24
|
+
const at = relative(world, folder).split(sep).join('/');
|
|
25
|
+
try { git(world, 'rev-parse', '--verify', '--quiet', `${ref}^{commit}`); } catch { throw new Error(`${ref}: no such branch or commit in ${world}`); }
|
|
26
|
+
// (-z: the names as they are — any letters — not quoted; from the world's folder)
|
|
27
|
+
const names = git(world, 'ls-tree', '-r', '-z', '--name-only', ref, '--', `${at}/`).split('\0').filter(Boolean);
|
|
28
|
+
if (!names.length) throw new Error(`${ref}: there is no ${at} in it`);
|
|
29
|
+
files = Object.fromEntries(names.map(p => [p.slice(at.length + 1), shown(world, ref, p)]));
|
|
30
|
+
} else files = Object.fromEntries(walk(folder).map(p => [relative(folder, p).split(sep).join('/'), readFileSync(p, 'utf8')]));
|
|
31
|
+
if (!('scene.json' in files)) throw new Error(`${folder}${ref ? ` (${ref})` : ''}: no scene.json`);
|
|
17
32
|
const spec = JSON.parse(files['scene.json']);
|
|
18
33
|
const used = new Set(['scene.json']);
|
|
19
34
|
for (const [key, name] of Object.entries(TOP)) if (name in files) { spec[key] = files[name].replace(/\n+$/, ''); used.add(name); }
|
|
@@ -31,13 +46,14 @@ export function readScene(folder) {
|
|
|
31
46
|
return spec;
|
|
32
47
|
}
|
|
33
48
|
|
|
34
|
-
// the modules a scene lists, from its world's repository: {name: {code, data}}
|
|
35
|
-
export function readModules(world, spec) {
|
|
49
|
+
// the modules a scene lists, from its world's repository: {name: {code, data}} (`ref`: at that commit)
|
|
50
|
+
export function readModules(world, spec, { ref } = {}) {
|
|
51
|
+
const read = path => (ref ? shown(world, ref, path) : existsSync(join(world, path)) ? readFileSync(join(world, path), 'utf8') : null);
|
|
36
52
|
const out = {};
|
|
37
53
|
for (const name of [...new Set((spec.modules || []).filter(n => typeof n === 'string'))].sort()) {
|
|
38
|
-
const code =
|
|
39
|
-
if (
|
|
40
|
-
out[name] = { code
|
|
54
|
+
const code = read(`modules/${name}.js`), data = read(`modules/${name}.data.json`);
|
|
55
|
+
if (code == null) throw new Error(`the scene lists modules/${name}.js, and there is none${ref ? ` at ${ref}` : ''}`);
|
|
56
|
+
out[name] = { code, data: data == null ? null : JSON.parse(data) };
|
|
41
57
|
}
|
|
42
58
|
return out;
|
|
43
59
|
}
|
package/video.js
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// Frames into a video — by an ffmpeg already on this machine (nothing is downloaded): MOOOD_FFMPEG,
|
|
2
|
+
// else the one on the PATH, else the one Playwright installs beside its browsers (that one writes
|
|
3
|
+
// .webm only). The frames come in as JPEGs.
|
|
4
|
+
import { spawn, spawnSync } from 'node:child_process';
|
|
5
|
+
import { existsSync, readdirSync } from 'node:fs';
|
|
6
|
+
import { dirname, extname, join } from 'node:path';
|
|
7
|
+
import { homedir } from 'node:os';
|
|
8
|
+
|
|
9
|
+
// what each kind of file is written with: the encoder it needs, and how
|
|
10
|
+
const KINDS = {
|
|
11
|
+
'.mp4': ['libx264', ['-c:v', 'libx264', '-pix_fmt', 'yuv420p', '-crf', '20', '-preset', 'veryfast', '-movflags', '+faststart']],
|
|
12
|
+
'.webm': ['libvpx', ['-c:v', 'libvpx', '-b:v', '4M', '-crf', '8', '-auto-alt-ref', '0']],
|
|
13
|
+
'.gif': ['gif', ['-vf', 'split[a][b];[a]palettegen[p];[b][p]paletteuse']],
|
|
14
|
+
};
|
|
15
|
+
export const VIDEOS = Object.keys(KINDS);
|
|
16
|
+
|
|
17
|
+
// the ffmpegs here: the one asked for, the PATH's, Playwright's (beside its browsers: where it keeps
|
|
18
|
+
// them on Linux, macOS or Windows, or up from the browser given)
|
|
19
|
+
function ffmpegs(chromium) {
|
|
20
|
+
const found = [process.env.MOOOD_FFMPEG, 'ffmpeg'];
|
|
21
|
+
const homes = [process.env.PLAYWRIGHT_BROWSERS_PATH, join(homedir(), '.cache', 'ms-playwright'), join(homedir(), 'Library', 'Caches', 'ms-playwright'),
|
|
22
|
+
process.env.LOCALAPPDATA && join(process.env.LOCALAPPDATA, 'ms-playwright')];
|
|
23
|
+
for (let d = chromium && dirname(chromium); d && dirname(d) !== d; d = dirname(d)) homes.push(d);
|
|
24
|
+
for (const home of homes.filter(h => h && existsSync(h))) {
|
|
25
|
+
for (const f of readdirSync(home).filter(f => f.startsWith('ffmpeg-')).sort().reverse()) {
|
|
26
|
+
for (const exe of ['ffmpeg-linux', 'ffmpeg-mac', 'ffmpeg-win64.exe']) if (existsSync(join(home, f, exe))) found.push(join(home, f, exe));
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return [...new Set(found.filter(Boolean))];
|
|
30
|
+
}
|
|
31
|
+
const encoders = exe => { const r = spawnSync(exe, ['-hide_banner', '-encoders'], { encoding: 'utf8' }); return r.status === 0 ? r.stdout : null; };
|
|
32
|
+
|
|
33
|
+
// a video being written to `out` at `fps` frames a second: write(jpeg) each frame, then done()
|
|
34
|
+
export function video(out, fps, { chromium } = {}) {
|
|
35
|
+
const kind = KINDS[extname(out).toLowerCase()];
|
|
36
|
+
if (!kind) throw new Error(`--video: ${out} — a .mp4, .webm or .gif`);
|
|
37
|
+
const [need, how] = kind, tried = [];
|
|
38
|
+
let exe = null;
|
|
39
|
+
for (const f of ffmpegs(chromium || process.env.MOOOD_CHROMIUM)) {
|
|
40
|
+
const list = encoders(f);
|
|
41
|
+
if (list == null) continue;
|
|
42
|
+
tried.push(f);
|
|
43
|
+
if (new RegExp(`^\\s*V\\S*\\s+${need}\\s`, 'm').test(list)) { exe = f; break; }
|
|
44
|
+
}
|
|
45
|
+
if (!exe) {
|
|
46
|
+
throw new Error(tried.length
|
|
47
|
+
? `--video: ${extname(out)} needs an ffmpeg with ${need}, and ${tried.join(', ')} ${tried.length > 1 ? 'have' : 'has'} none — write .webm, or install ffmpeg, or point MOOOD_FFMPEG at one`
|
|
48
|
+
: '--video: no ffmpeg here — install it (or point MOOOD_FFMPEG at one); Playwright\'s own (npx playwright install ffmpeg) writes .webm');
|
|
49
|
+
}
|
|
50
|
+
const p = spawn(exe, ['-hide_banner', '-loglevel', 'error', '-y', '-f', 'image2pipe', '-c:v', 'mjpeg', '-framerate', String(fps), '-i', '-', ...how, out], { stdio: ['pipe', 'ignore', 'pipe'] });
|
|
51
|
+
let said = '';
|
|
52
|
+
p.stderr.on('data', d => { said += d; });
|
|
53
|
+
// (ffmpeg gone — it could not start, or it stopped: what it said is the error, the frames' pipe
|
|
54
|
+
// closing with it is not)
|
|
55
|
+
const ended = new Promise((ok, no) => {
|
|
56
|
+
p.on('error', e => no(new Error(`--video: ffmpeg (${exe}) could not run: ${e.message}`)));
|
|
57
|
+
p.on('close', code => (code === 0 ? ok() : no(new Error(`--video: ffmpeg (${exe}) failed: ${said.trim() || `exit ${code}`}`))));
|
|
58
|
+
});
|
|
59
|
+
ended.catch(() => {});
|
|
60
|
+
p.stdin.on('error', () => {});
|
|
61
|
+
return {
|
|
62
|
+
ffmpeg: exe,
|
|
63
|
+
write: jpeg => new Promise((ok, no) => p.stdin.write(jpeg, e => (e ? ended.then(() => no(e), no) : ok()))),
|
|
64
|
+
done: async () => { p.stdin.end(); await ended; },
|
|
65
|
+
};
|
|
66
|
+
}
|