Я задался вопросом: как должен выглядеть видеоредактор, если проектировать его в эпоху ИИ-агентов — не «редактор плюс кнопка с нейросетью», а инструмент, где агент работает на том же таймлайне, теми же операциями, что и человек? Ответа не нашлось, и я начал писать свой.
Так появился Kadr — открытый (GPL-3.0) многодорожечный видеоредактор на Electron + React + TypeScript: GPU-композитинг на WebGL2, WYSIWYG-экспорт, локальное распознавание речи, программируемая моушн-графика на React — и настоящая интерактивная сессия Claude Code в панели рядом с превью, подключённая к живому проекту через MCP.
В статье — что редактор умеет и какие технические решения под этим лежат: почему пришлось выбросить аппаратный кодировщик Chromium, как кадры доезжают до ffmpeg без единого копирования, зачем нужен ping-pong из FBO для эффектов и как убедить Whisper не сочинять субтитры из тишины.
Зачем ещё один видеоредактор
Мне нужен был монтаж для YouTube-канала, а из инструментов на Linux ничего не подходило: где-то нет GPU-эффектов, где-то экспорт живёт своей жизнью и не совпадает с превью, и нигде агент не может посмотреть на проект и что-то в нём сделать. А мне хотелось ровно этого: сказать «добавь сюда анимированные субтитры» — и смотреть, как они появляются в превью.
Отсюда два принципа, из которых выросла вся архитектура:
-
Превью и есть рендер. Один и тот же WebGL2-композитор рисует и предпросмотр, и офлайн-кадры экспорта. Что видишь — то и получаешь, пиксель в пиксель.
-
Всё, что может сделать человек мышью, агент может сделать через API. Каждая операция редактора доступна из
window.kadrEditor, и каждая правка агента попадает в общую историю отмен.
Что умеет редактор
Коротко, чтобы дальше говорить о технике (полный разбор — в FEATURES.md, демо — в видео):
-
многодорожечный монтаж: трим, луп, фейды, связанные AV-клипы, скорость ×0.02–×100 с прилипанием, реверс клипа, полная история отмен;
-
ключевые кадры на всём — позиция, масштаб, поворот, прозрачность, громкость, маски — с изингами в стиле After Effects;
-
настоящее 3D с перспективно-корректными текстурами, «полёт камеры» целой дорожкой;
-
маски до 8 фигур с растушёвкой, эффекты (дымное сияние, размытие), 26 GPU-переходов;
-
локальный faster-whisper с пословными таймкодами, авто-субтитры с караоке-подсветкой;
-
Remotion-фрагменты: моушн-графика на React/TSX как обычные клипы;
-
импорт отовсюду: drag-and-drop в любое место окна, картинки прямо из браузера, Ctrl+V из буфера, XDG-порталы;
-
экспорт: ffmpeg x264 с честным битрейтом, motion blur, frame blending, пресеты YouTube/Shorts/WebM/MP3.
И — встроенный Claude Code, о котором ниже.
GPU-композитор: превью == рендер
Сердце редактора — квадовый композитор на WebGL2 (src/gl/compositor.ts). Каждый клип — текстурированный квад: перспективная проекция для 3D-наклонов, до 8 фигурных масок с растушёвкой в шейдере, кэш текстур по ключу источника. Функция drawFrame — чистая: ей отдают список слоёв на момент времени t, она рисует кадр. Превью вызывает её из rAF-цикла, экспорт — из офлайн-цикла с точным степпингом (k+0.5)/fps. Разойтись им негде — это один и тот же код.
Эффекты устроены как цепочка FBO. Слой рендерится в полноразмерный fx-буфер; гауссово размытие проходит горизонталь→вертикаль ping-pong’ом; для сияния альфа-канал слоя спирально размывается в поле четвертного разрешения, а композитный шейдер строит из него дымный ореол: fbm-шум рвёт край на «щупальца», дым дрейфует наружу вдоль градиента поля, круглые мерцающие искры даёт клеточный хэш. Часы эффекта привязаны к локальному времени клипа, поэтому в превью и в экспорте картинка идентична вплоть до положения искр.
Переходы — в стиле Vegas: просто перекройте два клипа на одной дорожке, длительность перехода равна перекрытию. A и B рендерятся в два FBO и смешиваются шейдером из реестра gl-transitions. Отдельный класс — «торцевые» переходы (вип-паны, зум-блюры, RGB-расхождение, глитч): это эффект одного клипа с пиком интенсивности в точке склейки, как в BorisFX.
Экспорт: как Chromium игнорировал битрейт и что с этим делать
Самая поучительная часть проекта. Изначально видео кодировалось через WebCodecs — красивый современный API. Но на чистом статичном 1080p Chromium-энкодер (программный OpenH264) стабильно выдавал ~0,7 Мбит/с независимо от запрошенного битрейта. Запрашиваешь 12 — получаешь 0,7 и «мыло». Это не баг конфигурации: параметр битрейта просто игнорируется.
Я измерил честно: SSIM относительно исходника — 0,9963 у WebCodecs против 0,9996 у ffmpeg x264 на той же скорости (±15%). Вывод очевиден, но возникла проблема транспорта: как отдавать сырые кадры из рендерера в ffmpeg? Кадр 1080p RGBA — это ~8 МБ, при 60 fps — полгигабайта в секунду.
Оказалось, что любой штатный канал Electron — IPC, contextBridge, даже WebSocket на localhost — копирует буфер и упирается в 350–850 МБ/с, утраивая время экспорта. Решение: contextIsolation: false (осознанный компромисс — приложение не грузит произвольный веб), и тогда preload-скрипт живёт в одном V8-контексте с рендерером. Preload сам спавнит ffmpeg (rawvideo rgba на stdin, vflip + out_color_matrix=bt709, libx264 с VBV под битрейт пресета), а буферы readPixels передаются по ссылке, без сериализации. Двойная буферизация: слот переиспользуется только после того, как stdin ffmpeg принял кадр.
Декодирование на экспорте тоже своё: mp4box демуксит контейнер, WebCodecs VideoDecoder декодирует последовательно с упреждающим чтением — выходит ~8× быстрее, чем гонять <video>-элемент перемотками. Учтён edit list (elst) со сдвигом cts; потоки без цветовых тегов перезаворачиваются с эвристикой HD→bt709, SD→bt601. Любой сбой — нет moov в первых 16 МБ, неподдерживаемый кодек — и клип прозрачно откатывается на элементные перемотки.
Поверх этого два «кинематографических» флажка, оба включены по умолчанию:
-
Motion blur: 180° затвор, 8 суб-кадровых композитов, усреднённых на GPU. Декод не трогается — суб-сэмплы переиспользуют кадры источника, движутся только трансформации, маски и переходы.
-
Frame blending: если источник не заполняет fps проекта (25-fps клип в 60-fps проекте, замедление), следующий кадр источника подмешивается с весом по фазе времени сэмпла. Источники с достаточным fps не трогаются — их экспорт остаётся бит-в-бит.
Аудио собирает системный ffmpeg одним фильтр-графом: на каждый сегмент volume,atempo*,afade,adelay,apad,atrim, затем amix с точной компенсацией уровня (все потоки паддятся до полной длительности, чтобы масштаб amix был константой и сокращался обратным volume). Ускорение любой кратности — цепочка atempo (каждый чейн держит 0,5–2×, дальше перемножаем).
Кстати, о скорости клипов: превью честно ограничено — Chromium бросает исключение при playbackRate вне жёсткого диапазона [0.0625, 16], поэтому плеер клампит значение и добирает точность ресинками, а вот экспорт точен на любой скорости. Мелочь, которую не найдёшь, пока ×50 не уронит весь rAF-цикл.
Встроенный Claude Code
Кнопка 🤖 открывает панель с xterm.js, в которой главный процесс спавнит обычный CLI claude пользователя в PTY (node-pty): та же авторизация, тот же интерактивный TUI, только внутри редактора. Никакого «обёрнутого API» — это полноценный Claude Code со всеми его инструментами.
Связь с проектом — через MCP. При старте сессии редактор поднимает HTTP-мост на 127.0.0.1 (POST /eval → webContents.executeJavaScript) и генерирует --mcp-config со stdio-сервером, который даёт агенту пять инструментов:
|
Инструмент |
Что делает |
|---|---|
|
|
весь живой проект: дорожки, клипы, пути медиа, транскрипты |
|
|
выполнить JS против API редактора; каждая правка — в истории отмен |
|
|
отрендерить проект или диапазон и дождаться файла |
|
|
локальный Whisper по файлу или диапазону таймлайна |
|
|
создать Remotion-композицию как клип таймлайна |
На скриншоте — реальная сессия: я спросил, что сейчас на таймлайне, агент вызвал kadr_state, увидел, что стейт большой, сам догадался прогнать его через jq и выдал сводку — попутно заметив, что у меня две дорожки называются A7, и предложив навести порядок:
Самое неожиданное в этой интеграции — не MCP, а гигиена процессов. Дерево, порождённое из Electron, наследует его файловые дескрипторы, включая слушающий сокет отладочного порта Chromium. Если сессия claude переживёт жёсткую смерть редактора, следующий запуск не сможет забиндить порт — и виноватого не найти. Поэтому claude запускается exec’ом внутри bash-обёртки со сторожем, убивающим свою группу процессов, если Electron умер; закрытие панели шлёт группе SIGHUP с эскалацией до SIGKILL; при старте приложение выметает осиротевшие группы по сигнатурам в cmdline; а window-all-closed форс-экситит процесс через 2,5 секунды, чтобы зависший экспорт не оставил безоконного зомби.
Remotion-фрагменты: моушн-графика без рендеров
Программируемая графика — клипы типа remotion: React/TSX-композиции в общем workspace с vite dev-сервером. В превью фрагмент живёт как iframe с @remotion/player поверх GL-канвы, синхронизированный с часами редактора через postMessage — причём рассинхрон корректируется подталкиванием playbackRate (±8%), а не перемотками: жёсткие seek’и заставляли движущийся текст видимо заикаться.
Главный цикл работы с агентом: Claude создаёт фрагмент через kadr_fragment_create, правит его TSX обычными файловыми инструментами — vite горячо перезагружает превью за ~2 секунды. Ни одного рендера во время итераций. Рендер ровно один — при экспорте: remotion render на хэш содержимого (кэшируется), в почти-лосслесс качестве, потому что этот интермедиат ещё пройдёт финальный энкод. Тут тоже пришлось мерить: Remotion по умолчанию гонит кадры через JPEG q80, а vp8 «мылил» текст субтитров — PSNR текстовой области 28,0 дБ против 32,6 у vp9 с alpha при --crf=12.
Если на фрагмент навесить GL-фичи — маски, 3D, переходы, сияние — он автоматически переключается в режим пиксельного захвата: скрытое offscreen-окно рендерит ту же композицию, его BGRA-кадры стримятся в композитор как текстура, и фрагмент становится обычным видеослоем.
На фрагментах построены и авто-субтитры: транскрипция диапазона → пословные таймкоды → генерация прозрачного caption-фрагмента с караоке-подсветкой и анимациями появления. Сгенерированный TSX остаётся редактируемым — руками или агентом.
Локальный Whisper и борьба с галлюцинациями
Распознавание речи — faster-whisper (large-v3, int8), полностью локально, ~2× реального времени на CPU. Аудио для транскрипции готовит тот же экспортный микшер — агент слышит ровно то, что услышит зритель (WYSIWYG и здесь).
Whisper знаменит тем, что в тишине и шуме сочиняет текст. Против этого — VAD, condition_on_previous_text=False, пороги и пост-фильтры, а контракт закреплён тестом: синусоида 440 Гц обязана дать ноль реплик. Пословные таймкоды позволяют перекраивать реплики в группы по N слов без потери точности: балансировка групп (7 слов → 3+2+2, без «сирот»), разрывы по концам предложений и паузам >0,6 с, склейка продолжений («мини» + «-программами»).
Тесты, которые водят настоящее приложение
Юнит-тесты для видеоредактора почти бесполезны: всё интересное происходит на стыке GL, медиа-элементов, IPC и ffmpeg. Поэтому e2e-сьюты (их за тридцать) подключаются к работающему приложению по Chrome DevTools Protocol и работают как пользователь: двигают клипы реальными событиями мыши, роняют файлы через Input.dispatchDragEvent, проверяют PSNR экспорта против эталона, каденс frame blending по подсчёту дубликатов кадров и тот самый антигаллюцинационный контракт транскрипции.
Из CDP-специфики: awaitPromise ненадёжен под GC — асинхронные результаты паркуются в глобалы и опрашиваются; xclip -i форкает демона, наследующего stdio, и execFileSync виснет навсегда, если не редиректнуть вывод. Такие вещи узнаёшь только на живом прогоне.
Что дальше
-
MCP-сервер наружу — чтобы внешние агенты (не только встроенная сессия) управляли редактором;
-
шаблоны сцен и библиотека фрагментов;
-
больше GPU-эффектов.
Проект открытый на Гитхабе, GPL-3.0. А вживую всё показано в видео-демо:
Запуск — три команды (нужны Node ≥ 20 и ffmpeg в PATH):
git clone https://github.com/HelpFreedom/kadr.git && cd kadr
npm install # postinstall пересоберёт node-pty под Electron
npm run dev
Панель 🤖 опциональна — без CLI claude это просто быстрый GPU-редактор. Для распознавания речи — pip install faster-whisper.
Буду рад вопросам про архитектуру, замечаниям и issue. Если у вас есть идеи, каким должен быть монтаж в паре с агентом, — обсудим в комментариях.
Автор: Black_Triangle_Habr


