← Back to Skills

recording-walkthrough-videos

active

Creates narrated screen-recorded walkthrough videos from written guides for web apps (Odoo, Business Central, any SaaS). Handles TTS synthesis, Playwright recording, ffmpeg montage with per-step synchronisation. Use when a guide or walkthrough document needs to become a video with voiceover, when feedback arrives about voice/action sync issues, or when recordings show pages jumping instead of being clicked.

Owner: cassanelligiovanni Category: workflow-automation Version: 1.1.0 Tokens: ~44k
videowalkthroughttsplaywrightffmpegodoobusiness-centralnarrationscreen-recording
92

Quality Score Breakdown

Structure (15%) 18/18
Show checks (11)
  • ✓ SKILL.md exists with exact casing 3/3
  • ✓ Valid YAML frontmatter 3/3
  • ✓ No unexpected frontmatter keys 1/1
  • ✓ Name field valid (kebab-case) 2/2
  • ✓ Name matches folder name 1/1
  • ✓ Description field present 2/2
  • ✓ No angle brackets in frontmatter 1/1
  • ✓ Folder name is kebab-case 1/1
  • ✓ No README.md inside skill folder 1/1
  • ✓ Test directory with test-cases.yml exists 2/2
  • ✓ Status 'active' is valid 1/1
Description (20%) 14.5/22
Show checks (7)
  • ✓ Contains action verbs: creates, handles 4/4
  • ✓ Contains trigger indicators: use when 2.5/5
  • ✓ Description is specific and actionable 4/4
  • ✗ Skill handles files but description doesn't mention file types 0/3
  • ✓ Description length: 410/1024 chars 2/2
  • ✗ Consider adding 'Do NOT use for...' to reduce over-triggering 0/2
  • ✓ Owner/author specified in metadata 2/2
Instructions (25%) 28/28
Show checks (8)
  • ✓ Skill body has content 3/3
  • ✓ Has step/section structure 4/4
  • ✓ Includes examples 5/5
  • ✓ Includes error handling 4/4
  • ✓ Uses progressive disclosure (references/scripts) 4/4
  • ✓ Actionable language: 6/10 verb patterns found 3/3
  • ✓ Word count: 2957/5000 2/2
  • ✓ All referenced paths exist 3/3
Test Coverage (25%) 29/29
Show checks (10)
  • ✓ test-cases.yml exists and parses 3/3
  • ✓ 9 should-trigger tests ✓ 4/4
  • ✓ 6 should-not-trigger tests ✓ 3/3
  • ✓ 7 functional tests ✓ 5/5
  • ✓ 3 negative tests ✓ 3/3
  • ✓ 2 edge case tests ✓ 3/3
  • ✓ Performance baseline documented 2/2
  • ✓ All functional tests have ≥2 assertions 2/2
  • ✓ All trigger phrases are diverse 2/2
  • ✓ All assertions are specific 2/2
Security (15%) 14/15
Show checks (5)
  • ✓ No secrets detected 5/5
  • ✓ No injection vectors in frontmatter 3/3
  • ✓ Name is not reserved 3/3
  • ✓ No suspicious code patterns 2/2
  • ✗ 6 external URLs found — verify they're necessary: https://…, http://www.w3.org/2000/svg, https://api.elevenlabs.io/v1/voices 1/2

Suggestions

  • → Add negative triggers like 'Do NOT use for simple X — use Y skill instead'

Test Coverage

9
Should Trigger
6
Should Not Trigger
7
Functional
3
Negative
2
Edge Cases
No
LLM Evals

Recording walkthrough videos

Prerequisites / ensure-deps

Before starting any video project, verify all dependencies are installed. Run the check script from the skill directory:

python3 scripts/check.py <projectDir>

If the script fails or reports missing tools, install the required dependencies:

Required tools

  1. Python 3.8+ with pip
  2. Node.js 18+ with npm
  3. ffmpeg (for video assembly)
  4. Playwright browsers (system Chrome)

Installation steps

# Check if ffmpeg is available
ffmpeg -version || sudo apt-get install -y ffmpeg

# Check Node.js
node --version || echo "Install Node.js 18+ from nodejs.org"

# Check Python
python3 --version || echo "Install Python 3.8+"

# Install Playwright and browsers (in the video project)
cd <projectDir>
npm install playwright
npx playwright install chromium

Authentication (one-time human setup)

  • Business Central: Uses a persistent Chrome profile (profileDir in video.config.json). Run node <skill>/scripts/session_check_bc.js --headed once to complete MFA login manually. The session persists in the profile.
  • Odoo: Uses JSON-RPC login with authCmd pointing to credentials in a local env file (e.g., ~/.velais/odoo-sandbox.env). The session file auto-refreshes.

These logins are human one-time actions — the recording scripts reuse the saved session, they do not perform silent auto-login.

Verify setup

After installing dependencies, re-run the check:

python3 scripts/check.py <projectDir>

The check validates: scene step names, narration anchor uniqueness, sentence endings, and estimates video length. A non-zero exit code means errors that must be fixed before recording.


Overview

A narrated walkthrough video is a scripted recording (no model in the loop while filming) whose narration is synchronised per step, not per scene: every sentence is attached to the exact action it describes, and the montage fits each step’s footage to its sentence. Navigation is clicked the way a viewer would click. Everything is measured with real timestamps, never assumed.

One pipeline, two proven app adapters:

AppSessionLocatorsReferenceWorked example
Odoo 19JSON-RPC login → host-only cookie (authCmd, sessionFile, auto re-auth)field, tab, menuGo, row, card built into record.js; probe.jsodoo-19-ui.mdworked-example.md — Telchar, 26 min
Business Central (it-it)persistent Chrome profile (profileDir = chrome-profile-video2, human MFA once), shared by parallel recordingsscripts/bc_helpers.js (iframe, aria-labelledby, stacked pages, grids, lookups, Cerca); probe_bc.js, session_check_bc.js, scenes.example.bc.jsbc-ui.md — read it whole before the first BC sceneworked-example-bc.md — Saito, 17 min

Any other web app: the Odoo path minus the Odoo locators (write a small <app>_helpers.js the same way as the BC one).

When to use

  • A guide exists (markdown + demo data + reset/replay scripts) and the client wants “the same, as a video”.
  • Feedback on a cut: “parla dei reparti ma ha già cliccato”, “salta da una pagina all’altra”, “si vede fermo”, “i riquadri sono sballati”.
  • Any browser demo with a generated or recorded voice (see Voices).

Not for live capture of a human, or silent GIFs of a flow.

Project layout (one folder per video, next to the guide)

video/
  video.config.json  baseUrl, viewport [1600,900], chromeArgs ["--no-proxy-server"], locale, timezone
                     + Odoo: sessionFile, authCmd, loginPath   |   + BC: tenantPath, company, profileDir
                     + optional: channel ("chromium" when Google Chrome is not installed; default "chrome"),
                       clientLogo (path relative to the project) + clientName → client logo on title cards
  scenes.js          module.exports = (h) => ({ S00: async () => {…} })   ← scenes.example.js (Odoo) / scenes.example.bc.js (BC)
  narration.json     [{id, label, segments:[{at, text}]}]  (at = step name)
  pages.json         title / terminal / file pages → python3 scripts/pages.py pages.json pages/
  ids.json           record ids + tokens resolved by API AFTER the data replay (+ BC: pre.py writes ids.json › video)
  probes/            read-only DOM probes, one per page you will click (keep them: they are the evidence)
  package.json       npm i playwright   (system Chrome is used: channel "chrome")
  clips/ audio/ build/ profile/   generated — never committed (media stay local or on a media branch)

Odoo authCmd: ["python3", "<skill>/scripts/auth_odoo.py", "~/.velais/odoo-sandbox.env", "~/.velais/odoo-edu-session.txt"]. BC: "profileDir": "~/.claude/chrome-profile-video2", "tenantPath": "<tenant>/<env>", "company": "…"; check with node <skill>/scripts/session_check_bc.js from the project dir. One browser on a profile at a time (parallel videos: § Parallel recordings).

The pipeline (in this order)

  1. Fresh data, then ids. Run the guide’s reset → seed → demo flows; create what will be worked live on camera; resolve ids into ids.json. Keep stock/records for TWO runs (headless replay + live recording) and a pre.py that rebuilds the “already done by the office” state before every recording pass.
  2. Probe before you script. Odoo: node scripts/probe.js video/ /odoo/accounting --menus "Fornitori". BC: copy scripts/probe_bc.js into probes/, one per page/menu/button, dump captions and actions, walk the write path once (create → fill → post) and verify the outcome through the API. A selector you did not see in the DOM is a guess, and guesses fail at minute 12 of the recording.
  3. Scenes = steps. One h.step('<name>', …) per thing the voice will say; names unique per scene (anchors are looked up by name — a second registra in the same scene silently breaks the montage). Fixed pauses 1–3 s, real clicks on menus/rows/breadcrumbs, highlight() on the fields the voice names (the box follows the element), arrow(loc, 'Registra', 'bottom') for a labelled pointer, zoom(loc, 1.7) for small details, type/upload/ scroll/frame for input. Deep links only in a scene’s first apri step. Files, terminals and title cards are local pages from pages.py with real outputs. Numbers created live are handed to later scenes through clips/runtime.json.
  4. Narration as segments — plain language for a newcomer, digits and acronyms spelled for TTS, product labels quoted as shown, one WAV per segment. Two layers per scene: the action (what is clicked) and the context (why the office does it, how it was done before, why the system stops you, how to fix it) — context goes where the footage has silent room. Rules, word budgets, the context rules and the review-lens prompts: narration-and-sync.md. python3 scripts/check.py video/ validates anchors, uniqueness, sentence endings and estimates the length (≈ 2.5 words/s) before anything is recorded (steps inside helper functions are not seen by its regex: those anchors are validated by steps.json after recording).
  5. Review the narration with independent lenses (a Sonnet subagent: sentences longer than the step, English terms, claims not shown on screen), apply, then synthesise — see Voices.
  6. Record: node scripts/record.js video/ [S01,S02] (BC: same script; the project may keep a record_bc.js copy). Each scene → clips/<S>.webm + clips/<S>.steps.json (t0/t1/ok per step; a failed step is logged ERR with the full Playwright message). Re-record only scenes with a failed step — after resetting the app state those scenes left behind (documents are real). When a probe passes but the scene fails, run a diagnostic mini-scene through record.js (scenes.diag.js + symlink) with log() after every sub-click: the recorder’s click glides, scrolls and waits, a probe’s does not.
  7. Review sync with the real timestamps, move sentences to steps with room, then W=1600 H=900 MAX_SPEED=1.6 python3 scripts/assemble.py video/ out.mp4 (Odoo footage tolerates 2.5×; BC page loads make 2.5× unwatchable). Read the per-step report: a fermo above ~10 s is a sentence on the wrong step; a muto ≥ 4 s is a silent gap → add a context sentence there (~2 words per silent second), re-TTS, re-assemble. Unchanged segments are not re-encoded.
  8. Verify, then deliver: look at frames at 3–4 segment boundaries (boxes on the right element, no tooltip, no stale dialog), ffprobe the duration, confirm an audible audio stream (volumedetect), phone copy (scale=1152:720 -crf 30, ≈ 1 MB/min, ≤ 30 MiB). Say the length, what is live vs pre-built, every step that was simplified or failed, and check nothing sensitive is on screen or in URLs.

Parallel recordings (several videos at once)

Several Claude sessions may each record a video at the same time — nothing to coordinate by hand. For BC (profileDir set) record.js, probe_bc.js and session_check_bc.js do not launch Chrome themselves: they open a tab in ONE shared headless Chrome per profile (scripts/bc_browser.js), started by the first client and closed by itself after 15 idle minutes; each tab records its own clip (page.screencast). One login serves every video. Locks in ~/.claude/video-locks/ stop real conflicts and exit 3 naming the holder. Status: node scripts/bc_browser.js status · node scripts/video_lock.js status (company! = exclusive).

ResourceRuleWhy
Chrome profile"profileDir": "~/.claude/chrome-profile-video2" for every video (shared, one tab each)~/.claude/chrome-profile belongs to the Playwright MCP: a shared browser on it locks the MCP out until idle-exit
Project folderone recorder per project (all its scenes in one run)clips/_tmp, durations.json, runtime.json are per project
Company (BC) / database (Odoo)shared (Saito: Saito - Test v6, never the Golden itself); "exclusiveCompany": true in video.config.json for a video that changes setup, closes a period (VAT settlement) or deletes data — nobody else records there meanwhilecopying a company per video costs too much time
TTS · check.py · assemble.pyno lockpure files

A video on a shared company has a pre.py that only adds: its own documents on its own customer / vendor / item (stock is shared — two videos shipping one item starve each other), no Setup/No. Series changes, no deletes or resets outside what it created, and scenes open their documents by id (ids.json), never “the latest in the list”. The other video’s documents may show in lists: filter, or accept it.

Shared-browser consequences (all tabs live and die together):

  • Session expired (login page in a clip): wait until video_lock.js status shows no record, then node scripts/bc_browser.js stop → session_check_bc.js --headed (human MFA) → re-record the failed scenes. Never stop while another session is recording: it kills its tabs.
  • A scene with a download (DownloadFromStream closes the Chrome context = every tab): run that video alone, VIDEO_SOLO=1 node scripts/record.js … on a profile nobody else uses (own browser, profile locked).
  • Deploys/app changes break every running video: hold the project’s environment lock for the batch.
  • Measured: 4 recordings started in the same second (Cerca → list → card) all passed; scenes ~12 % slower than alone.
  • End every script with await br.close() (the object from bc_browser.open()): it closes YOUR tabs, then disconnects. A bare page.close() leaves the process connected forever, holding its locks. A disconnect without closing leaves a live BC tab in the shared Chrome, and the server never idles out while one is open.
  • JS dialogs (alert/confirm/beforeunload) are closed by the client that owns the tab; the server closes them only on orphan tabs, after 10 s. Before 28/09 every connected client auto-dismissed every dialog, and the losers died of «No dialog is showing»: the server died 6 times, and every tab of every session with it.
  • «Blocked»? Run node scripts/bc_browser.js status: it prints CDP health and the open tabs. If CDP does not answer, note the tab count and ps of the Chrome processes before stop: that hang is not reproduced yet.

Voices (same contract: one WAV per segment + sidecar .txt; only changed segments regenerate)

Default voice = edge-tts Isabella (2026-09-15). Unless Giovanni names another voice in the request, synthesise with scripts/tts_edge.py (it-IT-IsabellaNeural). Mistral («la mia voce»), ElevenLabs, Diego, or microphone only on explicit request.

VoiceCommandNotes
Default = edge-tts Isabellapython3 scripts/tts_edge.py $PWD/video/narration.json $PWD/video/audiofree, no API key. Override: EDGE_VOICE=it-IT-DiegoNeural (or Elsa / GiuseppeMultilingualNeural). Optional EDGE_RATE=+0%.
Giovanni’s cloned voice = Mistralpython3 scripts/tts.py $PWD/video/narration.json $PWD/video/audiovoice “Me” from the telegram-voice skill env. When Giovanni says «la mia voce» he means THIS.
Own microphonepython3 scripts/voice_record.py … --speaker <name>interactive, one take per sentence, trimmed + normalised, --redo
ElevenLabs libraryELEVEN_VOICE=<id> ELEVEN_LANG=it python3 scripts/tts_eleven.py …free tier: no cloning, ~10k chars quota (a 12-min Italian video ≈ 10k). Italian-verified: Matilda XrExE9yKIg1WjnnlVkGX, Alice Xb7hH8MSUJpSbSDYk0k2, Lily pFZP5JQG7iQjIQuC4Bku, River SAz9YHcvj6GT2YYXdXww; English: Daniel onwK4e9ZLuTAKqWW03F9. Key in <skill>/.env.
Silent placeholderpython3 scripts/silent_wavs.py …mute cut for sync review before the real voice exists

Pronunciation and pauses (all voices). Every TTS script sends spoken.py text, not the raw narration:

  • English and product words are respelled from scripts/pronunce.json (generic: «Business Central» → «Bìsnes Sèntral»), overlaid by <project>/pronunce.json (names, acronyms: "DDT": "di di ti"). The narration keeps the written form. check.py lists the words that look English and are not mapped yet.
  • Edge voices give every in-sentence mark the same ~0.3 s pause. tts_edge.py therefore synthesises each segment in pieces split after . ? ! … ; : — and joins them with spoken.PAUSE: full stop 0.55 s, «…» 0.45, «;» and «:» 0.4, «—» 0.35. Commas stay with the voice (~0.3 s). The engine’s own lead and tail silence is trimmed. Use EDGE_PIECES=0 for one call per segment. assemble.py puts 0.5 s (GAP) between segments, so the punctuation you write is the pacing you hear.
  • Dead footage: when a step’s footage is still longer than its voice at MAX_SPEED, assemble.py shortens the still stretches (page loads, waits), longest first, down to KEEP (0.6 s). The report shows this in the tagliato column.

