motigenjs 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.
Files changed (53) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +174 -2
  3. package/bin/motigen.js +7 -0
  4. package/docs/audio.md +54 -0
  5. package/docs/canvas.md +54 -0
  6. package/docs/guide.md +100 -0
  7. package/package.json +60 -4
  8. package/runtime/canvas/ink.js +314 -0
  9. package/runtime/canvas/kit.js +391 -0
  10. package/runtime/guide/player.css +54 -0
  11. package/runtime/guide/player.js +364 -0
  12. package/runtime/preview/index.html +28 -0
  13. package/runtime/preview/preview.css +17 -0
  14. package/runtime/preview/preview.js +127 -0
  15. package/skill/SKILL.md +38 -0
  16. package/src/audio/decode.js +26 -0
  17. package/src/audio/index.js +7 -0
  18. package/src/audio/loudness.js +161 -0
  19. package/src/audio/mix.js +175 -0
  20. package/src/audio/pcm.js +57 -0
  21. package/src/audio/srt.js +29 -0
  22. package/src/audio/vo.js +148 -0
  23. package/src/audio/wav.js +52 -0
  24. package/src/browser.js +34 -0
  25. package/src/cli.js +250 -0
  26. package/src/index.js +4 -0
  27. package/src/init.js +38 -0
  28. package/src/preview.js +59 -0
  29. package/src/project.js +42 -0
  30. package/src/record/browser.js +167 -0
  31. package/src/record/docker.js +38 -0
  32. package/src/record/hooks.js +19 -0
  33. package/src/record/index.js +4 -0
  34. package/src/record/term.js +165 -0
  35. package/src/render.js +198 -0
  36. package/src/serve.js +114 -0
  37. package/templates/canvas/audio/lines.txt +3 -0
  38. package/templates/canvas/film.html +15 -0
  39. package/templates/canvas/film.js +77 -0
  40. package/templates/canvas/gitignore +6 -0
  41. package/templates/canvas/motigen.json +14 -0
  42. package/templates/dom/film.html +65 -0
  43. package/templates/dom/gitignore +6 -0
  44. package/templates/dom/motigen.json +10 -0
  45. package/templates/guide/audio/lines.txt +4 -0
  46. package/templates/guide/brand.svg +5 -0
  47. package/templates/guide/edit.js +32 -0
  48. package/templates/guide/film.html +34 -0
  49. package/templates/guide/gitignore +7 -0
  50. package/templates/guide/motigen.json +11 -0
  51. package/templates/guide/site/index.html +47 -0
  52. package/templates/guide/take.mjs +46 -0
  53. package/templates/guide/theme.css +5 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 crocone
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,175 @@
1
- # Temporary Holding Version
1
+ # motigen
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Video as code, for developers and their coding agents.
4
+
5
+ A web page is the film: it draws frame `t` on demand, in plain HTML, Canvas or anything else a browser runs.
6
+ motigen opens it headless, renders every frame in order and pipes them into ffmpeg, so one project gives the same
7
+ MP4 on every run. Real terminals and real browsers are recorded and replayed inside the same page; the narration,
8
+ the sound effects and the music are mixed from the timeline the page exports; the subtitles come from the same
9
+ cues. Everything is a file in git and every step is one command, which is exactly what a coding agent needs to
10
+ make a product video on its own.
11
+
12
+ ```bash
13
+ npm i -g motigenjs && npx playwright install chromium # Node 22+, ffmpeg in PATH; the command is `motigen`
14
+ motigen init guide my-guide && cd my-guide
15
+ motigen record take.mjs # a real browser and a real terminal, recorded into take/
16
+ motigen vo # narration from audio/lines.txt (ElevenLabs), one file per line
17
+ motigen build # events → mix → render → subtitles: out/my-guide.mp4 + .srt
18
+ motigen preview # scrub it, play it with sound, render from a button
19
+ ```
20
+
21
+ ## What you get
22
+
23
+ - **Three kinds of film, one contract.** A page registers `window.__film = { ready, dur, renderAt(t) }`. The
24
+ `dom` template is that contract in 80 lines; the `guide` template is a screencast player (browser window,
25
+ terminal window, chapters, key badges, cards) driven by one edit list; the `canvas` template is a hand-drawn
26
+ 2D engine (paper, letterpress type, ink strokes, a thread, cards, a pencil) with its own offline mix.
27
+ - **A recorder for the real thing.** `Term` records a shell through a pty with `script`-style timing, so the
28
+ player replays the actual bytes through xterm.js; `Browser` records a CDP screencast with the mouse path, the
29
+ clicks, the keys and the URLs, and can serve a local folder or a local instance of your site under its real
30
+ domain.
31
+ - **Sound from the timeline.** `motigen events` asks the page where the voice lines, clicks and key presses fall;
32
+ `motigen vo` makes the narration (one ElevenLabs call with timestamps, split per line, or your own recordings);
33
+ `motigen mix` lays everything out, ducks the music under the voice and masters to −14 LUFS / −1 dBTP;
34
+ `motigen srt` writes the subtitles.
35
+ - **Deterministic, parallel rendering.** Every frame depends on `t` alone, so `-j 4` renders four slices at once
36
+ and joins them losslessly, and the preview scrubs to any moment instantly.
37
+ - **Made for agents.** `skill/SKILL.md` tells a coding agent how to take a script to a finished video with these
38
+ commands; the whole project is text it can read and change.
39
+
40
+ ## Install
41
+
42
+ Node 22 or newer and ffmpeg in your PATH. Playwright downloads its Chromium once; `--channel chrome` uses an
43
+ installed Chrome instead.
44
+
45
+ ```bash
46
+ npm i -g motigenjs
47
+ npx playwright install chromium
48
+ ```
49
+
50
+ The npm package is `motigenjs`; the command it installs is `motigen`. As a project dependency, `npx motigen …`
51
+ works the same way.
52
+
53
+ The terminal recorder uses `node-pty` (an optional dependency with prebuilt binaries for Windows, macOS and
54
+ Linux). Without it, record in a container: `backend: 'docker'`.
55
+
56
+ ## Commands
57
+
58
+ | Command | What it does |
59
+ |---|---|
60
+ | `motigen init <guide\|dom\|canvas> <dir>` | A new project from a template. |
61
+ | `motigen preview [--port 4310] [--open]` | A player over the project: scrub, play with `audio/mix.wav`, switch formats, render and export events from buttons. |
62
+ | `motigen frames -t 1,2.5,10` | Stills into `shots/` (`--png`, `--every 5`, `--element '#terminal'` with a transparent background, `--scale 2`). |
63
+ | `motigen render` | The video to `out/<name>.mp4`: `-j N` pages in parallel (default: half the cores, at most 4), `--range 10-20`, `--audio file.wav` or the project's `audio/mix.wav`, `--crf`, `--preset`. |
64
+ | `motigen events` | The page's `events()` into `audio/events.json`: voice cues, clicks, keys, Enter presses, effects. |
65
+ | `motigen record [take.mjs]` | Runs a take script; it imports `{ Term, Browser, sleep }` from `motigenjs/record`. |
66
+ | `motigen vo [lines.txt]` | Narration: one ElevenLabs call for the whole script, split per line into `audio/vo/` with `vo.json`. `vo split` uses a saved response, `vo import` your own recordings, `vo check` flags breathy or garbled lines. |
67
+ | `motigen mix` | `audio/mix.wav` from `events.json`, `vo/`, `sfx/` and `bed.*`; or the canvas kit's own mix when `"mix": {"engine": "page"}`. |
68
+ | `motigen srt` | Subtitles from the voice cues, with the written forms from `"spellings"`. |
69
+ | `motigen build` | `events` → `mix` → `render` (every format) → `srt`. |
70
+ | `motigen skill [dir]` | Installs `skill/SKILL.md` into `.claude/skills/motigen/`, so a coding agent in that folder knows the workflow. |
71
+
72
+ Common options: `-C dir` (the project), `-p page`, `--fmt name`, `--size WxH`, `--query "fmt=v"`, `--channel chrome`,
73
+ `--no-gpu`, `--ffmpeg path`, `--timeout s`. `motigen help` lists everything.
74
+
75
+ ## The contract
76
+
77
+ ```js
78
+ window.__film = {
79
+ ready: true, // fonts and assets are loaded
80
+ dur: 38, // seconds
81
+ fps: 30, // optional
82
+ renderAt(t) {}, // draw the frame at t seconds; sync or async; a pure function of t
83
+ events() {}, // optional: { dur, vo: [{id, t}], clicks: [t], keys: [t], enters: [t], sfx: [{id, t, db}] }
84
+ canvas, // optional: an HTMLCanvasElement to read frames from, at its own size
85
+ };
86
+ ```
87
+
88
+ A frame must depend on `t` alone: no `Date.now()`, no CSS animations or transitions, no random numbers without a
89
+ seed, nothing that keeps running between calls. Then any frame can be rendered at any time in any order, which is
90
+ what parallel rendering and scrubbing rely on.
91
+
92
+ ## A project
93
+
94
+ ```
95
+ my-guide/
96
+ motigen.json page, fps, size, formats, audio folder, out, vo / mix / spellings settings
97
+ film.html the page; the guide template adds edit.js, theme.css, brand.svg, take.mjs, site/
98
+ take/ recordings: take/<clip>/ (frames + clip.json), take/term.out + term.tm
99
+ audio/ lines.txt → vo/ (L01.wav… + vo.json); bed.wav; sfx/; events.json, mix.wav (generated)
100
+ out/ renders and subtitles
101
+ ```
102
+
103
+ ```json
104
+ { "page": "film.html", "fps": 30, "size": [1920, 1080], "out": "out/my-film.mp4",
105
+ "formats": { "h": { "query": "fmt=h" }, "v": { "size": [1080, 1920], "query": "fmt=v" } },
106
+ "vo": { "voice": "<elevenlabs voice id>", "model": "eleven_v4", "seed": 7042 },
107
+ "mix": { "voiceDb": -3, "duckDb": -9, "target": { "lufs": -14, "dbtp": -1 } },
108
+ "spellings": [["Acme Cloud dot com", "acmecloud.com"], ["Pie", "Pi"]] }
109
+ ```
110
+
111
+ ## The three templates
112
+
113
+ **guide** — a screencast. `take.mjs` records the demo site in `site/` (served inside the recorder as
114
+ `https://demo.local`) and a shell on your machine; `edit.js` cuts them:
115
+
116
+ ```js
117
+ export default {
118
+ cols: 112, rows: 30, take: 'take', term: 'take/term',
119
+ shots: [
120
+ { card: intro, dur: 4, vo: [['L01', -1, 0.5]] },
121
+ { browser: 'site', segs: [[0, 9.5]], chapter: ['Step 1', 'Connect'], zoom: [[0, 1, 800, 450], [1.4, 1.35, 700, 420]] },
122
+ { term: true, segs: [[0, 'edit.js|+1.2'], [null, 'node --version|+1.6']], keys: [['node --version|-0.2', 1.4, 'Enter', 'run it']] },
123
+ { card: outro, dur: 4 },
124
+ ],
125
+ };
126
+ ```
127
+
128
+ Segments play source seconds back to back and can be stretched or squeezed; terminal bounds can be text anchors
129
+ (`'Installed|+0.8'` = the moment that text appears, plus 0.8 s); the window zooms along keyframes; cards, chapters,
130
+ key badges and notes are overlays. [docs/guide.md](docs/guide.md) has the whole format and the recorder API.
131
+
132
+ **dom** — the contract and nothing else: a title, a bar and three typed lines in one HTML file. Start here for a
133
+ film in plain HTML and CSS.
134
+
135
+ **canvas** — a 12-second film on the hand-drawn engine from a real promo series: paper, a letterpress title, an
136
+ ink thread with a needle, a rule card with a highlighter, tally marks, a pencil that underlines, a stamped name.
137
+ Both formats come from one `LAYOUTS` table. [docs/canvas.md](docs/canvas.md) lists the kit.
138
+
139
+ ## Sound
140
+
141
+ `audio/lines.txt` is the script, one line per cue. `motigen vo` sends it to ElevenLabs in one call with character
142
+ timestamps (`ELEVENLABS_API_KEY`, the voice id in `motigen.json`) and cuts the take at the quietest point between
143
+ lines, so pauses sound natural and every line is its own file. Recorded your own? `motigen vo import` writes
144
+ `vo.json` for the files in `audio/vo/`. Then `motigen mix` places each line where the edit put its cue, adds the
145
+ click / key / Enter sounds for what the recorder logged (synthesised when `audio/sfx/` is empty), loops the music
146
+ bed with a crossfade, ducks it under the voice and masters with ffmpeg's two-pass loudnorm. The levels, the
147
+ ducking and the target live under `"mix"` in `motigen.json`. [docs/audio.md](docs/audio.md).
148
+
149
+ ## Why not …
150
+
151
+ - **Remotion**: React, a bundler and a licence with limits for companies. motigen takes any page, needs no build
152
+ step, and is MIT.
153
+ - **Motion Canvas**: its own editor and generator syntax. motigen is a CLI over an ordinary HTML page.
154
+ - **VHS, asciinema**: the terminal only. motigen puts a real terminal next to a real browser, cuts them with one
155
+ edit list, and adds narration, sound and subtitles from the same timeline.
156
+ - **Screen recorders**: a GUI and one take. A motigen project is text in git; change one line and render again.
157
+
158
+ ## Where it comes from
159
+
160
+ The format and the tools were extracted from a pipeline that produced a real series of product guides and promo
161
+ films. Every port was checked against that pipeline's own output: the guide player gives the same events and the
162
+ same frames (SSIM 0.99 against the earlier render), the mix the same loudness and the same waveform within the
163
+ random jitter of the key clicks, the subtitles the same file byte for byte, the canvas films the same frames.
164
+
165
+ ## Development
166
+
167
+ ```bash
168
+ git clone https://github.com/crocone/motigen && cd motigen
169
+ npm i && npx playwright install chromium
170
+ npm test
171
+ node bin/motigen.js build -C examples/hello-dom
172
+ cd examples/hello-guide && node ../../bin/motigen.js record take.mjs && node ../../bin/motigen.js build
173
+ ```
174
+
175
+ MIT.
package/bin/motigen.js ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env node
2
+ import { main } from '../src/cli.js';
3
+
4
+ main(process.argv.slice(2)).catch(e => {
5
+ console.error(process.env.MOTIGEN_DEBUG ? e?.stack || e : `motigen: ${e?.message || e}`);
6
+ process.exit(1);
7
+ });
package/docs/audio.md ADDED
@@ -0,0 +1,54 @@
1
+ # Sound: narration, the mix, subtitles
2
+
3
+ Everything sits in the project's audio folder (`"audio"` in motigen.json, default `audio/`):
4
+
5
+ ```
6
+ audio/
7
+ lines.txt the script, one line per cue (L01, L02, …); blank lines and # comments are skipped
8
+ vo/ L01.wav … + vo.json [{id, dur, text, speechStart, speechEnd}]; resp.json (the TTS response)
9
+ bed.wav|mp3|… the music, optional
10
+ sfx/ click.wav, key.wav, enter.wav and any id used in events.sfx; sfx.json with each file's `hit`
11
+ events.json from `motigen events`
12
+ mix.wav from `motigen mix` (mix-raw.wav is the unmastered sum)
13
+ ```
14
+
15
+ ## Narration
16
+
17
+ `motigen vo` reads `lines.txt`, sends the whole script to ElevenLabs in one `with-timestamps` call
18
+ (`ELEVENLABS_API_KEY`; `"vo": { "voice", "model", "seed", "settings" }` in motigen.json or `--voice`, `--model`,
19
+ `--seed`), saves the response as `audio/vo/resp.json` and splits it: each line is cut at the quietest point between
20
+ it and the next (the alignment runs a little late), silence is trimmed, and `vo.json` records the duration, the
21
+ text and the measured speech span of every line.
22
+
23
+ - `motigen vo split resp.json lines.txt` splits a saved response (no API call).
24
+ - `motigen vo import [dir] [lines.txt]` takes files you recorded or generated elsewhere (any format, sorted by
25
+ name), renames them to L01…, and writes `vo.json`.
26
+ - `motigen vo check` prints the spectral flatness of every line and flags the breathy or garbled ones (`--limit`,
27
+ default 0.12); exit code 2 when any is flagged.
28
+
29
+ Spelling for the voice is not spelling for the screen: write "Acme Cloud dot com" in `lines.txt` and put
30
+ `["Acme Cloud dot com", "acmecloud.com"]` under `"spellings"` so the subtitles read right.
31
+
32
+ ## The mix
33
+
34
+ `motigen mix` builds `audio/mix.wav` from `events.json`:
35
+
36
+ 1. Voice: every line peak-normalised to `voiceDb` (−3 dBFS) and placed at its cue.
37
+ 2. Effects: `click` at every logged click (`clickDb` −14), `key` at every key press (`keyDb` −27 with a seeded
38
+ ±2.5 dB jitter), `enter` at every Enter badge (`enterDb` −18), plus `events.sfx` entries by id. Files come from
39
+ `audio/sfx/<id>.wav` (or `sfx.json`'s `file`, placed so the transient at `hit` lands on the event); missing
40
+ click / key / enter are synthesised.
41
+ 3. Music: the bed, looped from `bedLoopFrom` (8 s) with a `bedCrossfade` (2 s) when the film is longer, faded out
42
+ over `bedFadeOut` (3 s), levelled to `bedRmsDb` (−36 dB RMS), then ducked under the voice by `duckDb` (−9) with
43
+ `duckAttack` 120 ms and `duckRelease` 450 ms.
44
+ 4. Master: `"master": "loudnorm"` (default) runs ffmpeg's EBU R128 normaliser in two passes to `target.lufs` −14 /
45
+ `target.dbtp` −1 / `target.lra` 11; `"master": "limiter"` applies a linear gain and a look-ahead limiter instead
46
+ (no ffmpeg, flatter dynamics).
47
+
48
+ All of these are keys under `"mix"` in motigen.json. The canvas kit mixes inside the page with its own levels and
49
+ the effects library; `"mix": { "engine": "page" }` makes `motigen mix` save that instead.
50
+
51
+ ## Subtitles
52
+
53
+ `motigen srt` writes one cue per voice line, from its event time to that plus its duration, with the written forms
54
+ from `"spellings"` put back. `motigen build` runs it after the render, next to the video.
package/docs/canvas.md ADDED
@@ -0,0 +1,54 @@
1
+ # The canvas template: ink.js and kit.js
2
+
3
+ `motigen init canvas <dir>` makes a film on a 2D canvas. `film.js` imports two modules served by motigen:
4
+
5
+ ```js
6
+ import * as K from '/_motigen/canvas/ink.js'; // drawing primitives
7
+ import * as kit from '/_motigen/canvas/kit.js'; // the stage, cards, the mix, the contract
8
+ await kit.boot({ id, load, render, mix, logo, fonts, paper, maxDur });
9
+ ```
10
+
11
+ `load()` returns `{ dur }` after loading what the film needs; `render(ctx, f)` draws frame `f` (30 fps, so
12
+ `f = t · 30`) and must be a pure function of `f`; `mix()` returns the sound spec. `boot` loads the fonts (Geist from
13
+ the package by default), the optional logo, the sound library (`audio/sfx/sfx.json`), registers `window.__film` and
14
+ draws frame 0. Formats: `?fmt=h` is 1920×1080, `?fmt=v` is 1080×1920 (`kit.FMT`, `kit.W`, `kit.H`); keep one
15
+ `LAYOUTS` table per format.
16
+
17
+ ## ink.js
18
+
19
+ Everything is a pure function of its inputs; strokes wobble by a seed, so a frame is the same every time.
20
+
21
+ - Math: `clamp`, `lerp`, `prog(t, a, b)`, `ease.{out, inOut, outBack, …}`, `keys(t, ks)`, `mulberry32(seed)`, `noise1`.
22
+ - Paths: `linePts`, `bezierPts`, `splinePts(pts, perSeg, tension)`, `ellipsePts`, `resample`, `pointAt(pts, u)` → `{x, y, a}`, `pathLength`.
23
+ - Strokes: `inkStroke(ctx, pts, {w, seed, boilT, taper, alpha, color, wobble})`, `inkLine`, `inkRect`, `inkEllipse`, `hatch`.
24
+ - The thread: `thread(ctx, pts, {to, w, color})`, `needle(ctx, x, y, a)`.
25
+ - Paper and marks: `paperTexture(w, h, base, seed)`, `card(ctx, x, y, w, h, {r, seed, boilT, lw, shadow})`, `underline`, `highlight(ctx, x, y, w, h, {p, color, seed})`, `checkBadge`, `tally(ctx, x, y, n)`.
26
+ - Type: `text(ctx, str, x, y, {kind: 'heavy'|'sans'|'serif'|'mono'|'hand', size, weight, color, align, track, alpha, spread})`, `measure`, `typed(str, t, cps)`, `typedTimes`, `stampWords`.
27
+ - Images and camera: `drawImage`, `camera(ctx, W, H, {cx, cy, z, rot})`.
28
+ - Colours `C` (paper, ink, muted, orange, yellow, blue, card, …) and fonts `F`.
29
+
30
+ ## kit.js
31
+
32
+ - Stage: `bg(ctx, color)`, `label(ctx, text, {x, y, fs})`, `superLines(ctx, lines, x, y, f, f0, f1, {size, maxW, lift})` (letterpress landing, optional lift-off), `stampLine`, `stampScale(age)`, `fitSize`, `emphasis(ctx, x, y, angle, age, size, seed)`, `deckle`.
33
+ - Images: `loadImg`, `tryImg`, `loadSvg`, `tryJson`, `feather(img, px)` (no rectangle edge), `inkLayer` / `drawInk` (art as ink on the paper, whatever its background), `warpImage(ctx, img, w, h, quad)`.
34
+ - Logo: `drawLogo(ctx, x, y, w)`, `stampLogo(ctx, x, y, w, age)`, `logoH(w)` — when `boot({ logo })` was given an image.
35
+ - Geometry: `place(ctx, {cx, cy, s, rot})`, `mapPt`, `lerpT`, `toScreen`, `applyCam`, `camPt`, `keyPose(f, ks)`.
36
+ - Props and cards: `drawPencil(ctx, x, y, angle, boilT)`, `successChip`, `ruleCard(ctx, rect, age, f, label, body, {hl, hlText})`, `wordGroups`.
37
+ - Sound: `mix(spec)` (below), `keyClicks`, `enterClick`; `boot` exposes `__film.buildMix()`, `__film.wav()` (what `motigen mix` saves when `"mix": {"engine": "page"}`), `__film.exportMP4()` (in-browser, WebCodecs through mediabunny) and `__film.events()` (derived from the spec: voice cues and effects on the output clock).
38
+
39
+ ### The mix spec
40
+
41
+ ```js
42
+ const mixSpec = () => ({
43
+ music: { file: 'audio/bed.wav', lufs: -18.5 }, // levelled before ducking; null for none
44
+ duck: t => (t < 14 ? -6 : -10), // duck depth under the voice at that time
45
+ vo: { json: VO, cues: { L01: 10, L02: { end: 261 }, L03: { f: 280, db: 4 } } }, // frames: first word on it, or last sound ends on it
46
+ sfx: [['FX4', 130, -8, { align: 'enter', rate: 1, maxDur: 1, fadeIn: 0, fadeOut: 0, at0: false }]],
47
+ extra: async ({ ac, sfxBus, voBus, decode, lib }) => {}, // film-specific sound
48
+ target: { lufs: -14, dbtp: -1 },
49
+ });
50
+ ```
51
+
52
+ `VO` is `audio/vo/vo.json` from `motigen vo` (or the older `{ voices: { name: { lines } } }` shape); `speechStart`
53
+ and `speechEnd` in it put the first word on the frame. The effects library `audio/sfx/sfx.json` maps an id to
54
+ `{ file, hit, dur, keys?, enter? }`, where `hit` is the transient's position inside the file so it lands on the cue.
package/docs/guide.md ADDED
@@ -0,0 +1,100 @@
1
+ # The guide template: recording and the edit
2
+
3
+ A guide is a screencast of the real thing: a browser window and a terminal window on a 1920×1080 stage, cut by one
4
+ edit list, with chapters, key badges, notes and full-screen cards. `motigen init guide <dir>` sets one up.
5
+
6
+ ## Recording a take
7
+
8
+ `take.mjs` is a Node script run by `motigen record take.mjs`. It imports the recorder:
9
+
10
+ ```js
11
+ import { Term, Browser, sleep, freshContainer, waitFile } from 'motigenjs/record';
12
+ ```
13
+
14
+ ### Browser
15
+
16
+ ```js
17
+ const b = await Browser.open('take', {
18
+ width: 1600, height: 900, scale: 1.5, // the viewport the player assumes (zoom coordinates are in these CSS px)
19
+ theme: 'light', headless: true, channel: undefined,
20
+ map: { 'example.com': '127.0.0.1' }, // film a local instance under its real domain (local certificate accepted)
21
+ proxy: 'http://127.0.0.1:2081', // everything but the mapped hosts goes through it
22
+ blockAnalytics: true, // the usual analytics hosts are aborted
23
+ cookies: [{ name: 'consent', value: 'none', domain: 'example.com', path: '/', secure: true }],
24
+ guard: async page => { /* throw if example.com is not the local stack */ },
25
+ permissions: ['clipboard-read', 'clipboard-write'], origin: 'https://example.com',
26
+ });
27
+ await b.serve('https://demo.local', 'site'); // a folder as a site, inside this browser only
28
+ await b.goto('https://demo.local/');
29
+ const clip = await b.clip('site'); // frames + clip.json → take/site/
30
+ await b.moveTo('#start', { ms: 900 }); // eased mouse path with a slight arc, logged
31
+ await b.click('#start'); // moveTo + a logged click
32
+ await b.typeInto('#name', 'maya'); // each key logged for the mix
33
+ await b.scroll(500, 1400); await b.scrollTo('#more', 120, 1000);
34
+ await clip.stop();
35
+ await b.close();
36
+ ```
37
+
38
+ A clip is a folder of JPEG screencast frames and `clip.json`: `frames: [{t, file, scrollY}]`, `mouse: [{t, x, y}]`,
39
+ `clicks`, `keys`, `urls: [{t, url}]`, `viewport`, `scale`, `dur`. Times are seconds from the clip's start.
40
+
41
+ ### Terminal
42
+
43
+ ```js
44
+ const t = new Term('take', { cols: 112, rows: 30, cwd: '/path', ps1: '\\w $ ' });
45
+ await t.start(); // bash through node-pty (or $SHELL; PowerShell on Windows without bash)
46
+ await t.waitFor('$'); // until the text appears after the previous match
47
+ t.mark('install'); // a named moment, saved to term.marks.json
48
+ await t.type('ls', { cps: 13 }); // like a person, with jitter
49
+ await t.enter(); await t.paste('curl -fsSL https://example.com/install.sh | sh');
50
+ await t.waitIdle(3000); // until the stream is quiet (a TUI keeps redrawing while it works)
51
+ t.send('\x03'); // raw bytes
52
+ await t.stop();
53
+ ```
54
+
55
+ Output: `take/term.out` (the raw stream after one header line) and `take/term.tm` (`script -T` timing: seconds
56
+ since the previous chunk and its byte count, one line per chunk). The player replays this through xterm.js, so the
57
+ screen is the real one at every moment.
58
+
59
+ For a clean machine, record in a container: `freshContainer({ share: 'take', image: 'debian:bookworm-slim',
60
+ setup: 'laptop/setup.sh' })` starts one with `take/` mounted at `/share`, and `new Term('take', { backend: 'docker',
61
+ container: 'motigen-take' })` records through `script -T` inside it. Build your own image with the tools the guide
62
+ shows.
63
+
64
+ ## The edit
65
+
66
+ `edit.js` exports one object. Top level: `cols`, `rows` (the recorded terminal's size), `fps` (30), `take`
67
+ ('take'), `term` ('take/term'), `brand` (image url for the top-right mark), `termTitle`, `host` (the address bar;
68
+ default: the clip's own), `maskPath` (`[[pattern, replacement]]` for the shown path), `shots`.
69
+
70
+ Shots, in order:
71
+
72
+ | Shot | Fields |
73
+ |---|---|
74
+ | `{ card: '<html>', dur }` | a full-screen card (fades in and out) |
75
+ | `{ browser: '<clip>', segs, zoom?, chapter?, keys?, note?, vo?, typed? }` | a browser clip |
76
+ | `{ term: true, segs, zoom?, chapter?, keys?, note?, vo?, typing? }` | the terminal stream |
77
+
78
+ - `segs: [[src0, src1, dur?], …]` — source seconds played back to back; `dur` stretches or squeezes that piece.
79
+ For terminal shots a bound may be a text anchor: `'Installed acme|-0.3'` is the moment that text appears
80
+ in the stream (searched forward from the previous anchor, so repeated words resolve in order) plus an offset;
81
+ `null` is where the previous piece ended.
82
+ - `zoom: [[t, scale, cx, cy], …]` — in shot time; `cx`, `cy` is the point brought to the middle of the window, in
83
+ page CSS px for browser shots (the recorded viewport) and in window px (1520×855) for terminal shots. Only the
84
+ window's content zooms.
85
+ - `chapter: ['Step 1', 'Install']` — the pill and title top-left; it stays until the next chapter.
86
+ - `keys: [[t0 | 'anchor|off', len, 'Enter', 'label']]` — a key badge for `len` seconds.
87
+ - `note: 'text'` — a line at the bottom of the stage for the shot.
88
+ - `vo: [[lineId, seg, offset]]` — voice cues: line `L03` starts `offset` seconds after segment `seg` begins
89
+ (`-1` = the shot's start). They become `events.vo` for the mix and the subtitles.
90
+ - `typing: [[src0, src1]]` — terminal ranges where a TUI echoes typed keys (counted as key presses).
91
+ - `typed: [[src0, src1, n]]` — browser shots: n key presses spread over that range, when the recorder did not log them.
92
+
93
+ The sound events (`motigen events`) come out of the edit: voice cues, clicks (from the clip's log), key presses
94
+ (from the terminal's one-byte echoes, the logged keys, or `typed`), and Enter badges.
95
+
96
+ ## Look
97
+
98
+ `/_motigen/guide/player.css` holds the geometry and the defaults; `theme.css` in the project overrides the colours
99
+ and fonts (`--paper`, `--ink`, `--accent`, `--term`, `--font-sans`, …). The stage is 1920×1080; the window is
100
+ 1520×901 at (200, 128) with a 46 px bar. Keep the geometry: the zoom coordinates refer to it.
package/package.json CHANGED
@@ -1,6 +1,62 @@
1
1
  {
2
2
  "name": "motigenjs",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "Video as code for developers and their coding agents: a web page draws frame t, headless Chrome renders it to MP4; real terminals and browsers are recorded, narration and the mix come from the same timeline.",
5
+ "type": "module",
6
+ "bin": {
7
+ "motigen": "bin/motigen.js"
8
+ },
9
+ "exports": {
10
+ ".": "./src/index.js",
11
+ "./render": "./src/render.js",
12
+ "./browser": "./src/browser.js",
13
+ "./serve": "./src/serve.js",
14
+ "./project": "./src/project.js",
15
+ "./record": "./src/record/index.js",
16
+ "./audio": "./src/audio/index.js"
17
+ },
18
+ "files": [
19
+ "bin",
20
+ "src",
21
+ "runtime",
22
+ "templates",
23
+ "README.md",
24
+ "LICENSE",
25
+ "skill",
26
+ "docs"
27
+ ],
28
+ "engines": {
29
+ "node": ">=22"
30
+ },
31
+ "scripts": {
32
+ "test": "node --test"
33
+ },
34
+ "dependencies": {
35
+ "@fontsource/geist-mono": "^5.3.0",
36
+ "@fontsource/geist-sans": "^5.3.0",
37
+ "@xterm/xterm": "^5.5.0",
38
+ "mediabunny": "^1.65.2",
39
+ "playwright": "^1.63.0"
40
+ },
41
+ "keywords": [
42
+ "video",
43
+ "render",
44
+ "headless",
45
+ "playwright",
46
+ "ffmpeg",
47
+ "screencast",
48
+ "terminal",
49
+ "canvas",
50
+ "motion",
51
+ "narration",
52
+ "tts"
53
+ ],
54
+ "repository": {
55
+ "type": "git",
56
+ "url": "git+https://github.com/crocone/motigen.git"
57
+ },
58
+ "license": "MIT",
59
+ "optionalDependencies": {
60
+ "node-pty": "^1.1.0"
61
+ }
62
+ }