Skip to content

Repository files navigation

HowTo Recorder

YAML in, narrated video out.
Automated screen recordings of any web app — 16:9 tutorials, 9:16 shorts, AI voiceover, karaoke subtitles, and publishing to YouTube.

Playwright ElevenLabs ffmpeg TypeScript MIT

Live page • Architecture • Quick start • Actions • Plugins • YouTube

English • Deutsch


What it does

You describe a flow as YAML. The recorder drives a real Chromium through it with an animated cursor, speaks over it with an AI voice, and hands back a finished video.

Nothing about it is tied to one application: every selector, format and style value lives in recorder.config.yaml, and anything a generic click cannot reach is added as a plugin.

Three formats, one file

Demo flow Tutorial Short
Turned on by no title: title: set format: short
Purpose silent walkthrough narrated explainer vertical clip for a cold audience
Resolution 1920×1080 1920×1080 1080×1920
Output .webm .mp4 (H.264 + AAC) .mp4 with burned-in subtitles
Login visible behind a frosted title card never in the video
Audio none voice + optional music voice at 1.1× + music

Quick start

git clone https://github.com/straussbastian/howTo-Recorder.git
cd howTo-Recorder
npm install && npm run install-browsers

cp recorder.config.example.yaml recorder.config.yaml
cp .env.example .env
# point both at your app

npm run record -- example-flow

Everyday commands

npm run record -- my-scenario              # record one scenario
npm run record -- my-scenario --preview    # silent dry run, no voice-synthesis cost
npm run record -- all-howtos               # batch (master YAML)
HEADED=1 npm run record -- my-scenario     # watch the browser while it works
npm run record -- --list-actions           # every action, plugins included

npm test                                   # unit tests
npm run typecheck

Writing a scenario

