Как перестать переснимать обучающие видео после каждого редизайна

от автора

У нас было несколько обучающих видео по личному кабинету. После большого редизайна большая часть стала неактуальной: кнопки переехали, разделы переименовали, один экран вообще уехал в другое меню.

Чтобы обновить одно видео, надо открыть OBS, поднять чистый профиль браузера — иначе в кадр лезут закладки и всплывашки. Потом записать дубль, в котором ты ни разу не промахнулся мышкой. Потом порезать в редакторе, наложить подписи. На семь видео уходит день. Через два месяца — опять день.

Предсказуемо, видео перестали обновлять.

Тогда я написал штуку, которая собирает ролик из текстового файла. Скрипт открывает сайт в headless‑хроме, водит нарисованным курсором, кликает, печатает, зумит куда надо, показывает подписи, читает их вслух синтезированным голосом, подмешивает звуки мыши и клавиш поверх музыки и выдаёт mp4.

Сценарий выглядит так:

{  "meta": { "url": "https://ru.wikipedia.org", "voice": { "name": "ru-RU-SvetlanaNeural" } },  "steps": [    { "type": "title",   "text": "Как найти статью" },    { "type": "caption", "text": "Впишите запрос", "read": 2500 },    { "type": "click",   "target": "input[name=\"search\"]" },    { "type": "type",    "target": "input[name=\"search\"]", "text": "Кот" },    { "type": "key",     "key": "Enter" },    { "type": "zoom",    "target": "h1", "scale": 1.4 }  ]}

npm run record scenarios/my.json — и на выходе видео. Изменился интерфейс — запустил ещё раз. Дубля нет, монтажа нет.

demo

demo

Гифка выше сделана самим инструментом из сценария, который лежит в репозитории.

Что оказалось сложнее, чем я думал

Playwright умеет писать видео сам, но плохо

У него есть recordVideo, и первую версию я собрал на нём. Выглядело дёшево. Он упирается примерно в 25 fps и на длинных записях начинает терять кадры. Плавное движение курсора превращается в дёрганье.

Пришлось брать кадры напрямую через CDP — Page.startScreencast. Он отдаёт каждый кадр композитора с точным таймстампом, до 60 fps. Кадры складываются jpeg‑ами во временную папку, потом собирается ffconcat‑список, где у каждого кадра своя длительность, а ffmpeg фильтром fps выравнивает это в ровный поток.

Отдельная засада: даже в headless нужна GPU‑растеризация. Без флагов --enable-gpu --enable-gpu-rasterization растр на 1080p проседает до ~40 кадров в секунду там, где анимация полноэкранная — на зуме и титрах. Где GPU нет, флаги просто игнорируются, так что вреда от них тоже нет.

Курсор и зум — это DOM, а не композитинг

Курсор, камера зума, подписи, титульные карточки нарисованы обычными элементами и инжектятся скриптом прямо в страницу. Поэтому нет отдельного слоя, который надо накладывать на видео, и нет зависимости от оконного менеджера: что в странице, то и в кадре.

Зачем это в CI

Собственно, ради этого всё и затевалось. Если запись — одна команда, её можно повесить на тег релиза, и видео физически не может отстать от интерфейса. Не «мы стараемся обновлять», а не может.

Для этого нужен вход без человека, то есть токен в localStorage из секрета репозитория. Папку voice/ имеет смысл закэшировать, чтобы не синтезировать одно и то же на каждом прогоне. И заложить время: в раннере GPU нет, растр софтверный, запись идёт заметно медленнее, чем на ноутбуке.

Чего оно не умеет

Синтез речи ходит в ту же неофициальную ручку, что и «Прочитать вслух» в браузере Edge. Бесплатно, без ключей и регистрации, но неофициально. Есть ретраи, есть кэш, но риск честный.

Селекторы ломаются ровно так же, как в e2e‑тестах. Переименовали класс — сценарий надо чинить. Это цена того, что демка живёт в репозитории рядом с кодом, и меня она устраивает.

Монтажа нет. Что записалось, то и в ролике: склеить два дубля или вырезать неудачный кусок нельзя.

И drag‑n-drop, канвас и хитрые ховер‑меню — не поддерживаются. Инструмент для линейных туториалов «нажми сюда, впиши это, посмотри результат».

Три рантайм‑зависимости (playwright, ffmpeg‑static, node‑edge‑tts), около тысячи строк, MIT. Работает на macOS, Windows и Linux.

Гитхаб: https://github.com/harlamenko/silky‑recorder

ссылка на оригинал статьи https://habr.com/ru/articles/1066964/