П/ВИН

Video Studio: рендерим Reels кодом на Remotion и HyperFrames

·7 мин чтения

Если ты думаешь, что видео — это After Effects, Premiere и ручная работа на несколько часов, то этот пост перевернёт картину. Мы построили video-studio — песочницу, в которой видео пишется как код, рендерится командой в терминале и деплоится на домен с HTTPS и basic auth. Два независимых движка, один сервер, ноль ручного монтажа.

Давай расскажу, что умеем.

Что такое video-studio и зачем два движка

Проект живёт в /home/deploy/projects/video-studio и состоит из двух принципиально разных подходов к программному видео.

Remotion — это React-видео. Буквально: пишешь компоненты на TypeScript, описываешь анимации через хуки, а Remotion рендерит их в mp4 через headless Chrome. Стек: Remotion 4.0.515 + React 19 + Tailwind v4 + TypeScript. Есть визуальная студия с таймлайном, hot-reload и кнопкой Render прямо в браузере.

HyperFrames — HTML-composition движок от HeyGen. Другая философия: описываешь слайды-фреймы в декларативном формате, движок собирает их в видео. Есть свой CLI с командами lint, check, render и встроенной проверкой контраста.

Почему оба? Потому что задачи разные. Remotion даёт полный контроль через React-компоненты — идеально для сложных анимаций и кастомных сцен. HyperFrames быстрее в прототипировании слайдовых форматов. Мы хотели понять оба инструмента изнутри, поэтому сделали benchmark: один и тот же Reels-ролик на каждом движке.

Remotion: React-компоненты как кадры

Начали с Remotion. Задача — собрать вертикальный Reels (1080×1920, 30fps) с TikTok-стайл субтитрами, анимированными оверлеями и несколькими сценами.

Первый шаг — транскрипция аудио. Вытащили речь из исходника, получили 91 слово за 36.288 секунды. На основе транскрипта сгенерировали captions.json с таймингами для каждого слова.

Структура проекта в bench/remotion-agent/:

src/
  Root.tsx          — регистрация композиций
  ViralReels.tsx    — главная композиция
  CaptionOverlay.tsx — субтитры с подсветкой
  scenes/           — компоненты сцен

Главная композиция ViralReels — это набор <Sequence> блоков, каждый из которых монтирует свою сцену в нужный временной диапазон:

// ViralReels.tsx (упрощённо)
export const ViralReels: React.FC = () => {
  const { durationInFrames } = useVideoConfig();
 
  return (
    <AbsoluteFill style={{ background: '#0f0f0f' }}>
      <Audio src={staticFile('voice.mp3')} />
 
      <Sequence from={0} durationInFrames={90}>
        <HookScene />
      </Sequence>
 
      <Sequence from={90} durationInFrames={180}>
        <BeforeAfterScene />
      </Sequence>
 
      <Sequence from={270} durationInFrames={durationInFrames - 270}>
        <CtaScene />
      </Sequence>
 
      <CaptionOverlay captions={captions} />
    </AbsoluteFill>
  );
};

Субтитры — отдельный компонент поверх всего. Логика: берём текущий кадр через useCurrentFrame(), находим активное слово по таймингу, подсвечиваем его акцентным цветом. Первая версия имела баг — между страницами субтитров были пустые промежутки, где текст исчезал. Починили, растянув каждую страницу до начала следующей:

// CaptionOverlay.tsx — фикс пустых промежутков
const pages = captions.map((page, i) => ({
  ...page,
  endTime: captions[i + 1]?.startTime ?? page.endTime,
}));

После TypeScript-проверки (tsc --noEmit — 0 ошибок) и ESLint (0 warnings) запустили рендер:

npx remotion render ViralReels \
  /home/deploy/projects/video-studio/deliver/bench-remotion-agent.mp4

Результат: 1089 кадров, H264+AAC, 1080×1920, 30fps. Проверка аудио показала mean −20.1 dB, max −3.8 dB — не тишина. Три контрольных кадра (начало, середина, конец) — разные, значит картинка движется. Повторная транскрипция рендера совпала с исходником по порядку и таймингу.

HyperFrames: декларативные фреймы

Параллельно собрали тот же Reels на HyperFrames в bench/hf-agent/.