Scenarios live in scenarios/*.yaml. A step is an action plus its argument; narrate: and highlight: are metadata any step can carry.

name: howto-create-user
title: "How do I create a user?"    # a title turns on the narrated pipeline
fadeOut: 1.5

steps:
  - login
  - wait.ready

  - narrate: In this tutorial I'll show you how to add a user.

  - click.text: Users
    narrate: We open the user list.
    highlight: Users               # spotlight while the sentence plays
  - wait.url: /users

  - fill:
      Name: Ada Lovelace
    narrate: We type the name.

  - click.button: Create
    narrate: One click and the user is saved.
  - wait.idle

Narration fires before the action, and the runner holds the step until the audio clip ends — so the voice never runs ahead of the picture.

Scenario fields

Field Default Meaning
title — Set it and the narrated pipeline turns on (voice, title card, MP4)
format howto howto or short — selects the whole recording profile
narration — One clip for the entire video (alternative to per-step narrate:)
music profile File in assets/ used as the music bed
musicVolume profile 0–1
speechRate profile ffmpeg atempo factor on the voice
subtitles profile Burn karaoke subtitles into the picture
targetLength profile Seconds. Warns before the browser starts; never aborts
fadeOut 0 true for 1.5 s, or a number of seconds

Pronunciation markup

Speech synthesis mangles domains, abbreviations and product names. Write what should be shown in brackets and what should be spoken after narrate::

- narrate: Everything lives at [go-lizard.com](narrate: go minus lizard dot com).

The subtitle reads go-lizard.com; the voice says "go minus lizard dot com". The karaoke timing keeps the whole substitution as one token, so it stays in step.

Batch recording

# scenarios/all-howtos.yaml
name: all-howtos
scenarios:
  - howto-create-user
  - howto-edit-profile

One failure does not stop the batch; the summary at the end lists what succeeded.


Preview mode

Speech synthesis costs money, and narration written against an imagined screen is usually wrong. --preview records the flow silently, estimates how long each sentence would take, and pulls one frame per planned narration point:

npm run record -- my-scenario --preview
[howto] PREVIEW — no speech synthesis, 6 clips estimated
6 preview frames → videos/my-scenario.preview
  01   4.2s  In this tutorial I'll show you how to add a user.
  02  11.8s  We open the user list.

Check the frames, fix the wording, then record for real. Frames rather than screenshots, deliberately: a screenshot shows the viewport, not what lands in the video.


Configuration

recorder.config.yaml

Everything app-specific lives here. Copy the example, then override only what differs — every key has a working default.

app:
  base_url: https://your-app.com
  ready_selector: ".app-shell"        # the app has finished loading
  loading_selector: ".spinner"        # must disappear before continuing
  dialog_selector: '[role="dialog"]'  # clicks and fields scope to an open modal

auth:
  login_path: /login
  selectors:
    email: "input[type='email']"
    password: "input[type='password']"
    submit: "button[type='submit']"

formats:
  short:
    speechRate: 1.2
    musicVolume: 0.1

plugins: ["./plugins/filament"]

Ready-made presets ship for WordPress, Next.js (NextAuth), React-Admin, Laravel Filament, Vue + Vuetify and Django Admin — uncomment the matching block at the bottom of the example file.

.env

RECORDER_BASE_URL=https://your-app.com
RECORDER_EMAIL=user@example.com
RECORDER_PASSWORD=secret

ELEVENLABS_API_KEY=            # only for narrated videos
ELEVENLABS_VOICE_ID=

YOUTUBE_CLIENT_ID=             # only for publishing
YOUTUBE_CLIENT_SECRET=

Optional assets

File Effect
assets/logo.svg Logo shown throughout a short. Absent → skipped
assets/*.ttf Subtitle font; name it under subtitles.font_name
assets/*.mp3 Music bed; name it under music: or formats.<name>.defaultMusic

Actions

Navigation and waiting

Action Argument Description
login — Sign in with the .env credentials
goto URL Navigate, relative or absolute
wait.ready — ready_selector present and loading finished
wait.idle — Every loading_selector element gone
wait.url substring URL contains the substring
wait.selector CSS Element is visible
wait milliseconds Hard pause

Interaction

Action Argument Description
click CSS Click the first match, cursor animated
click.text visible text Click by text, scoped to an open dialog
click.button label Click a button or link by role, exact label first
fill { Label: value } Type into a field by its label
fill.css { selector: value } Type into a field with no unique label
select.native { Label: option } Pick from a native <select>
upload file path Put a file into a file input
drag { from, to | dx, dy, edge? } Realistic mouse drag
scroll.to heading text Scroll a section into the middle
scroll.end — Step to the bottom, pulling in lazy content

Presentation

Action Argument Description
narrate text Voiceover step (also usable as step metadata)
hud text or { text, ms } Keystroke overlay
blackout / blackout.off — Curtain; survives navigation, even across domains
screenshot file path Save a PNG

Any step also accepts highlight: — a spotlight frame around an element, useful for narration steps where nothing else moves:

- narrate: Top left you filter by location.
  highlight: { target: "Location", nth: 2 }

Plugins

Plugins register extra actions for widgets a generic click cannot reach.

// plugins/my-app/index.ts
import type { Page } from 'playwright';
type ActionHandler = (page: Page, arg: unknown) => Promise<void>;

export function register(actions: Record<string, ActionHandler>): void {
  actions['modal.open'] = async (page, arg) => {
    await page.getByRole('button', { name: String(arg) }).click();
    await page.waitForSelector('.modal', { state: 'visible' });
  };
}
# recorder.config.yaml
plugins: ["./plugins/my-app"]

plugins/filament ships as a working reference: Filament selects (including searchable and multi), vue-treeselect, the TipTap rich editor, stats widgets and Fabric.js canvases.


Publishing to YouTube

Optional. Without a youtube: block in the config, everything else works unchanged.

npm run youtube-auth                                # once, see docs/youtube-setup.md
npm run publish-video -- my-scenario --dry-run      # exactly what would go up
npm run publish-video -- my-scenario

npm run playlist -- --list
npm run playlist -- --move my-scenario --to 3
npm run unpublish -- my-scenario                    # deletes for good; asks you to type the id

The recorder writes videos/<name>.timing.json next to every MP4 — narration text with its offset and duration, which is what chapter markers are built from.

Full walkthrough: docs/youtube-setup.md.


Architecture

Explore the interactive diagram →

howto-recorder/
├── recorder.config.yaml     # everything app-specific
├── .env                     # credentials and API keys
├── scenarios/               # YAML scenarios
├── plugins/                 # custom actions (filament ships as a reference)
├── assets/                  # logo, fonts, music
├── videos/                  # output: mp4, timing.json, youtube.json
└── src/
    ├── cli.ts               # entry point
    ├── config.ts            # config loader and defaults
    ├── formats.ts           # howto / short profiles
    ├── recorder.ts          # recording session and media pipeline
    ├── yaml-scenario.ts     # parser and step runner
    ├── actions.ts           # action registry and plugin loader
    ├── narration-text.ts    # pronunciation markup
    ├── subtitles.ts         # ASS karaoke subtitles
    ├── preview.ts           # frame extraction for the dry run
    ├── audio/               # ElevenLabs, tempo, ffmpeg mux
    ├── overlays/            # cursor, HUD, title card, highlight, blackout, logo
    ├── core/                # auth, waits, forms, scroll, upload, drag
    └── youtube/             # OAuth, upload, playlist, sidecar

How a recording runs

  1. Parse — the scenario resolves into steps, narration texts and a format profile.
  2. Synthesize — every clip is generated before the browser opens, so the recorded timeline never stalls on an API call. ElevenLabs returns word-level timings, which is what makes the karaoke subtitles land on the beat.
  3. Warn — with the speech duration known, an overlong video is flagged before a browser even starts.
  4. Record — Playwright drives the flow with cursor, HUD and overlay layers. For shorts the login happens in a separate, unrecorded context first.
  5. Mux — ffmpeg lays the clips at their measured offsets, mixes music, burns in subtitles before the fade, and writes the MP4 plus its timing sidecar.

Requirements

Node 20 or newer. ffmpeg ships with the install (ffmpeg-static); Chromium comes from npm run install-browsers. ElevenLabs credentials are needed only for narrated videos, YouTube credentials only for publishing.

Contributing

Issues and pull requests are welcome. npm run typecheck and npm test should pass; CI runs both. The tests cover the parser, subtitles, format profiles and YouTube logic — everything decidable without a browser or a live API.

Credits

Built and maintained by Wolf+Strauss Solutions.

The recorder grew out of the tooling behind Lizard, a Laravel/Filament product whose entire tutorial library is produced with it — which is why the bundled reference plugin targets Filament. You can watch the output on the Lizard YouTube channel.

License

MIT — see LICENSE.


Built with Playwright, ElevenLabs and ffmpeg by Wolf+Strauss Solutions
Lizard • YouTube • bastianstrauss.digital

About

YAML-driven screen recorder that turns declarative scenarios into polished How-To videos with AI voiceover, animated cursor, and title cards — for any web app.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages