Video Onboarding for a B2B Platform: A Reproducible Pipeline That Builds Training Videos from Code
Training video built with the same discipline as a software release: when the UI changes, the pipeline rebuilds the video.
Context
The B2B agricultural marketplace we designed and deployed for a nationwide supplier of seeds and crop protection products entered an active user-acquisition phase. The first barrier at the door is registration: a full account with role selection, company details, tax ID, phone, and email. For a product team this is a trivial form. For the target audience — farms and traders who make decisions over the phone and in messengers and do not live in web interfaces — it is the point where a significant share of inbound traffic can be lost.
A written manual does not solve this: nobody reads it. A support call does, but it does not scale — every lead who needs to be talked through "click the green button in the top right" costs the team time. The market's standard answer is a training video. The standard way to produce one is to record the screen by hand, send it to an editor, and hire a narrator. Such a video is expensive to make and, more importantly, goes stale instantly: the product is under active development, the interface changes, and a month later the video shows a form that no longer exists.
We treated this as an engineering problem, not a production shoot. Since we built the platform and control its markup and API, a training video can be assembled like a build artifact — from a script, the live product, and code — rather than filmed. A UI change then means re-running the pipeline, not reshooting.
The task
The deliverable was a "how to register" training video: a complete walkthrough from role selection to a finished account and landing in the catalog, with Russian voice-over, captions, and visual step-by-step guidance. Format: 1920×1080, H.264, compact enough to distribute through messengers and embed on the site without friction.
A core requirement was to film the live product, not a mockup. The video had to show the same form, the same validations, and the same outcome a real user sees — including the tax-ID checksum validation, the mandatory consent, and the redirect to the catalog after successful registration. That ruled out a staged recording on a mock and demanded careful handling of the production database: the demo registration goes through for real, via Supabase Auth, and must leave no trace in production after filming.
The second part of the task followed immediately: a user who has just registered needs to be shown the next step — how to list a lot for sale or place a purchase request. So the pipeline was designed from day one for a series with role parameterization, not for a single video: the same pipeline had to build instructions for the seller and the buyer, differing in script and in the entry point into the account area.
Approach
We dropped manual screen recording and the video editor entirely. The whole video is described by data and code: narration texts live in a JSON script, the voice is synthesized, the screen is captured by a programmatically driven browser, graphics are generated from HTML, and a script performs the edit on top of ffmpeg. The project repository contains no editing-suite source files — only the script, the pipeline code, and the output mp4.
The registration script is broken into six steps: role selection, the sign-up button, company and full name, tax ID with phone and email, password with consent, and the finale with the redirect to the catalog — plus full-frame intro and outro. The key editing decision: the duration of each step is not set by hand but computed from the actual length of the narration line plus a fixed margin. Change the script text and the timing of the entire video is recalculated automatically — voice and picture cannot drift out of sync by construction.
Filming runs against live production, so data hygiene is built into the pipeline. Each run registers a unique demo account with a disposable email on a service domain; the tax ID is checksum-valid so the form behaves normally and highlights the field green. After the final render, the demo accounts are deleted from the auth tables and profiles — the production database stays clean.
Visual guidance is handled by two layers on top of the screencast: a synthetic cursor and highlighting of active fields, injected into the page itself at capture time, and animated step cards with an "N of 6" badge rendered as separate transparent PNGs composited at edit time with a smooth fade-in. The cards are laid out in HTML in the platform's brand palette (lime on dark olive, Manrope) and rendered by the same Chrome — the video's graphics are guaranteed to match the product's style.
Pipeline architecture
The pipeline consists of five stages with clear contracts between them. First, voice-over: a script reads the JSON scenario and synthesizes the lines via edge-tts with the ru-RU-SvetlanaNeural voice at a 4% slower rate, producing mp3 files and a JSON of actual durations. That durations file becomes the single source of truth for all downstream timing.
Second, capture. A puppeteer-core script drives the system Chrome in headless mode and records the screen via page.screencast, segment by segment: recording starts and stops at the boundaries of each script step, yielding one webm clip per step. Inside the run, a real registration takes place: the script fills the form by selectors, passes the validations, and carries the flow through to the catalog redirect.
Third, normalization. Chrome's screencast produces webm with a variable frame rate and sparse keyframes: ffprobe cannot determine the duration of such a file, and -ss seeking lands off the target frame. Each clip is re-encoded into mp4 at a fixed 30 fps — after which the footage becomes predictable enough for frame-accurate editing.
Fourth, graphics: a generator lays out the intro, outro, and six step cards in HTML and renders them to transparent PNGs via Chrome with omitBackground. Fifth, assembly: a Python assembler assigns each step a duration of "narration plus 1.4 seconds", stretches short clips via setpts, builds the final segment as a special xfade join of two fragments (the form-submit moment and the already-loaded catalog), overlays the cards with an alpha fade, lays the voice-over under it with a 500 ms delay, concatenates the segments, and performs a final re-encode with a short keyframe interval so that in-player seeking stays smooth. The output is a 1920×1080 master of about 94 seconds at roughly 13 MB, plus a poster frame.
Scaling to a series
The architecture's real test was the second run. Right after registration, two account-area walkthroughs were needed: "how to list a lot for sale" for the seller and "how to place a purchase request" for the buyer. Both videos were built by the same pipeline: the role is passed as an environment parameter, the script is five steps plus intro and outro, and the runtimes came out at about 69 and 65 seconds respectively.
Filming the account area required one addition: sign-in happens off-camera, via a direct call to the platform's auth API before recording starts, after which the browser opens straight into the account area. The viewer sees only the substantive part, with no password entry on screen. The scripts diverge by role: the seller goes through the offer-creation modal with crop, price, volume, and mandatory quality metrics — the pipeline fills them all, otherwise the product blocks submission; the buyer fills the request form with a target price and a location cascade. In both cases the product's normal reactions make it into the frame: the moderation-submitted toast and the new record appearing in the list.
The series assembler gained a second duration-alignment mode: instead of slowing a clip via setpts, it freeze-pads the last frame when the narration runs longer than the on-screen action. As in the first video, the demo seller and buyer accounts and their test lots were removed from the production database after filming.
Outcome
Delivered: a series of three training videos — registration (~94 s), seller lot listing (~69 s), buyer purchase request (~65 s) — all 1920×1080, H.264, with Russian voice-over, step cards, and poster frames. The main video weighs about 13 MB and travels through messengers without recompression. Every video shows the live product with real validations, not a staged mock.
The client's main asset is not the files but the pipeline. The cost of the next video is the cost of a script: narration texts in JSON and an action sequence in the capture script. When the UI changes, the video is rebuilt by re-running the pipeline — no studio, no narrator, no editor. The second and third videos proved this in practice: scaling required role parameterization and one new assembler mode, not a second production from scratch.
The funnel impact — fewer registration support tickets and higher form-completion conversion — is designed to be measured, not asserted: the platform logs both registrations and support tickets, so a before/after comparison runs on standard instrumentation once publication statistics accumulate, with no retroactive surveys.
What we built
Script as data
Per-step narration texts live in JSON; the voice-over is synthesized from them and each segment's duration is computed from them. Editing the script means editing one file, with timing recalculated automatically.
Voice-over synthesis
edge-tts, ru-RU-SvetlanaNeural voice at a 4% slower rate; output is per-line mp3 files plus a JSON of actual durations — the source of truth for the edit.
Segment-by-segment capture of the live product
puppeteer-core drives the system Chrome in headless mode; page.screencast starts and stops at each step boundary. On screen: a real registration with validations and the catalog redirect.
VFR screencast normalization
Every webm is re-encoded to mp4 at a fixed 30 fps — otherwise ffprobe cannot determine duration and -ss seeking misses the target frame.
HTML overlay generator
Intro, outro, and step cards with an "N of 6" badge are laid out in HTML in the brand palette (lime on olive, Manrope) and rendered to transparent PNGs via Chrome with omitBackground.
Programmatic ffmpeg assembler
Step duration = narration + 1.4 s; alignment via setpts or tpad freeze-padding, a special segment via xfade, cards with alpha fade, voice-over delayed by 500 ms, and a final re-encode tuned for smooth seeking.
Role parameterization of the series
The role (seller/buyer) is passed as an environment parameter; account sign-in happens off-camera via the auth API before recording starts. One pipeline builds three different videos.
Production data hygiene
Demo accounts with disposable emails and a checksum-valid tax ID; after rendering, the accounts and test lots are deleted from the production auth tables and profiles.
Engineering challenges
Chrome's screencast is unusable for editing as-is
page.screencast outputs webm with a variable frame rate and sparse keyframes: ffprobe returns duration=N/A and -ss seeking lands off the target frame. The fix: mandatory normalization of every clip to mp4 at a fixed 30 fps, and frame checks only with accurate seek (-ss after -i).
Filming on the production database without leaving a trace
The video shows a real registration through Supabase Auth. The pipeline generates a unique demo email per run and uses a checksum-valid tax ID so the validations behave normally; after filming, demo accounts and test lots are deleted from production.
Voice-to-picture sync without a timeline
There is no manual alignment: each segment's duration is computed from the actual narration length plus a fixed margin. When the action is shorter than the line, the clip is stretched via setpts or frozen on the last frame via tpad — desync is ruled out architecturally.
CLI traps in voice-over synthesis
Lines starting with a hyphen broke edge-tts argument parsing — parameters had to be passed strictly as --rate=/--text=. A separate ffmpeg trap: the option is -video_track_timescale, not -timebase. These details are captured in the pipeline so they do not recur.