Рабочий процесс здесь другой. Сначала читаешь skill-документацию (skill://hyperframes), потом описываешь композицию в декларативном формате — фреймы, слои, тайминги. CLI сам проверяет структуру.

Первый прогон hyperframes lint дал 0 ошибок и 3 предупреждения о плотности контента — это было намеренно, оставили. Затем hyperframes check — 12 проверок контраста, 2 упали: бейджи на светлом фоне не проходили WCAG AA.

Фикс простой — добавили непрозрачный тёмный фон под бейджи:

// до
{ "background": "rgba(0,0,0,0.4)", "color": "#ffffff" }
 
// после
{ "background": "#1a1a1a", "color": "#ffffff" }

После фикса hyperframes check прошёл с 0 ошибок по всем 12 контрастным проверкам. Рендер:

hyperframes render \
  --output /home/deploy/projects/video-studio/deliver/bench-hyperframes-agent.mp4

Аудио и движение подтверждены аналогично Remotion-версии.

Деплой: две студии на одном домене

Оба движка имеют веб-UI. Remotion Studio — визуальный редактор с таймлайном, деревом слоёв и кнопкой Render. HyperFrames — своя студия с превью фреймов.

Задача: выставить обе на video.pashavin.ru/remotion и video.pashavin.ru/hyperframes с авторизацией.

Проблема с path-хостингом

Первая попытка — разместить студии в подпапках одного домена — провалилась сразу. Оба приложения тянут ассеты от корня (/api, /static, etc.), и оба используют /api — прямая коллизия. Remotion ещё и занимает root-пути под имена композиций (/HelloWorld, /OnlyLogo). Переписывать их JS ради path-prefix — не вариант.

Решение: каждая студия получает свой поддомен. Но сначала разобрались с инфраструктурой.

Схема сети

DNS *.pashavin.ru указывает на 45.91.55.56 — это RU-бридж (SNI passthrough, обход DPI). Бридж прозрачно проксирует трафик на Hetzner-сервер 135.181.115.234, где живёт Caddy. Фаервол снаружи открывает только 80/443 — значит Caddy единственная дверь, basic_auth не обойти по прямому порту.

DNS и systemd

Создали DNS-запись video.pashavin.ru через Vercel API (там управляется зона). Пока TTL расходился — оформили студии как systemd-сервисы:

# /etc/systemd/system/remotion-studio.service
[Unit]
Description=Remotion Studio
After=network.target
 
[Service]
Type=simple
WorkingDirectory=/home/deploy/projects/video-studio/remotion
ExecStart=/usr/bin/npm run dev -- --port 3457
Restart=always
User=deploy
 
[Install]
WantedBy=multi-user.target

Аналогично для HyperFrames на порту 3458. Оба сервиса подняты и живут без активной сессии.

Caddy: конфиг без правок главного файла

Главный Caddyfile содержит import sites/*.caddy — добавили отдельный файл, не трогая основной конфиг:

# /etc/caddy/sites/video-studio.caddy
video.pashavin.ru {
  basicauth /* {
    # [СКРЫТО] — хеш пароля
  }
 
  handle /remotion* {
    reverse_proxy localhost:3457
  }
 
  handle /hyperframes* {
    reverse_proxy localhost:3458
  }
}

Валидация: caddy validate --config /etc/caddy/Caddyfile — OK.

Инцидент с reload

Первый systemctl reload caddy упал: Caddy не смог создать лог-файл из-за прав на директорию. Systemd залип в состоянии reloading (Type=notify так и не получил READY) — все последующие reload отваливались по таймауту.

Сам Caddy при этом жил и обслуживал трафик. Применили конфиг без даунтайма через admin API:

curl -X POST http://localhost:2019/load \
  -H 'Content-Type: text/caddyfile' \
  --data-binary @/etc/caddy/Caddyfile

После этого починили права на лог-директорию и восстановили нормальную работу systemctl reload.

Сертификаты и финальная проверка

Caddy автоматически выпустил TLS-сертификаты через Let's Encrypt. Проверка через публичный домен показала 404 от Vercel — оказалось, локальный резолвер закешировал старый wildcard-ответ. После истечения TTL (660с) всё заработало корректно.

Оба UI доступны по HTTPS с basic auth:

  • https://video.pashavin.ru/remotion → Remotion Studio
  • https://video.pashavin.ru/hyperframes → HyperFrames Studio

Результат

Что в итоге работает:

  • Два рендер-движка в одном репозитории — Remotion (React) и HyperFrames (декларативный)
  • Два готовых Reels в deliver/ — один на каждом движке, оба с аудио, субтитрами и анимациями
  • Два веб-UI на продакшн-домене с HTTPS и авторизацией
  • systemd-сервисы — студии живут после перезагрузки сервера
  • Caddy без даунтайма — конфиг применяется через admin API

Время рендера Remotion-ролика (1089 кадров, 36 секунд) — в пределах минуты на сервере. HyperFrames сопоставимо.

Выводы и уроки

Главный урок — программное видео реально работает в продакшне. Не как игрушка, не как демо, а как инструмент, который можно автоматизировать, версионировать в git и деплоить как любой другой сервис. Это меняет подход к контент-производству: вместо «открыть Premiere, потыкать мышкой» — git commit, render, готово.

Второй урок — два движка лучше одного, если понимаешь зачем. Remotion даёт максимальный контроль через React: хочешь кастомную физику, сложные переходы, данные из API прямо в кадре — пожалуйста. HyperFrames быстрее для слайдовых форматов с готовыми шаблонами. Мы не выбирали «лучший» — мы поняли, когда использовать каждый.

Третий урок — path-хостинг dev-студий не работает без боли. Оба приложения проектировались для корня домена: /api, статика, роутинг — всё от /. Попытка засунуть их в подпапку требует либо переписывания JS, либо реверс-прокси с rewrite-магией. Проще дать каждому свой поддомен или путь через отдельный vhost. Мы выбрали второй вариант — и не пожалели.

Четвёртый урок — Caddy admin API спасает когда systemd reload ломается. Классическая ситуация: сервис завис в промежуточном состоянии, systemctl reload не работает, но сам Caddy живой. curl -X POST localhost:2019/load применяет конфиг мгновенно без перезапуска и без даунтайма. Держи этот трюк в голове.

Пятый — проверяй контраст автоматически. HyperFrames встроил WCAG-проверку в CLI, и это поймало два реальных бага с бейджами до рендера. В Remotion такого из коробки нет — но можно добавить через axe-core или ручную проверку палитры. Видео смотрят на разных экранах, и плохой контраст убивает восприятие.

Дальше планируем: автоматический рендер по webhook (пришёл скрипт → отрендерилось → загрузилось), шаблоны для разных форматов (Shorts, Stories, горизонталь), и возможно — генерацию скриптов через LLM прямо в пайплайне. Но это уже следующая глава.

Ссылки

Паша Вин
Паша Вин

AI-инженер, предприниматель, маркетолог. Основатель feberra.com и x10seo.ru. 13 лет в перфоманс-маркетинге, 3 года в системной интеграции AI в бизнес.