Switching voice regenerates everything on purpose (the sidecar stamps voice+model); keep the other voice’s WAVs in audio-<voice>/. A second language = a second project folder with its own narration, pages and audio.

Quick reference

NeedDo
Voice ↔ action syncper-step timestamps + per-segment audio, never one stretch factor per scene
Footage shorter than voiceslow ≤ 1.5× then hold; hold > 10 s → move text or add a pause to the step
Footage longer than voicespeed ≤ MAX_SPEED (2.5 Odoo, 1.6 BC) then silence tail; never cut the action
Visible interactioninjected cursor (28 px, Velais Energetic blue, click ripple), glide() ~0.5 s, following highlight() (timed: gone after ms, keep it 1.2–1.6 s; click() clears boxes and arrows), arrow(), zoom()
Odoo sessionJSON-RPC login → host-only cookie (url: not domain:) + auto re-auth on /web/login
BC session / DOMpersistent profile via profileDir; iframe + aria-labelledby + stacked pages → bc_helpers.js; bc-ui.md
Several videos at oncejust start them (one tab each in the shared browser); shared company, additive pre.py (§ Parallel recordings)
Chrome hangs on every sitesystem PAC proxy → --no-proxy-server
Slow menus / lookupsCLICK_TIMEOUT=20000 (record.js default) — 8 s timeouts fail on BC
Sentences/chapters run togetherassemble.py keeps ≥ GAP 0.6 s after every sentence, SCENE_GAP 1.2 s at a scene end; open a chapter with a full clause («Nel pomeriggio, …»), not a one-word «Pomeriggio.»
Keyboard shortcut in the voice"tasti": "Alt+Q" (+ "rate": "-12%") on the segment → keycap badge bottom-right; spoken «Alt e Q»
Length targetcheck.py estimate; 15 min ≈ 2 250 words; phone copy ≈ 1 MB/min

Common mistakes (all seen in real cuts)

  • Uniform stretch per scene → “parla dei reparti mentre è già sulla scheda”. Sync per step.
  • Deep links between chapters → the viewer sees pages jump. Click menus, rows, breadcrumbs.
  • Domain cookie → session dies after ~5 min (the app rotates the sid; a domain cookie coexists and wins).
  • Guessed selectors (docs label, <button> for a styled <a>) → 30 s timeouts inside the clip. Probe first; every step is logged; never catch(() => {}) silently.
  • Highlight drawn once → box on the wrong spot after a scroll, a FastTab opening or a dialog sliding in.
  • Unscoped .last() / bare [role=dialog] (BC) → box or click on the page BEHIND, or on an empty twin dialog.
  • Per-user UI state (BC filter pane, FastTabs) → a toggle that worked in the probe closes it in the scene.
  • Duplicate step names → the montage anchors the sentence to the last occurrence.
  • Sentences longer than the step → frozen frame. A 5 s highlight carries ~12 words.
  • Only the clicks, no reasons → silent stretches between clicks and a viewer who learns buttons, not the process (a BC video: 630 s of silence in 24 min). Fill muto gaps with sourced context: why, what goes wrong, how it was before.
  • Claims without measurement (“misurato sul provino”) → ffprobe, steps.json, frames. Report what failed.
  • One-shot narration → lens reviews found 45 real issues on the first script, 28 on the segmented one, 16 on BC.
  • Re-recording without resetting state → documents already shipped/posted, duplicate vendor invoice numbers, empty orders in the lists.

Rationalisations to refuse

ExcuseReality
”I’ll sync by adding sleeps that match the audio”Page loads vary 2–40 s; only timestamps + montage hold.
”Deep links are cleaner, I’ll cover them with a card”The client asked to see the clicks.
”The selector is obvious from the label”Approva was Valida; Registra was Conferma registrazioni contabili; BC’s «Nuovo» matched a hidden group.
”The probe passed, so the scene will pass”The recorder clicks differently; run the diagnostic mini-scene.
”The narration is fine, it’s just words”Two review rounds rewrote a third of it.
”Re-recording everything is simpler”Re-record failed scenes only; clips, WAVs and encoded segments are cached — but reset the app state first.
”The step logged ok, so it posted”Check the outcome through the API; a dialog on the wrong element logs ok too.