talktrack 0.0.0 → 0.1.1
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 +21 -0
- package/README.md +139 -0
- package/dist/index.cjs +678 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +200 -0
- package/dist/index.d.ts +200 -0
- package/dist/index.js +658 -0
- package/dist/index.js.map +1 -0
- package/package.json +101 -2
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Joseph Mearman
|
|
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
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# talktrack
|
|
2
|
+
|
|
3
|
+
Narrated, cursor-driven demo recordings from Playwright. Playwright drives the page, talktrack makes the recording read like a person at the keyboard with someone talking over their shoulder: a visible cursor that glides, clicks and types at human speed, caption text timed to what was actually happening on screen, and a framed result with a header band above the app and the narration below it.
|
|
4
|
+
|
|
5
|
+
It works inside `@playwright/test` specs and from plain Node scripts, with the same helpers in both.
|
|
6
|
+
|
|
7
|
+
## Requirements
|
|
8
|
+
|
|
9
|
+
- Node 20 or newer
|
|
10
|
+
- `@playwright/test` 1.55 or newer (a peer dependency)
|
|
11
|
+
- `ffmpeg` and `ffprobe` on `PATH`. The framing pass shells out to them and needs the `libvpx-vp9` encoder, which a standard `brew install ffmpeg` or `apt-get install ffmpeg` includes. If either binary is missing, the error says so.
|
|
12
|
+
|
|
13
|
+
```sh
|
|
14
|
+
pnpm add -D talktrack
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## In a Playwright test
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
import { test } from "@playwright/test";
|
|
21
|
+
import {
|
|
22
|
+
finalizeFramedRecording,
|
|
23
|
+
gotoWithCursor,
|
|
24
|
+
installCursor,
|
|
25
|
+
moveAndClick,
|
|
26
|
+
narrateScene,
|
|
27
|
+
} from "talktrack";
|
|
28
|
+
|
|
29
|
+
const size = { width: 1440, height: 810 };
|
|
30
|
+
test.use({ viewport: size, video: { mode: "on", size } });
|
|
31
|
+
|
|
32
|
+
test("checkout demo", async ({ page }) => {
|
|
33
|
+
await installCursor(page);
|
|
34
|
+
await gotoWithCursor(page, "https://example.com");
|
|
35
|
+
await narrateScene(page, "The page loads", () => page.getByRole("heading").waitFor());
|
|
36
|
+
await moveAndClick(page, page.getByRole("link", { name: "More information" }));
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test.afterEach(async ({ page }, testInfo) => {
|
|
40
|
+
await finalizeFramedRecording(page, testInfo);
|
|
41
|
+
});
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Set the video `size` to the viewport size. Playwright otherwise scales the recording down to fit inside 800x800, the bands would no longer line up with the picture, and `finalizeFramedRecording` throws rather than produce a mis-laid-out file.
|
|
45
|
+
|
|
46
|
+
`finalizeFramedRecording` closes the page (Playwright only finalises a video once its page is closed), writes `<video>-framed.webm` next to the raw recording and attaches it to the test. It returns the framed path, or `undefined` when the run has no video.
|
|
47
|
+
|
|
48
|
+
## From a script
|
|
49
|
+
|
|
50
|
+
```ts
|
|
51
|
+
import { recordDemo } from "talktrack";
|
|
52
|
+
|
|
53
|
+
const demo = await recordDemo("checkout", {
|
|
54
|
+
viewport: { width: 1440, height: 810 },
|
|
55
|
+
url: "https://example.com",
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
await demo.narrateScene("The page loads", () => demo.page.getByRole("heading").waitFor());
|
|
59
|
+
await demo.moveAndClick(demo.page.getByRole("link", { name: "More information" }));
|
|
60
|
+
demo.caption("That is the whole flow");
|
|
61
|
+
await demo.beat(1500);
|
|
62
|
+
demo.hideCaption();
|
|
63
|
+
|
|
64
|
+
const framedPath = await demo.finish();
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
`recordDemo` launches Chromium (or uses the `browser` / `browserType` you pass), records at exactly the viewport size and writes to `talktrack-output/` unless you set `outputDir`. Call `finish()` once at the end: it closes the recording context so Playwright flushes the video, frames it and returns the path.
|
|
68
|
+
|
|
69
|
+
## Helpers
|
|
70
|
+
|
|
71
|
+
All take the `page` first in test mode; on a `recordDemo` session they are methods without it.
|
|
72
|
+
|
|
73
|
+
| Helper | What it does |
|
|
74
|
+
|---|---|
|
|
75
|
+
| `installCursor(page)` | Injects the cursor overlay and starts the recording clock. Call it first. |
|
|
76
|
+
| `gotoWithCursor(page, url)` / `reloadWithCursor(page)` | Navigate without the cursor snapping back to the corner. |
|
|
77
|
+
| `moveAndClick(page, locator, pauseMs?)` | Scrolls the target into view, glides the cursor to it along an eased, slightly curved path, clicks. |
|
|
78
|
+
| `fillVisibly(page, locator, text, pauseMs?)` | Clicks into the field and types character by character. |
|
|
79
|
+
| `naturalScrollIntoView(page, locator)` | Scrolls with real wheel events instead of an instant jump. |
|
|
80
|
+
| `showCaption(page, text)` / `hideCaption(page)` | Start and end a narration line. |
|
|
81
|
+
| `narrateScene(page, text, settle, opts?)` | Shows a caption, waits for `settle` (the real page condition the caption claims), holds a short dwell, hides it. |
|
|
82
|
+
| `beat(page, ms?)` | A jittered pause for reading time. |
|
|
83
|
+
|
|
84
|
+
Captions are timestamps, not an in-page overlay. Playwright records exactly the browser viewport, so anything the page draws is inside the recorded rectangle; the framing pass is what puts narration below the app.
|
|
85
|
+
|
|
86
|
+
## Framing and styling
|
|
87
|
+
|
|
88
|
+
The framed recording is the raw video padded onto a taller canvas: a static header band (the short commit hash and recording time by default, just the time outside a git repository) above the app, and a footer band that shows each caption for exactly the span it was on screen. The bands are rendered as PNGs by a throwaway browser context and composited with ffmpeg's `pad` and `overlay` filters, so no ffmpeg build with `drawtext` is needed.
|
|
89
|
+
|
|
90
|
+
Override any part of the look, and the header text:
|
|
91
|
+
|
|
92
|
+
```ts
|
|
93
|
+
await finalizeFramedRecording(page, testInfo, {
|
|
94
|
+
headerText: "Checkout flow, v2",
|
|
95
|
+
bands: { background: "#0b1020", footerHeightPx: 120, footerFontSizePx: 24 },
|
|
96
|
+
});
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
`bands` accepts any subset of `BandStyle`: `headerHeightPx`, `footerHeightPx`, `background`, `textColor`, `fontStack`, `headerFontSizePx`, `footerFontSizePx`. The same options exist on `recordDemo`.
|
|
100
|
+
|
|
101
|
+
## Development
|
|
102
|
+
|
|
103
|
+
TypeScript, pnpm, Turborepo, Playwright. You need Node 22 (see `.tool-versions`), the pnpm version in `packageManager`, and `ffmpeg` plus `ffprobe` with the `libvpx-vp9` encoder on `PATH`; the end-to-end suite needs them and nothing else does.
|
|
104
|
+
|
|
105
|
+
```sh
|
|
106
|
+
pnpm install
|
|
107
|
+
pnpm exec playwright install chromium # once, for the e2e suite
|
|
108
|
+
pnpm typecheck && pnpm lint && pnpm test && pnpm test:e2e
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
`pnpm prepush` runs that whole matrix through turbo and is what the pre-push hook executes.
|
|
112
|
+
|
|
113
|
+
### Architecture
|
|
114
|
+
|
|
115
|
+
`src/` splits the pure halves (`human-path.ts` cursor geometry, `filter-graph.ts` caption windows and the ffmpeg filter graph, `timeline.ts` per-page narration events) from everything that touches a browser, the filesystem or ffmpeg (`cursor.ts`, `bands.ts`, `ffmpeg.ts`, `frame.ts`). The two adapters, `test-adapter.ts` (`finalizeFramedRecording`) and `standalone.ts` (`recordDemo`), are thin wrappers over `frameRecording`, so both driving modes produce identical output.
|
|
116
|
+
|
|
117
|
+
Tests are `test/unit/` (Vitest, pure modules) and `test/e2e/` (Playwright against `test/fixtures/demo-page.html`, driving both modes and probing the framed video with ffprobe).
|
|
118
|
+
|
|
119
|
+
### Conventions
|
|
120
|
+
|
|
121
|
+
- Turbo task names are the underscore-prefixed leaf commands (`_build`, `_lint`, ...). Run the public names (`pnpm lint`, `pnpm test`); they go through turbo and its cache.
|
|
122
|
+
- Conventional commits, enforced by commitlint in the `commit-msg` hook and in CI. Releases are cut from them by semantic-release on every push to `main` (`release.config.ts`); never bump the version or edit the changelog by hand.
|
|
123
|
+
- Dependencies are added through the package manager and pinned exactly (`saveExact`). Anything younger than the configured release age waits, except the org's own `@exadev/*` tooling package, which is exempt in `pnpm-workspace.yaml` and `.npmrc`.
|
|
124
|
+
- `AGENTS.md` and `CLAUDE.md` are symlinks to this file. Keep them that way.
|
|
125
|
+
|
|
126
|
+
### Releasing
|
|
127
|
+
|
|
128
|
+
Pushing to `main` runs the `release` job in `.github/workflows/ci.yml` after the required checks pass. It publishes to npm through trusted publishing (OIDC), so there is no npm token anywhere: the publisher is registered on npmjs.com for this repository and the `ci.yml` workflow. The release commit and tags are pushed to `main` straight from the job, which the ruleset on `main` otherwise forbids, so the job checks out with the repository's write deploy key (secret `RELEASE_DEPLOY_KEY`), the one actor the ruleset lets bypass its pull-request requirement. `release.config.ts` names the SSH repository URL so semantic-release pushes through that key rather than over HTTPS with the workflow token. Tags are `talktrack@<version>`, continuing from `talktrack@0.0.0`, the tag that marks the stub the npm name was reserved with.
|
|
129
|
+
|
|
130
|
+
### Gotchas
|
|
131
|
+
|
|
132
|
+
- The framing pass needs the recording to be exactly viewport-sized. In `@playwright/test`, set `video: { mode: "on", size }` to the viewport size; Playwright otherwise scales the video down and `finalizeFramedRecording` throws rather than produce a mis-laid-out file.
|
|
133
|
+
- `finalizeFramedRecording` closes the page. Playwright only finalises a video once its page is closed, so the adapter does it itself; call it from `afterEach`, not mid-test.
|
|
134
|
+
- Do not set `registry-url` in `actions/setup-node` anywhere: it writes an `_authToken` line that wins over the OIDC exchange and silently breaks publishing.
|
|
135
|
+
- The ESLint config bans inline `eslint-disable` comments. Fix the code or restructure the test (for example `test.info()` instead of an empty destructuring pattern).
|
|
136
|
+
|
|
137
|
+
## Licence
|
|
138
|
+
|
|
139
|
+
MIT
|