retake-demos 0.2.2 → 0.4.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/.env.example +10 -0
- package/AGENTS.md +277 -0
- package/README.md +63 -22
- package/dist/captions.js +55 -0
- package/dist/cards.js +177 -0
- package/dist/cli.js +300 -15
- package/dist/describe.js +30 -10
- package/dist/dryrun.js +185 -2
- package/dist/env.js +76 -0
- package/dist/manifest.js +178 -10
- package/dist/operator/run.js +4 -0
- package/dist/operator/tools.js +111 -13
- package/dist/pace.js +99 -0
- package/dist/presets.js +7 -7
- package/dist/record.js +475 -46
- package/dist/render.js +427 -64
- package/dist/tidy.js +103 -0
- package/dist/ui/chat.html +301 -43
- package/dist/ui/guide.html +2 -1
- package/dist/ui/server.js +172 -36
- package/dist/voice.js +36 -0
- package/llms.txt +11 -0
- package/package.json +5 -3
- package/site/releases.json +45 -0
- package/skill/SKILL.md +272 -8
- package/demos/example.yaml +0 -65
- package/dist/ui/index.html +0 -724
- package/site/index.html +0 -158
package/.env.example
CHANGED
|
@@ -1,3 +1,13 @@
|
|
|
1
|
+
# Your app's demo account, for demos that start behind a login. You rarely edit
|
|
2
|
+
# this by hand: the agent asks by name and the Retake window (or `retake secret
|
|
3
|
+
# APP_USER APP_PASSWORD`) writes it here. Stays on this machine; never sent to the
|
|
4
|
+
# agent. A manifest references ${APP_USER} with `secret: true` — never the values.
|
|
5
|
+
# APP_TOTP_SECRET is the authenticator enrolment secret; ${TOTP:APP_TOTP_SECRET}
|
|
6
|
+
# becomes the current code. Blank lines below count as unset.
|
|
7
|
+
APP_USER=
|
|
8
|
+
APP_PASSWORD=
|
|
9
|
+
APP_TOTP_SECRET=
|
|
10
|
+
|
|
1
11
|
# Retake — optional. Only `retake describe` and `retake ideas` use a model; validate/dry/run/render/check never do.
|
|
2
12
|
# With a coding agent driving Retake over MCP you can leave all of this empty.
|
|
3
13
|
# Pick explicitly with RETAKE_MODEL=claude-code|codex|groq|mistral|local; otherwise the first available is used.
|
package/AGENTS.md
ADDED
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
# Using Retake from Claude Code, Codex, or any coding agent
|
|
2
|
+
|
|
3
|
+
Retake records product demos of web apps from a small YAML manifest. Its CLI
|
|
4
|
+
is deliberately agent-shaped: every command prints plain text, exits non-zero
|
|
5
|
+
on failure, and the cheap checks come before the expensive recording. An agent
|
|
6
|
+
can drive the whole loop — write, check, record, inspect, fix — without ever
|
|
7
|
+
opening the UI.
|
|
8
|
+
|
|
9
|
+
This file is also what Claude Code and Codex read when you point them at this
|
|
10
|
+
folder, so it doubles as the tool's own instructions to them.
|
|
11
|
+
|
|
12
|
+
`retake` here is the installed command (`npm install -g retake-demos`). From a
|
|
13
|
+
clone of this repo the same commands run from source as `npm run retake -- …`.
|
|
14
|
+
The MCP route is better than the CLI when it exists: `retake install` registers
|
|
15
|
+
the tools and the skill with Claude Code, so the loop below becomes tool calls.
|
|
16
|
+
|
|
17
|
+
## What a change costs
|
|
18
|
+
|
|
19
|
+
Reaching for a new recording when a re-render would do is the one mistake
|
|
20
|
+
that wastes real time. Almost nothing needs the camera twice.
|
|
21
|
+
|
|
22
|
+
| You want to change | Do this | Cost |
|
|
23
|
+
|---|---|---|
|
|
24
|
+
| size, shape, format | `retake render <dir> --preset <other>` | seconds |
|
|
25
|
+
| caption words, speed, zoom, cards, music | edit the manifest, `retake render` | seconds |
|
|
26
|
+
| **where** a caption or still sits | `nudge: <ms>` on the scene, then render | seconds |
|
|
27
|
+
| the ENDING of a demo | `retake run … --from <scene>` | the tail only |
|
|
28
|
+
| one beat, while iterating | `retake run … --until <scene>` | the head only |
|
|
29
|
+
| what the demo actually DOES | full `retake run` | the whole thing |
|
|
30
|
+
|
|
31
|
+
`--from` runs the earlier steps at full speed and off camera so the app
|
|
32
|
+
reaches the right state, then starts recording there. A seven-minute demo
|
|
33
|
+
whose last third changed costs ninety seconds.
|
|
34
|
+
|
|
35
|
+
Housekeeping, since outputs grow: `retake takes <dir>` lists the kept
|
|
36
|
+
versions of a demo and `retake restore <dir> <id>` puts one back;
|
|
37
|
+
`retake tidy` reports reclaimable disk and removes nothing without
|
|
38
|
+
`--apply`. Neither ever touches a raw recording or a take.json — those are
|
|
39
|
+
the only things a re-render cannot rebuild.
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
## Starting from nothing (the person pasted the repo link)
|
|
43
|
+
|
|
44
|
+
```
|
|
45
|
+
npm install -g retake-demos
|
|
46
|
+
mkdir -p ~/retake-demos && cd ~/retake-demos && retake install # workspace + tools + skill
|
|
47
|
+
retake ui # their window: http://localhost:4310
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
`retake install` registers the MCP tools with Claude Code (user scope) and
|
|
51
|
+
prints the Codex / Cursor config. Tools load at the start of a session, so
|
|
52
|
+
in the session that ran the install, use the CLI loop below and tell the
|
|
53
|
+
person to restart you afterwards. `retake doctor` says what, if anything,
|
|
54
|
+
is missing. Always tell the person the window's address — they may not know
|
|
55
|
+
it exists.
|
|
56
|
+
|
|
57
|
+
## The loop an agent should run
|
|
58
|
+
|
|
59
|
+
```
|
|
60
|
+
write demos/<name>.yaml
|
|
61
|
+
→ retake validate demos/<name>.yaml schema + warnings (instant)
|
|
62
|
+
→ retake dry demos/<name>.yaml every selector & wait, no camera (~30–60s)
|
|
63
|
+
→ fix what dry reports, repeat until "all N steps resolved"
|
|
64
|
+
→ retake run demos/<name>.yaml --preset preview-fast first take (~2 min)
|
|
65
|
+
→ read outputs/<name>/proof-log.md what actually happened, per step
|
|
66
|
+
→ retake check outputs/<name> pass/fail on the result
|
|
67
|
+
→ adjust, then: retake run demos/<name>.yaml final quality
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Rule of thumb: **never go straight to `run`.** `dry` catches most failures in
|
|
71
|
+
seconds; a failed `run` costs two minutes and a confusing video. When `dry`
|
|
72
|
+
fails it prints the step, the error, and the text that was on screen — read
|
|
73
|
+
that before changing anything.
|
|
74
|
+
|
|
75
|
+
## How to tell Claude Code / Codex to use it
|
|
76
|
+
|
|
77
|
+
From the Retake folder:
|
|
78
|
+
|
|
79
|
+
```
|
|
80
|
+
claude "Record a demo of http://localhost:3000 that shows a user creating
|
|
81
|
+
their first project. Use Retake: draft demos/first-project.yaml,
|
|
82
|
+
dry-run it until it passes, record a preview, then check it.
|
|
83
|
+
Show me the proof log."
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
or with Codex:
|
|
87
|
+
|
|
88
|
+
```
|
|
89
|
+
codex "Using Retake (see AGENTS.md), make a 30-second demo of the sign-up
|
|
90
|
+
flow at http://localhost:3000. Dry-run before recording."
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Both CLIs read this file automatically when run in this directory, so the
|
|
94
|
+
loop above is already in their context. If the agent works from *another*
|
|
95
|
+
folder (your app's repo), add the workspace: `claude --add-dir <your retake workspace>`
|
|
96
|
+
— or skip all this with `retake install`, which gives the agent the tools directly.
|
|
97
|
+
|
|
98
|
+
## Letting the agent draft with Retake's own scouting
|
|
99
|
+
|
|
100
|
+
The agent does not have to write the manifest from nothing:
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
retake describe <name> <url> "<one sentence of what to show>" -P <path-to-app-source>
|
|
104
|
+
retake ideas <url> -P <path-to-app-source> # 5–7 demo ideas, saved to ideas/
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
`describe` scouts the live page for real, unique selectors, reads the app's
|
|
108
|
+
source for routes / sign-in fields / things that animate, and drafts the YAML.
|
|
109
|
+
`-P` is optional but makes drafts much better. The model is whichever is set in
|
|
110
|
+
`.env` (`RETAKE_MODEL=claude-code|codex|groq|mistral|local`) — so an agent can
|
|
111
|
+
use Retake's drafting, or draft itself; both are fine.
|
|
112
|
+
|
|
113
|
+
## What the agent must know about manifests
|
|
114
|
+
|
|
115
|
+
Full reference: `README.md` → "Demo-as-code". The short version:
|
|
116
|
+
|
|
117
|
+
- **Wait for the RESULT, and know the three ways a bare selector lies.** It
|
|
118
|
+
resolves the moment the element exists, which is wrong when (a) the element
|
|
119
|
+
is the PREVIOUS action's banner still on screen — use
|
|
120
|
+
`{ action: waitFor, selector: ".landed", gone: true }` and wait for it to
|
|
121
|
+
leave; (b) the element is a shell that streams its text in — use
|
|
122
|
+
`minChars: 40`; (c) the element is there but still changing — use
|
|
123
|
+
`stableMs: 500`, which resolves when its subtree has been quiet that long.
|
|
124
|
+
Each of these cost a real take before it existed.
|
|
125
|
+
- **Wait for results, not timers.** After any click that loads or computes
|
|
126
|
+
something, `{ action: waitFor, selector: <thing that appears> }`. A fixed
|
|
127
|
+
`wait` is for pacing only.
|
|
128
|
+
- **Selectors must be unique.** Playwright runs strict: a selector matching two
|
|
129
|
+
elements fails. Prefer ids, `data-testid`, `aria-label`, then
|
|
130
|
+
`button:has-text("…")`; append `>> nth=0` only when duplicates are intended.
|
|
131
|
+
- **`run --name <x>`** records into outputs/<x> without touching the
|
|
132
|
+
original; **`render <dir> --out <newdir>`** re-renders a copy and leaves
|
|
133
|
+
the published cut alone. Revising is never overwriting.
|
|
134
|
+
- **A failed step writes `failed-step.png`** (full page, the exact moment)
|
|
135
|
+
and the failing URL into the log and take.json. Read the picture first.
|
|
136
|
+
- **`manifest.used.yaml`** sits in every output — the exact manifest this
|
|
137
|
+
video came from, not whatever the file on disk says today.
|
|
138
|
+
- **Size is a render-time choice.** One preset = one output size, always
|
|
139
|
+
(the caption band sits inside the canvas). Any take renders at any preset:
|
|
140
|
+
`retake render outputs/<name> --preset post-vertical`. Never re-record to
|
|
141
|
+
change a shape, and never set `viewport` — `validate` warns because it
|
|
142
|
+
makes one demo a different shape from the rest.
|
|
143
|
+
- **`retake contact outputs/<name>`** — one timestamped grid of the whole
|
|
144
|
+
video. This is how you review a cut without pulling frames by hand, and
|
|
145
|
+
how you check a caption is still true in the middle of its own scene.
|
|
146
|
+
Launch cuts write `contact.png` automatically.
|
|
147
|
+
- **The poster** is `thumbnail.png`. `outputs.thumbnail: { scene: <label> }` picks the scene; the person can override it in the window from any frame, a generated cover, or their own image (`POST /api/cover/<name>`). Launch mode also writes `cover.png` (the title card) and `cover-titled.png` (a real frame with the title on it) as candidates. Do not re-render to change a poster.
|
|
148
|
+
- **`mode:`** — `demo` (default) proves the interaction: nothing in the frame
|
|
149
|
+
but the product. A CLIENT WALKTHROUGH IS A DEMO. `launch` presents the
|
|
150
|
+
product in public (a launch, a post, an ad) and may carry a title card,
|
|
151
|
+
music, emphasis and a branded ending — ending with THEIR product, never
|
|
152
|
+
with Retake. Decide from the audience, not the subject; never upgrade a
|
|
153
|
+
demo yourself (`validate` warns).
|
|
154
|
+
- **voiceover** needs a continuous `script:` and `approved: true` from a
|
|
155
|
+
person who heard it. Off otherwise.
|
|
156
|
+
- **The launch cut**: `compressIdle: true` (waits shown short — render-time),
|
|
157
|
+
`voiceover: true` (captions read aloud), `typing: brisk` (re-record). Offer
|
|
158
|
+
as one package when the destination is a launch or post; default is plain.
|
|
159
|
+
- **`music: <file>`** mixes a bed under the video at render (looped, faded
|
|
160
|
+
out). The person supplies the file — never invent a path, never bundle.
|
|
161
|
+
- **Cards and callouts:** `intro: { title, subtitle }` / `outro:` splice a
|
|
162
|
+
title card (render-time; the intro frame becomes cover.png). A
|
|
163
|
+
`{ action: callout, selector, label }` step rings an element on camera.
|
|
164
|
+
At most one callout per scene; never callout what the caption already says.
|
|
165
|
+
- **Scenes carry the story.** `{ action: scene, label, caption }` at each beat;
|
|
166
|
+
captions burn into the video. The camera is still by default; `camera: auto`
|
|
167
|
+
eases toward the last thing touched, or per scene `camera: { focus:
|
|
168
|
+
<selector>, zoom: 1.3 }`. Add zoom only when the person asks.
|
|
169
|
+
- **Logins go in `auth.setup`**, never in `steps` — see "Logins" below.
|
|
170
|
+
- **Backend not available?** `stub:` answers API calls with canned JSON for the
|
|
171
|
+
take, and a `stub` step swaps the answer mid-demo. Every stub is named in the
|
|
172
|
+
proof log.
|
|
173
|
+
- **Long forms:** `{ action: scroll, to: <selector>, align: center }` before
|
|
174
|
+
filling them, so the viewer sees the whole thing.
|
|
175
|
+
|
|
176
|
+
## Logins
|
|
177
|
+
|
|
178
|
+
Most real demos start behind a sign-in. The agent never sees a password:
|
|
179
|
+
|
|
180
|
+
- **Ask by name, not for the value.** Over MCP, call `secrets` with the
|
|
181
|
+
variable names (`APP_USER`, `APP_PASSWORD`, `APP_TOTP_SECRET` for an
|
|
182
|
+
authenticator) and a one-line why. With the Retake window open, a form
|
|
183
|
+
appears there and the values go straight into the workspace `.env` — the
|
|
184
|
+
tool returns "set". Without a window, the tool returns the sentence to
|
|
185
|
+
relay: the person runs `retake secret APP_USER APP_PASSWORD` (typed into
|
|
186
|
+
their terminal, hidden, kept on their machine) and the agent calls
|
|
187
|
+
`secrets` again. From the CLI only, say the same sentence yourself.
|
|
188
|
+
- **Put the sign-in under `auth.setup`** with `${APP_USER}` / `${APP_PASSWORD}`
|
|
189
|
+
and `secret: true`, plus `auth.storageState: .auth/<name>.json`. It runs
|
|
190
|
+
before the camera, is trimmed off, and the session is reused by later takes.
|
|
191
|
+
A failed sign-in never saves a session.
|
|
192
|
+
- **Authenticator codes:** `${TOTP:APP_TOTP_SECRET}` is the current six-digit
|
|
193
|
+
code, computed when the field is filled.
|
|
194
|
+
- **SMS, SSO, captcha:** a script cannot; a person can, once.
|
|
195
|
+
`retake signin demos/<name>.yaml` opens a real window, they log in, press
|
|
196
|
+
Enter, and the session is saved. Leave `auth.setup` empty; a take with no
|
|
197
|
+
fresh session and no setup refuses to run rather than record logged out.
|
|
198
|
+
- **Firebase-style sessions** (IndexedDB) cannot be restored from a saved
|
|
199
|
+
session: put the login under plain `setup` so it runs every take.
|
|
200
|
+
- **Demo accounts only.** The output is a video.
|
|
201
|
+
|
|
202
|
+
## Reading the results
|
|
203
|
+
|
|
204
|
+
- `outputs/<name>/proof-log.md` — per-step ✓/✗ with timings, the shot list,
|
|
205
|
+
quality facts, and which endpoints were stubbed. This is the file to read
|
|
206
|
+
when something looks wrong.
|
|
207
|
+
- `outputs/<name>/take.json` — the same, machine-readable.
|
|
208
|
+
- `outputs/<name>/demo.mp4` — the video. `master.mp4` is the CRF-14 keeper
|
|
209
|
+
on post presets.
|
|
210
|
+
- `retake check outputs/<name>` — resolution, fps, duration, files,
|
|
211
|
+
pass/fail. Exit 3 on failure.
|
|
212
|
+
|
|
213
|
+
Exit codes: `run` → 0 all steps passed, 2 something failed (video still
|
|
214
|
+
produced, proof log says why). `dry` → 3 if any step would fail.
|
|
215
|
+
|
|
216
|
+
## Fixing a take without re-recording
|
|
217
|
+
|
|
218
|
+
Captions, camera zoom, hold length, trim, format and layout are **render-time**.
|
|
219
|
+
Change them in the YAML and `retake render outputs/<name>` — seconds,
|
|
220
|
+
no browser. Only changes to what the browser *does* (steps, selectors, waits,
|
|
221
|
+
viewport) need `run` again; `run --reuse` skips the browser when nothing
|
|
222
|
+
recording-relevant changed, and the UI's one button works this out for you.
|
|
223
|
+
|
|
224
|
+
## Things that trip agents up
|
|
225
|
+
|
|
226
|
+
- The app must be running and reachable at the manifest's `url` before `dry`
|
|
227
|
+
or `run`. Retake says "Nothing is running at …" if not.
|
|
228
|
+
- `describe` and `ideas` need a model configured (`.env` or the UI's Settings).
|
|
229
|
+
`validate`, `dry`, `run`, `render`, `check` never use a model.
|
|
230
|
+
- Two runs on the same demo at once are refused (a lock); wait for the first.
|
|
231
|
+
- Deleted demos go to `.trash/`; `POST /api/demos/<name>/restore` or copy the
|
|
232
|
+
file back.
|
|
233
|
+
- **Wait for durable state, not its announcement.** Toasts and banners clear
|
|
234
|
+
themselves (and often share a class with other notices); a wait on one can
|
|
235
|
+
time out while the thing it announced sits on the board. Wait for the row,
|
|
236
|
+
the count, the heading — `.act >> nth=1` beats `.landed`.
|
|
237
|
+
- `dry` runs file/command seeds first so it sees the state `run` will
|
|
238
|
+
(`--no-seed` to skip). `evaluate` seeds only run with a camera.
|
|
239
|
+
- A one-line `script:` with braces is a YAML mapping, not a string — write
|
|
240
|
+
`script: |` and put the JS on the next line.
|
|
241
|
+
- Iterating on one beat of a long demo: `run demos/x.yaml --until <scene>`
|
|
242
|
+
records up to the end of that scene and stops (MCP: `run` with `until`).
|
|
243
|
+
Better still, keep demos short — above ~40 steps, split the story.
|
|
244
|
+
- If Retake itself fails (render/ffmpeg error, crash), stop and report it —
|
|
245
|
+
do not patch around the tool.
|
|
246
|
+
- **Cursor cap: ~180 moves per take.** (Was ~45; Retake now flattens testreel's
|
|
247
|
+
cursor expressions at install.) `validate` warns past it, the recorder
|
|
248
|
+
detects a failed overlay and `check` FAILs. Split long stories anyway —
|
|
249
|
+
above ~40 steps nobody is watching.
|
|
250
|
+
- Stills: `NN-label.png` is mid-scene, `NN-label-end.png` the last moment.
|
|
251
|
+
- Scroll pacing defaults to constant pixels-per-second; `speed:` overrides.
|
|
252
|
+
- **New tabs are folded back into the recorded page** (`keepInTab`, default
|
|
253
|
+
true): `window.open` navigates in place and `target="_blank"` is stripped.
|
|
254
|
+
Retake records one page, so a popup login or a "preview" button used to lose
|
|
255
|
+
the demo's subject entirely. Set `keepInTab: false` if a flow genuinely needs
|
|
256
|
+
a separate window — and know it will not be on camera.
|
|
257
|
+
- **Iterate at `--preset draft`.** Same 960px layout as the post presets (so
|
|
258
|
+
selectors, wrapping and timing match) at a quarter of the pixels. Measured on
|
|
259
|
+
a 22s demo: capture 22.4s vs 30.1s, render 1.2s vs 3.6s — the 1080p encoder
|
|
260
|
+
adds ~8s to every take and competes with the app being recorded. Ladder:
|
|
261
|
+
`draft` while fixing, `preview-fast` to check final framing, a post preset to
|
|
262
|
+
ship. Never ship a draft.
|
|
263
|
+
|
|
264
|
+
## Every release gets a note a non-developer can read
|
|
265
|
+
|
|
266
|
+
`site/releases.json` is the one list of what shipped. The landing page and the
|
|
267
|
+
Retake window both read it — there is no second copy to update.
|
|
268
|
+
|
|
269
|
+
When a release changes anything a person can see, add an entry: a version, a
|
|
270
|
+
date, a headline, and a few lines. Write them for the person using Retake, not
|
|
271
|
+
for whoever wrote the code. Name no files. Name no flag they will not type. Do
|
|
272
|
+
not write "refactor", "regression", "schema" or "pipeline". If a line only
|
|
273
|
+
makes sense to someone who has read the source, it is not a release note —
|
|
274
|
+
it belongs in the commit message, which is where that audience already is.
|
|
275
|
+
|
|
276
|
+
Good: "You choose the cover image. Click any frame to make it the one people
|
|
277
|
+
see first." Bad: "Poster selection is now persisted via a marker file."
|
package/README.md
CHANGED
|
@@ -19,6 +19,19 @@ It is built for the way people work now: **you ask your coding agent for a demo
|
|
|
19
19
|
|
|
20
20
|
<p align="center"><img src="assets/window.png" width="900" alt="The Retake window: your demos on the left, the video and its scenes in the middle"></p>
|
|
21
21
|
|
|
22
|
+
## If you are a coding agent reading this
|
|
23
|
+
|
|
24
|
+
Someone pasted this link and wants demos recorded. Do this, in order, and say what you are doing as you go — assume they have never seen Retake:
|
|
25
|
+
|
|
26
|
+
1. `npm install -g retake-demos` (Node 20+; ffmpeg is bundled, Chromium downloads once).
|
|
27
|
+
2. Make a workspace and set up from inside it: `mkdir -p ~/retake-demos && cd ~/retake-demos && retake install`. This creates `demos/` and `outputs/`, registers Retake's tools with Claude Code and installs its skill, and prints the config for Codex/Cursor — apply it if that is what you are.
|
|
28
|
+
3. **Your tools arrive at your next session.** For this one, drive Retake from the CLI exactly as [`AGENTS.md`](AGENTS.md) describes (`validate → dry → run → check`). Tell the person: "Restart me after this and I'll have Retake's tools directly."
|
|
29
|
+
4. Start the window for them — `retake ui` in the workspace — and give them the link: http://localhost:4310. That is where the video, stills and proof log appear.
|
|
30
|
+
5. If they did not say what to record, ask one question: which app (URL) and what to show. If the app needs a login, read the "Behind a login" section below — you never handle the password yourself.
|
|
31
|
+
6. Read [`skill/SKILL.md`](skill/SKILL.md) once: it is the method (order of operations, failures as text, when to stop) and it is what Claude Code loads automatically after step 2.
|
|
32
|
+
|
|
33
|
+
Everything below is for the person; it is accurate for you too.
|
|
34
|
+
|
|
22
35
|
## Install
|
|
23
36
|
|
|
24
37
|
Needs Node 20+. ffmpeg ships with the package; Chromium is downloaded once.
|
|
@@ -48,12 +61,15 @@ outputs/<name>/
|
|
|
48
61
|
demo.mp4 the shareable — captions burned in
|
|
49
62
|
master.mp4 the CRF-14 keeper (post presets only)
|
|
50
63
|
stills/ one PNG per scene, mid-scene and at its last moment
|
|
51
|
-
thumbnail.png the frame
|
|
64
|
+
thumbnail.png the poster — pick any frame, a cover, or your own image in the window
|
|
65
|
+
cover.png launch mode: the title card, settled
|
|
66
|
+
cover-titled.png launch mode: a real frame with the title over it
|
|
52
67
|
proof-log.md result, shot list, every step's timing, pass/fail, what was on screen when it failed
|
|
68
|
+
contact.png launch cuts: one timestamped grid of the whole video (`retake contact`)
|
|
53
69
|
take.json the raw timeline the renderer reads
|
|
54
70
|
```
|
|
55
71
|
|
|
56
|
-
In the window: play it, switch speed (0.75× to 1.3×, re-rendered in seconds), download, or open its folder — stills, proof log, manifest. Demos group by the app they point at; archive what you're done with.
|
|
72
|
+
In the window: play it, switch speed (0.75× to 1.3×, re-rendered in seconds), download, or open its folder — stills, proof log, manifest. **The poster is chosen, not guessed**: scrub to a frame and press *Use this frame*, click any scene still, take one of the generated covers, or upload your own. It rewrites `thumbnail.png`, so it costs no re-render — and on a feed the poster decides whether anyone presses play. Demos group by the app they point at; archive what you're done with.
|
|
57
73
|
|
|
58
74
|
## The loop
|
|
59
75
|
|
|
@@ -82,7 +98,7 @@ preset: post-landscape # `retake presets` — 1920×1080 page + capti
|
|
|
82
98
|
viewport: { width: 1440, height: 1080 }
|
|
83
99
|
scale: 1.8 # page drawn at 1.8× so text stays crisp
|
|
84
100
|
reducedMotion: true
|
|
85
|
-
camera:
|
|
101
|
+
camera: static # still (the default); `auto` eases toward the last thing touched
|
|
86
102
|
|
|
87
103
|
seed: [] # put the app in a known state first
|
|
88
104
|
setup: [] # runs before the camera; trimmed off the video
|
|
@@ -103,9 +119,13 @@ outputs:
|
|
|
103
119
|
|
|
104
120
|
The full template is [`demos/example.yaml`](demos/example.yaml); `retake init` copies it into a new workspace.
|
|
105
121
|
|
|
106
|
-
**Steps** — `click`, `type`, `fill`, `hover`, `drag`, `scroll`, `keyboard`, `navigate`, `wait`, `waitFor`, `evaluate`, `upload`, `download`, `screenshot`, `scene`. Every step takes `pauseAfter`, `waitFor`, `timeout`, `secret`. Targets are a selector, or a point — `{x, y}` or `{selector, dx, dy}` — for canvases and SVG editors. `${VAR}` expands from `.env` and fails loudly when unset.
|
|
122
|
+
**Steps** — `click`, `type`, `fill`, `select`, `hover`, `drag`, `scroll`, `keyboard`, `navigate`, `wait`, `waitFor`, `evaluate`, `upload`, `download`, `screenshot`, `scene`. Every step takes `pauseAfter`, `waitFor`, `timeout`, `secret`. Targets are a selector, or a point — `{x, y}` or `{selector, dx, dy}` — for canvases and SVG editors. `${VAR}` expands from `.env` and fails loudly when unset.
|
|
123
|
+
|
|
124
|
+
**Waits** take three modifiers, because a bare selector resolves the moment the element exists: `gone: true` waits for it to leave (the previous action's banner), `minChars: 40` waits for it to hold real text (a streamed reply), `stableMs: 500` waits for its subtree to stop changing.
|
|
125
|
+
|
|
126
|
+
**Scenes** are the spine: `{ action: scene, label, caption }` at each beat. They take their timestamps from the actual run, so captions land on the frames they describe even when the app took 3 seconds this time and 11 the last. **Labels must be unique** — `thumbnail`, `until`, the stills and the proof log all address scenes by label, so two scenes called the same thing make every one of those ambiguous. `validate` rejects it.
|
|
107
127
|
|
|
108
|
-
**
|
|
128
|
+
**Two kinds of take.** `mode: demo` (the default) proves how something works — a client walkthrough, a PR clip, a lesson — and keeps everything but the product out of the frame. `mode: launch` presents the product in public and may carry a title card, music, emphasis and a branded ending. `validate` warns if a demo is carrying launch furniture. **The launch cut.** `compressIdle: true` shows the app's long waits as ~1.5 s each (your own pacing `wait`s are never touched); `voiceover: true` reads the captions aloud (edge-tts, `pipx install edge-tts` once); `typing: brisk` makes typed text land fast with the pauses carrying the meaning. All render-time except typing. **Music.** `music: track.mp3` (or `{ file, gainDb, fadeOutMs }`) mixes a bed under the video at render time, looped or trimmed to fit and faded out at the end. Bring your own track — it ships inside your video, so it must be one you may use (CC0 or licensed). **Cards and callouts.** `intro:`/`outro:` add a title card in Retake's own look — rendered at render time, so changing a word is a re-render, and the intro's settled frame is written as `cover.png`, the poster. `{ action: callout, selector, label, ms }` draws an animated ring and label around an element while the take holds — recorded coordinates, drawn at render, moving with the camera. Sparingly: the captions carry the story.
|
|
109
129
|
|
|
110
130
|
**Seeds** put the app in a known state before the camera: write a JSON file, run JS in the page, or run a shell command. **Setup** is the stuff that runs before recording and is trimmed off the front — logins live there. **`tempo`** speeds the finished video up or down at render time; **`lock`** names a shared resource so two demos that touch the same backend don't record over each other.
|
|
111
131
|
|
|
@@ -115,33 +135,54 @@ Think in the publishing format, not the browser window.
|
|
|
115
135
|
|
|
116
136
|
| Preset | Video | Page area | For |
|
|
117
137
|
|---|---:|---:|---|
|
|
118
|
-
| `draft` | 960×
|
|
119
|
-
| `preview-fast` | 1920×
|
|
120
|
-
| `post-landscape` *(default)* | 1920×
|
|
121
|
-
| `post-square` | 1080×1080 | 1080×
|
|
122
|
-
| `post-vertical` | 1080×1920 | 1080×
|
|
123
|
-
| `docs-gif` | 1440×
|
|
124
|
-
| `master` | 1920×
|
|
138
|
+
| `draft` | 960×540 | 960×464 | iterating — the finals' layout at a quarter of the pixels, fastest |
|
|
139
|
+
| `preview-fast` | 1920×1080 | 1920×930 | checking timing and framing, hardware-encoded |
|
|
140
|
+
| `post-landscape` *(default)* | 1920×1080 | 1920×930 | the general-purpose demo, plus a CRF-14 master |
|
|
141
|
+
| `post-square` | 1080×1080 | 1080×960 | feeds |
|
|
142
|
+
| `post-vertical` | 1080×1920 | 1080×1800 | shorts and reels |
|
|
143
|
+
| `docs-gif` | 1440×900 | 1440×800 | README and docs GIFs |
|
|
144
|
+
| `master` | 1920×1080 | 1920×930 | archive, CRF 12 |
|
|
125
145
|
|
|
126
|
-
**
|
|
146
|
+
**One preset, one size — and size is not recorded.** The caption band sits inside the canvas, so `post-landscape` is always exactly 1920×1080 and two demos of the same app are never different shapes. Better still, the shape is decided at render: the take is fitted into whichever canvas you ask for, so `retake render outputs/x --preset post-vertical` gives you a 9:16 cut of a demo you recorded landscape — seconds, no browser, no re-record. Setting `viewport` in a manifest overrides the page and `validate` warns, because that is how one demo ends up a different shape from the rest.
|
|
127
147
|
|
|
128
|
-
**Page scale** is what makes it crisp: the page lays out as if the viewport were smaller and every glyph is drawn at 2×, so 1080p text reads on a phone. **Camera** eases toward whatever the demo just touched, at render time, clamped so it can never crop the thing it points at
|
|
148
|
+
**Page scale** is what makes it crisp: the page lays out as if the viewport were smaller and every glyph is drawn at 2×, so 1080p text reads on a phone. **Camera** is still by default — a calm demo reads as real. `camera: auto` eases toward whatever the demo just touched, at render time, clamped so it can never crop the thing it points at; per scene: `camera: { focus: ".result", zoom: 1.3 }`. Zoom is something you ask for after seeing a take, not something that happens to you. Captions, camera, speed, trim, format and layout are all render-time: change them and `retake render` takes seconds, never touches the browser.
|
|
129
149
|
|
|
130
|
-
##
|
|
150
|
+
## Behind a login
|
|
151
|
+
|
|
152
|
+
Most real demos start on the far side of a sign-in. You never put a password anywhere an agent can read it.
|
|
153
|
+
|
|
154
|
+
**With the window open** (`retake ui`): when the agent sees the app needs a login, a small form appears in the window asking for a demo account by name — `APP_USER`, `APP_PASSWORD`. You type them; they are written to one local file, the workspace `.env` (readable by you only), and never sent to the agent, to any server, or to anyone. The agent learns only that the names are set.
|
|
155
|
+
|
|
156
|
+
**Without the window:** the agent relays one line — run this in your workspace and type the values (hidden):
|
|
157
|
+
|
|
158
|
+
```bash
|
|
159
|
+
retake secret APP_USER APP_PASSWORD
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
**Then:** the login runs *before the camera* and is trimmed off the front; the video starts signed in. The session is saved to `.auth/`, so later takes skip the login. A wrong password stops the take instead of recording a logged-out one.
|
|
163
|
+
|
|
164
|
+
**Two-factor.** Authenticator apps: add the enrolment secret as `APP_TOTP_SECRET` and the manifest uses `${TOTP:APP_TOTP_SECRET}` — Retake computes the current code when it fills the field. SMS codes, SSO, captchas — anything only a person can do:
|
|
165
|
+
|
|
166
|
+
```bash
|
|
167
|
+
retake signin demos/<name>.yaml
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
opens a real browser; you log in by hand, press Enter, and Retake keeps the session (not the password) for every later take.
|
|
171
|
+
|
|
172
|
+
What the agent writes:
|
|
131
173
|
|
|
132
174
|
```yaml
|
|
133
175
|
auth:
|
|
134
176
|
storageState: .auth/myapp.json # session saved here, reused until stale
|
|
135
177
|
maxAgeHours: 8
|
|
136
|
-
setup:
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
178
|
+
setup:
|
|
179
|
+
- { action: fill, selector: "#user", text: "${APP_USER}" }
|
|
180
|
+
- { action: fill, selector: "#password", text: "${APP_PASSWORD}", secret: true }
|
|
181
|
+
- { action: fill, selector: "#code", text: "${TOTP:APP_TOTP_SECRET}", secret: true } # only if asked
|
|
182
|
+
- { action: click, selector: "button[type=submit]" }
|
|
140
183
|
```
|
|
141
184
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
Apps that keep their session in IndexedDB (Firebase) can't be restored from `storageState`; put the login in plain `setup` instead.
|
|
185
|
+
`secret: true` keeps a value out of the terminal, the window and the proof log — but not out of the video, which is why `validate` warns when a secret step is in `steps` rather than `auth.setup`. Use a demo account: the output is a video you may publish. Apps that keep their session in IndexedDB (Firebase) can't be restored from a saved session; put the login under plain `setup` instead.
|
|
145
186
|
|
|
146
187
|
## Limits worth knowing
|
|
147
188
|
|
package/dist/captions.js
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/** Word-wrap long captions; two-line captions break near the middle so no
|
|
2
|
+
word is left orphaned on the second line. */
|
|
3
|
+
export function wrap(text, max) {
|
|
4
|
+
if (text.length <= max)
|
|
5
|
+
return text;
|
|
6
|
+
const words = text.split(/\s+/);
|
|
7
|
+
if (text.length <= max * 2) {
|
|
8
|
+
let best = 1, bestDiff = Infinity;
|
|
9
|
+
for (let i = 1; i < words.length; i++) {
|
|
10
|
+
const a = words.slice(0, i).join(" ").length;
|
|
11
|
+
const b = words.slice(i).join(" ").length;
|
|
12
|
+
if (a <= max && b <= max && Math.abs(a - b) < bestDiff) {
|
|
13
|
+
best = i;
|
|
14
|
+
bestDiff = Math.abs(a - b);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return words.slice(0, best).join(" ") + "\n" + words.slice(best).join(" ");
|
|
18
|
+
}
|
|
19
|
+
const out = [];
|
|
20
|
+
let line = "";
|
|
21
|
+
for (const w of words) {
|
|
22
|
+
if (line && (line + " " + w).length > max) {
|
|
23
|
+
out.push(line);
|
|
24
|
+
line = w;
|
|
25
|
+
}
|
|
26
|
+
else
|
|
27
|
+
line = line ? line + " " + w : w;
|
|
28
|
+
}
|
|
29
|
+
if (line)
|
|
30
|
+
out.push(line);
|
|
31
|
+
return out.join("\n");
|
|
32
|
+
}
|
|
33
|
+
/** Characters per caption line at this width and font size. */
|
|
34
|
+
export const maxCharsFor = (width, fontSize) => Math.max(28, Math.round((width / fontSize) * 1.55));
|
|
35
|
+
/** How many lines the longest caption needs. */
|
|
36
|
+
export function captionLines(texts, width, fontSize) {
|
|
37
|
+
const max = maxCharsFor(width, fontSize);
|
|
38
|
+
return Math.max(1, ...texts.map((t) => wrap(t, max).split("\n").length));
|
|
39
|
+
}
|
|
40
|
+
/** The band's height for this take: fitted to the text, or the preset's
|
|
41
|
+
fixed band when the canvas is the constraint. 0 when captions are off. */
|
|
42
|
+
export function bandHeightFor(q, captionTexts) {
|
|
43
|
+
if (q.layout !== "band" && q.layout !== "card")
|
|
44
|
+
return 0;
|
|
45
|
+
if (!q.captions)
|
|
46
|
+
return 0;
|
|
47
|
+
// A band with nothing to draw in it is an empty strip on the video: when
|
|
48
|
+
// the texts are known and none exist, the page gets the whole frame.
|
|
49
|
+
if (captionTexts && !captionTexts.some((t) => t && t.trim()))
|
|
50
|
+
return 0;
|
|
51
|
+
// Fixed per preset. The default page is the canvas minus this, so the
|
|
52
|
+
// finished video is exactly the preset's size — every take the same shape,
|
|
53
|
+
// and no player letterboxing it.
|
|
54
|
+
return q.bandHeight;
|
|
55
|
+
}
|