Внутри HyperFrames: HTML-нативный видеодвижок HeyGen под построчным аудитом
Открытый HyperFrames от HeyGen превращает обычный HTML в детерминированные MP4 и сделан для ИИ-агентов, а не для людей. Мы проиндексировали все 823 426 строк на коммите 89e970f и провели полный статический аудит: как шим виртуального времени превращает Chrome в камеру, почему функция на 3 195 строк впитывает вдвое больше багфиксов, чем весь репозиторий, валидатор вывода LLM, подключённый лишь к одному из четырёх пайплайнов, и двенадцать опубликованных пакетов без лицензионных метаданных.
Ключевые выводы
HyperFrames — фреймворк HeyGen под Apache-2.0, который рендерит обычные HTML-композиции в детерминированный MP4 через headless Chrome и FFmpeg, с двенадцатью перематываемыми анимационными адаптерами, 101 правилом линтера и 39 534 строками агентных инструкций. Полный статический аудит на коммите 89e970f показал отличную тестовую дисциплину (55 283 проверки, ноль тестов без проверок) и ноль циклов зависимостей, а рядом — шесть подтверждённых дефектов: функция рантайма на 3 195 строк с 73% багфиксов среди коммитов, валидатор вывода LLM в одном пайплайне из четырёх, шим детерминизма, чей докстринг преувеличивает покрытие setTimeout, сидирование случайности только на распределённом пути рендера, пять побайтово идентичных вендоренных бандлов и двенадцать npm-пакетов, опубликованных без поля лицензии при том, что CI-страж покрывает один пакет.
10 марта 2026 года HeyGen инициализировал пустой Git-репозиторий. Через сто сорок три дня в нём было 823 426 проиндексированных строк кода, 3 476 коммитов от 57 участников и 352 тегированных релиза — то есть выпущенная версия примерно каждые десять часов, включая выходные. Продукт называется HyperFrames, лицензия Apache-2.0, а весь его тезис умещается в одну строку: пиши HTML, рендери видео, сделано для агентов.
Такая посылка заслуживает проверки. Рендерить видео из браузера не ново — Remotion делает это на React с 2021 года, — но ставка HyperFrames острее. Он исходит из того, что автор композиции вообще не человек. Он исходит из того, что автор — языковая модель, и что языковая модель пишет HTML лучше, чем React. Всё, что идёт дальше по цепочке — линтер, наборы навыков, машинерия детерминизма, — следует из этого допущения.
Мы взяли репозиторий на коммите 89e970f (v0.7.87, 31 июля 2026) и прогнали через полный статический аудит с помощью Code Indexer — движка семантического поиска и анализа кода, который строит запрашиваемый граф кодовой базы вместо чтения файлов по одному. Индекс HyperFrames содержит 69 869 символов и 312 603 ссылки, разрешённые в 96 369 статических рёбер вызовов. Каждое число в этой статье вычислено по этому графу или проверено прямо по исходникам; каждая находка воспроизведена вручную прежде, чем была записана. Там, где инструментарий ошибся, мы об этом говорим — этому посвящён отдельный раздел.
Часть I — Что такое HyperFrames на самом деле
Композиция — это HTML-файл, и больше ничего
Единица работы в HyperFrames — обычный .html-файл. Никакого бандлера, никакого JSX, никакого проприетарного формата таймлайна. Тайминг живёт в data-атрибутах на обычных DOM-элементах: data-start, data-duration, data-track-index. Элемент, участвующий в таймлайне, несёт class="clip". Тег <video> — это видеоклип; тег <audio> — аудиодорожка с data-volume. Откройте файл в Chrome — и он воспроизведётся: без сборки, без dev-сервера.
В этом весь замысел, и у него есть конкретная отдача для агентных сценариев: артефакт, который произвела языковая модель, и есть артефакт, который рендерится. Нет стадии компиляции, где ошибка модели превращается в стектрейс от бандлера, которого она никогда не видела. Кривая композиция — это кривой HTML, а HTML деградирует мягко.
Цена этого выбора в том, что браузер приходится превратить в камеру — а браузер по своему устройству самая недетерминированная среда в мейнстримной разработке.
Как Chrome превращают в детерминированную камеру
Ключевой трюк живёт в packages/producer/src/services/fileServer.ts, в функции buildVirtualTimeShim. Прежде чем запустятся собственные скрипты композиции, локальный файловый сервер внедряет пролог, который подменяет странице всё её представление о времени. Date заменяется конструктором VirtualDate — цепочка прототипов сохранена, но now() возвращает переменную, которой управляет рендерер. performance.now переопределяется на ту же переменную. requestAnimationFrame больше ничего не планирует: колбэки складываются в очередь, которая синхронно осушается по требованию, с передачей виртуальной метки времени.
Дальше рендерер ведёт страницу покадрово, вызывая __HF_VIRTUAL_TIME__.seekToTime(ms). Каждая зависящая от времени строка JavaScript на этой странице видит ровно ms до следующего сика. Кадр 1 847 — это не «как выглядела страница через 61,6 секунды после загрузки», а намеренно сконструированное состояние в момент t = 61 566,67 мс, достижимое с холодного старта в любом порядке.
Распределённый путь идёт дальше. Когда кадры раздаются параллельным воркерам, seedRandomFromFrame включает PRNG Mulberry32, который пересеивается от текущего виртуального времени на каждом сике, а crypto.getRandomValues переписывается так, чтобы черпать из него. Два воркера, рендерящие несмежные кадры одной композиции, наблюдают один и тот же поток случайности на одной и той же метке времени. Для вывода зерна используется мультипликативный хеш Кнута со смещением золотого сечения — именно затем, чтобы нулевой кадр не выродился. Такая деталь говорит, что кто-то действительно смотрел, как первые кадры выходят неправильными.
Двенадцать адаптеров и контракт на 32 строки
Заморозка времени помогает только если анимационная библиотека согласна быть перематываемой. HyperFrames решает это кадровыми адаптерами — тонкими прослойками в packages/core/src/runtime/adapters/, которые переводят «композиция сейчас в моменте T» на язык конкретного рантайма. Их двенадцать: GSAP, CSS-анимации, WAAPI, Anime.js, Lottie, Three.js, TypeGPU, D3, Leaflet, Mapbox, MapLibre и Google Maps. У каждого рядом лежит собственный .test.ts.
Адаптер GSAP — который сама документация называет основным — занимает 32 строки. Это сильнейший архитектурный сигнал во всём репозитории: контракт перемотки достаточно узкий, чтобы поддержка нового анимационного рантайма стоила вечера, а не квартала.
GPU-адаптерам нужно больше. Three.js и TypeGPU оба шлют общее CustomEvent hf-seek, дедуплицированное по точной метке времени, — чтобы композиция, держащая обе библиотеки, не платила дважды за каждую перемотку. В detail события лежит функция waitUntil() — асинхронный барьер в стиле service worker, позволяющий слушателю сказать рендереру: «не снимай этот кадр, моя загрузка текстуры ещё в полёте». Вызов вне синхронного тела слушателя бросает исключение. Есть и аварийный люк forceDispatchSeekEvent, который повторно шлёт ту же метку времени после внедрения видеокадров, чтобы GPU-композиции перезалили текстуры из картинок, которых мгновение назад не существовало.
Видео, которое никогда не играет
Вот та часть, которую чаще всего делают неправильно при самостоятельных попытках. HyperFrames не воспроизводит элементы <video> во время рендера. И не может: часы воспроизведения медиаэлемента привязаны к настенному времени, а именно настенное время шим и упразднил.
Вместо этого videoFrameExtractor заранее декодирует исходное видео в стоп-кадры и строит таблицу соответствия. Во время захвата videoFrameInjector подменяет каждый <video> нужным кадром для текущего времени композиции. Эти кадры отдаются через LRU-кеш с двумя границами — по числу записей и по суммарным байтам, потому что кадр 4K PNG весит около 33 МБ после base64, и кеш, ограниченный только количеством, с удовольствием съел бы машину.
В этом кеше — один из лучших комментариев в кодовой базе. Если data-URI одного кадра превышает весь байтовый бюджет, запись отвергается, а не вставляется: вставка запустила бы цикл вытеснения, который немедленно выбросил бы только что добавленное, — превратив кеш в горячую точку CPU, вечно перечитывающую и перекодирующую один и тот же кадр. Авторы задокументировали инвариант, режим отказа и точный рефакторинг, который молча сломал бы учёт байтов. Это не та кодовая база, где кто-то угадывал.
Три способа сфотографировать кадр
frameCapture.ts поддерживает три режима захвата и выбирает между ними в рантайме. screenshot — это Page.captureScreenshot из CDP: медленно, корректно, эталон. beginframe управляет HeadlessExperimental.beginFrame, детерминированно продвигая композитор. drawelement — быстрый путь: элемент композиции рисуется прямо на canvas, а пиксели считываются обратно.
Быстрый путь он же и хрупкий, и способ борьбы с этим необычайно откровенен. drawElement иногда возвращает пустой кадр, не бросая исключения, — промах записи отрисовки. Код ловит это статистически: ведётся скользящая медиана размеров последних шестидесяти JPEG, и кадр переснимается скриншотом, если он оказался меньше max(20 КБ, медиана × 0,12). Пустой кадр 1080p весит 5–9 КБ; настоящий — от 50 КБ до 1 МБ. В комментарии указаны два коммита, на которых это проверялось: 462 повреждённых кадра до нуля и 11 до нуля.
Тот же блок фиксирует стратегию, которую попробовали и отвергли: проактивная съёмка скриншотом каждого кадра на границе клипа была включена по умолчанию и оказалась вредной — внедрённый canvas не отрисован в начале рендера, и «подстраховка» заменяла хорошие кадры белыми. Теперь это включается переменной окружения. Такая инженерная археология в публичных репозиториях редкость, и индекс кода находит её мгновенно, а беглый просмотр — никогда.
Интерфейс для агента — это 39 534 строки Markdown
HyperFrames поставляет девятнадцать агентных навыков общим объёмом 39 534 строки Markdown — корпус инструкций размером со средних размеров приложение. Один навык, /hyperframes, работает маршрутизатором: подтверждает бриф и отправляет в нужный воркфлоу — /product-launch-video, /faceless-explainer, /pr-to-video, /embedded-captions, /talking-head-recut, /motion-graphics, /music-to-video, /slideshow, /general-video, плюс /remotion-to-hyperframes для портирования существующих React-композиций.
За ними стоит реестр из 113 устанавливаемых блоков и 25 компонентов, а также доменный линтер со 101 правилом в девяти модулях — структура композиции, использование GSAP, субтитры, медиа, шрифты, адаптеры, текстуры, слайдшоу. hyperframes lint статичен; hyperframes check поднимает headless Chrome и проверяет рантайм-ошибки, вёрстку, движение и контраст по WCAG. Регрессионный набор фиксирует около 240 МБ эталонных MP4 в Git LFS.
Вместе линтер и навыки и есть настоящий продукт. Рендерер — лёгкая половина.
Часть II — Аудит
Метод, и почему это не помещается в окно чата
Обычное ревью кода языковой моделью читает файлы. На 823 426 строках это не стратегия, а выборка. Code Indexer работает иначе: он эмбеддит и индексирует каждый чанк, разрешает граф символов, а затем гоняет по нему графовые алгоритмы — персонализированный PageRank для поиска несущих символов, выделение сообществ по Лувену для поиска архитектурных мостов и джойн с историей git, чтобы найти файлы, где сходятся частота изменений, сложность и плотность багфиксов.
Разбиение, полученное для HyperFrames, дало 4 048 сообществ при модулярности Q = 0,534 — граф действительно модульный, а не монолит в костюме монорепы. Детекция циклов нашла ноль циклов функций и ноль циклов файлов. Для кодовой базы такого возраста и такой скорости это сильный результат.
Статический анализ по индексированному графу кода — код не исполнялся, горячие пути рантайма невидимы, а эвристические находки суть сигналы приоритизации, а не вердикты.
— Code Indexer, граница достоверности, прилагаемая к каждому отчёту
Мы отнеслись к этой границе всерьёз. Каждая находка ниже воспроизведена по исходникам до публикации, а три целых класса выводов инструмента отвергнуты как шум. Эти отказы задокументированы ниже — потому что аудит, сообщающий только о попаданиях, называется маркетингом.
| № | Находка | Расположение | Серьёзность | Как проверялось |
|---|---|---|---|---|
| 1 | Одна функция на 3 195 строк; цикломатика 658, когнитивная 1 378; 73% коммитов — багфиксы | <code>packages/core/src/runtime/init.ts:117-3311</code> | Высокая | Открыт диапазон строк; посчитана доля fix-префиксов в <code>git log</code> |
| 2 | Валидатор вывода LLM-воркеров подключён в 1 из 4 родственных сборщиков | <code>skills/*/scripts/assemble-index.mjs</code> | Высокая | Diff в 6 строк; проверка наличия <code>lib/frame-contract.mjs</code>; история со-изменений |
| 3 | Докстринг шима детерминизма обещает покрытие setTimeout, которого нет; линтер тоже не ловит | <code>packages/producer/src/services/fileServer.ts:216-394</code> | Средняя | В продакшн-коде нет ни присваивания <code>window.setTimeout</code>, ни <code>defineProperty</code>; прочитан список правил линтера |
| 4 | Сидирование случайности работает только на распределённом пути рендера | <code>packages/producer/src/services/distributed/renderChunk.ts</code> | Средняя | Перечислены все места вызова <code>seedRandomFromFrame</code> по индексу |
| 5 | Пять побайтово идентичных копий вендоренного бандла на 35 008 байт | <code>registry/blocks/*/lib/liquid-glass.iife.js</code> | Низкая | Сравнение MD5 по всем пяти файлам |
| 6 | Двенадцать опубликованных пакетов без поля лицензии; CI-страж покрывает один | <code>scripts/verify-packed-manifests.mjs</code> | Высокая (для внедрения) | Запрошен живой реестр npm на v0.7.87; прочитан ранний return стража |
Находка 1 — функция на 3 195 строк по имени «Modular»
По когнитивной сложности самая тяжёлая функция HyperFrames отрывается от второго места с большим запасом. initSandboxRuntimeModular() в packages/core/src/runtime/init.ts открывается на строке 117 и закрывается на строке 3311. Это одна функция на 3 195 строк с цикломатической сложностью 658 и когнитивной 1 378 — в 1,7 раза выше следующей худшей функции репозитория и примерно в 92 раза выше рекомендованного SonarSource потолка в 15.
И это же — браузерный бутстрап рантайма: код, который устанавливает плеер, медиаслой, адаптеры и хуки демонтажа внутри каждой страницы, которую снимает рендерер. В продукте нет пути исполнения, который его не проходит.
История git подтверждает то, что предсказывает структура. Из 120 коммитов, затронувших init.ts за последние шесть месяцев, 88 — 73% — имеют префикс fix. По всему репозиторию за те же 3 476 коммитов доля исправлений составляет 38%. init.ts впитывает багфиксы примерно вдвое интенсивнее базовой линии и делает это стабильно с марта.
Имя выдаёт историю. initSandboxRuntimeModular почти наверняка был модульной заменой чему-то худшему, а потом разросся. Это самый рычажный рефакторинг в репозитории: разделить функцию по швам, которые у неё уже есть — мост аналитики, установка плеера, медиаслой, регистрация адаптеров, демонтаж, — значит одновременно сдвинуть худший показатель поддерживаемости и худшую концентрацию дефектов.
Находка 2 — предохранитель, подключённый к одному пайплайну из трёх
Эту находку мы не искали специально, и именно она значит больше всего.
Анализ неявной связанности в Code Indexer добывает из истории git файлы, которые всегда меняются вместе, не импортируя друг друга. На вершине списка, с коэффициентом связанности 1,0 при семи со-изменениях: skills/faceless-explainer/scripts/assemble-index.mjs и skills/pr-to-video/scripts/assemble-index.mjs. Два файла, разные навыки, идентичная динамика изменений.
Это почти идентичные клоны — 620 и 626 строк, различающиеся ровно шестью строками. Эти шесть строк — импорт ./lib/frame-contract.mjs и вызов validateFrameHtml(), обёрнутый в блок с die() при ошибке. Проверка файловой системы закрывает вопрос: frame-contract.mjs существует ровно в одном месте репозитория — skills/pr-to-video/scripts/lib/. При этом копий assemble-index.mjs по дереву навыков четыре. Валидирует только одна.
То, что проверяет этот валидатор, не косметика. Это контракт между LLM-субагентом и сборщиком. Он отвергает вывод воркера, который оказался полноценным HTML-документом вместо голого фрагмента <template>; вывод, содержащий более одного template или разметку после закрывающего тега; корневой элемент без data-composition-id или с идентификатором, не совпадающим с запрошенным кадром; и корень, у которого data-duration отсутствует, нечисловой, нулевой или короче ожидаемого по раскадровке.
В /pr-to-video LLM-воркер, вернувший обёрнутый документ или сдвинувший идентификатор композиции, падает громко на этапе сборки. В /faceless-explainer и /product-launch-video, где та же архитектура прогоняется через тот же сборщик, такой вывод принимается и уходит в композицию. Git показывает, что оба файла были собраны вместе в PR #1778 — «rebuild faceless-explainer + pr-to-video on the shot-sequence architecture» — и с тех пор двигались синхронно. Расхождение — это дрейф, а не замысел.
Исправление невелико: вынести lib/frame-contract.mjs в общее место и подключить к трём остальным сборщикам. Показательна причина, по которой это осталось незамеченным: файлы не связаны графом импортов, ни один линтер их не сравнивает, и ни один человек не читает четыре родственных скрипта по 600 строк рядом. Чтобы это увидеть, понадобился джойн индекса кода с журналом коммитов.
| Навык | assemble-index.mjs | Импортирует frame-contract | Некорректный вывод воркера |
|---|---|---|---|
| /pr-to-video | 626 строк | Да | Жёсткое падение на сборке |
| /faceless-explainer | 620 строк | Нет | Принимается молча |
| /product-launch-video | 768 строк | Нет | Принимается молча |
| /music-to-video | 220 строк | Нет | Принимается молча |
Находка 3 — шим документирует гарантию, которую не реализует
JSDoc над buildVirtualTimeShim утверждает, что тот «замораживает Date.now, performance.now и конвейер rAF/setTimeout». Два из трёх пунктов верны. Date и performance.now подменяются; requestAnimationFrame и cancelAnimationFrame подменяются. setTimeout, clearTimeout, setInterval и clearInterval сохраняются в локальные переменные и реэкспортируются на __HF_VIRTUAL_TIME__ для нужд самого рендерера — но window.setTimeout страницы не подменяется никогда.
Мы проверили это двумя способами. Присваивания window.setTimeout в продакшн-коде нет вообще — единственное совпадение в репозитории находится внутри теста, собирающего мок-контекст, — как нет и Object.defineProperty(window, "setTimeout", …). Шим определяет на window ровно два свойства: Date и performance.now.
Последствие ограничено, но реально: композиция, гоняющая движение через setTimeout или setInterval, во время рендера продолжает жить по настенным часам, пока всё вокруг заморожено на виртуальной метке. И линтер этого не ловит. Правило non_deterministic_code отмечает Math.random(), Date.now(), new Date(), performance.now(), crypto.getRandomValues(), gsap.utils.random() и строковую форму "random(...)" в твинах GSAP — список продуманный, — но анимация на таймерах не попала ни в перечень шима, ни в перечень линтера. Это единственный вектор недетерминизма во фреймворке, который не виртуализирован в рантайме и не отклоняется на линте.
Находка 4 — детерминизм зависит от пути рендера
README формулирует гарантию без оговорок: «Deterministic: same input, same frames, same output». Блок сидированного PRNG, который делает случайность воспроизводимой, закрыт флагом seedRandomFromFrame со значением по умолчанию false. Перебор всех мест вызова по индексу даёт ровно один продакшн-вызов с true: packages/producer/src/services/distributed/renderChunk.ts.
То есть локальные внутрипроцессные рендеры оставляют Math.random и crypto.getRandomValues нативными. Композиция, использующая случайность, побайтово воспроизводима на распределённом пути и невоспроизводима от запуска к запуску на локальном. Комментарий в исходнике прямо говорит, что это сделано намеренно — сохраняется «недетерминированное поведение внутрипроцессного рендерера», — а линтер вообще стоит на позиции «не используйте случайность». Но безоговорочное заявление README и ограниченная реализация — это не одно и то же предложение, и команде, строящей регрессионные тесты в CI на локальных рендерах, стоит знать, на что именно она опирается.
Находка 5 — 175 КБ одного и того же файла, пять раз
Пять блоков реестра — ios26-liquid-glass, liquid-glass-context-menu, liquid-glass-media-controls, liquid-glass-notification и liquid-glass-widgets — каждый вендорит lib/liquid-glass.iife.js. Все пять побайтово идентичны: одинаковый MD5, одинаковые 35 008 байт. Любое исправление в этом бандле придётся вносить пять раз, и ничто в сборке этого не контролирует. Самая дешёвая находка отчёта и самая простая в исправлении.
Находка 6 — двенадцать опубликованных пакетов без лицензии в npm
Эту находку мы вытянули, потянув за нитку лицензирования, и именно она вероятнее всего заблокирует реальное решение о внедрении.
HyperFrames — Apache-2.0. Файл LICENSE в корне репозитория содержит стандартный текст на 190 строк, а CREDITS.md прямо заявляет, что весь код «независимо реализован и распространяется под лицензией Apache 2.0». Это и есть главное преимущество проекта перед Remotion, чья source-available лицензия берёт деньги с компаний выше порога по числу мест.
Намерение репозитория сомнений не вызывает. С опубликованными метаданными сложнее. Из четырнадцати workspace-пакетов ровно один — packages/cli — объявляет "license": "Apache-2.0" в своём package.json. Остальные тринадцать не объявляют ничего, и лишь один из них помечен как приватный.
Мы проверили живой реестр npm, а не репозиторий, — на случай если поле подставляется на этапе публикации. Не подставляется. В версии 0.7.87 CLI-пакет hyperframes публикуется с license: Apache-2.0. @hyperframes/core, @hyperframes/engine, @hyperframes/producer, @hyperframes/player и @hyperframes/sdk публикуются вообще без поля лицензии.
Это важно практически, а не педантически. Любой корпоративный инструмент анализа состава ПО — Snyk, FOSSA, Black Duck, встроенный dependency review в GitHub — разрешает отсутствующее поле license в «unknown», а «unknown» во множестве закупочных политик означает жёсткую блокировку. Разработчик, устанавливающий hyperframes, получает чистый сигнал Apache-2.0; разработчик, зависящий напрямую от @hyperframes/core — как поступил бы любой, встраивающий рендерер, — получает предупреждение сканера, которое юристам придётся разбирать вручную.
Самая острая деталь в том, что предохранитель в репозитории уже есть. В scripts/verify-packed-manifests.mjs лежит функция verifyCliLicense, которая бросает исключение, если исходный манифест не объявляет Apache-2.0, и бросает ещё раз, если упакованный тарбол не сохранил лицензию. Её первая исполняемая строка — if (workspace !== "packages/cli") return;. Проверка работает, крутится в CI и поимённо освобождает от себя тринадцать пакетов из четырнадцати. Расширить стража — правка в одну строку; добавить поле в тринадцать манифестов — работа на пять минут.
| Пакет | Манифест в репозитории | Опубликовано в npm (v0.7.87) | Покрыт CI-стражем лицензии |
|---|---|---|---|
| <code>hyperframes</code> (CLI) | Apache-2.0 | Apache-2.0 | Да |
| <code>@hyperframes/core</code> | не объявлено | поля лицензии нет | Нет |
| <code>@hyperframes/engine</code> | не объявлено | поля лицензии нет | Нет |
| <code>@hyperframes/producer</code> | не объявлено | поля лицензии нет | Нет |
| <code>@hyperframes/player</code> | не объявлено | поля лицензии нет | Нет |
| <code>@hyperframes/sdk</code> | не объявлено | поля лицензии нет | Нет |
Часть III — В чём аудит ошибся
Три сигнала инструментария ввели бы в заблуждение, если опубликовать их без разбора. Мы приводим их, потому что отвергнутая находка — такой же результат, как подтверждённая.
374 «критические» находки безопасности — это не уязвимости. Собственная градация уверенности аудита раскладывает их как 0 доказанных, 0 вероятных, 332 гипотезы — каждая представляет собой статическую эвристику без подтверждения потоком данных. В такой ситуации движок ставит категории «Безопасность» пол и помечает её как «unconfirmed-only … триаж прежде, чем считать вердиктом». Фильтрация по уровню actionable возвращает пустой список. Честная формулировка звучит так: статический анализ не нашёл в HyperFrames ни одной подтверждённой уязвимости — и нашёл груду паттерн-совпадений, которые человеку стоит выборочно проверить.
Оценка SEO «F» — ошибка категории. SEO-сканер нашёл 4 596 проблем в 612 HTML-файлах и выставил ноль во всех разделах. Эти HTML-файлы — видеокомпозиции. У них нет meta description, потому что они не страницы; их иерархия заголовков типографская, а не семантическая; их изображения — кадры анимации. Анализатор, заточенный под веб-приложение и наведённый на видеофреймворк, выдаёт претензии формата веб-приложения. Та же оговорка мягче применима и к общей оценке, которая считается против базовой линии «WebApp».
Большинство отметок самопризнанного техдолга — это проза. Сканер насчитал 100 маркеров SATD, из них 68 с тегом BUG. Мы взяли выборку, и оба попадания оказались поясняющими комментариями, просто содержащими это слово: в portUtils.ts описывается «класс багов на девбоксах, когда порт свободен на 127.0.0.1, но занят на 0.0.0.0 через SSH-форвардинг», а в scaffolding.ts объясняется, что «запись index.html здесь вызывала баг с двойным звуком». Оба — документация уже исправленных проблем, а не признание долга. Счётчики TODO (21) и XXX (10) заслуживают большего доверия, но и там несколько попаданий XXX — это исходники GLSL-шейдеров внутри минифицированного блока.
Часть IV — Профиль качества
Отложите буквенную оценку и читайте категории. HyperFrames набирает 53 из 100 по шкале, откалиброванной под корпоративную готовность к продакшену, — и это 93-й процентиль относительно корпуса из 64 проаудированных open-source инструментов для разработчиков, чья медиана равна 44. Зрелые open-source проекты на этой шкале регулярно оказываются в диапазоне C–F; защитимое число здесь — процентиль.
Тестовая дисциплина здесь выделяется сильнее всего. В 923 тестовых файлах лежат 15 843 тестовые функции и 55 283 проверки — в среднем 3,49 проверки на тест, при нуле тестов без единой проверки. Отношение тестовых файлов к исходным составляет 63% против медианы корпуса в 34%. У каждого из двенадцати кадровых адаптеров есть выделенный тестовый файл. Категория набирает идеальные 15 из 15.
Под этим объёмом есть и тестовые запахи — 1 558 случаев assertion roulette (несколько «голых» проверок в одном тесте, из-за чего падение не говорит, какая именно), 1 088 дублирующихся проверок и 919 «жадных» тестов. Для набора такого размера это гигиена, а не тревога, но пакеты AWS Lambda и авторизации CLI концентрируют их в себе.
Мёртвый код составляет фактически 0% кодовой базы — 19-й процентиль, где меньше значит лучше. «Магические числа» — 0,0 на тысячу строк. Покрытие документацией в 22% символов звучит скудно, пока не увидишь медиану корпуса в 7%; по этой оси HyperFrames находится в 100-м процентиле, и качество комментариев в движке — лучшее тому подтверждение в этой статье.
| Категория | Оценка | Прочтение |
|---|---|---|
| Покрытие тестами | 15 / 15 | 63% тестов к исходникам, 55 283 проверки, ноль тестов без проверок |
| Качество кода | 12 / 15 | 47 сложных функций, ~0% мёртвого кода, 30 мест глубокой вложенности |
| Гигиена | 9 / 15 | 0 опечаток, 0 устаревших API, медианный возраст SATD 31 день |
| Безопасность | 10 / 20 | Пол шкалы — все находки неподтверждённые эвристики, 0 доказанных |
| Поддерживаемость | 5 / 15 | 57 «божественных» файлов, 14 таких же модулей, дублирование в реестре и навыках |
| Документация | 4 / 10 | 1 121 из 4 988 символов задокументированы — и всё равно p100 против конкурентов |
| Архитектура | 2 / 10 | 0 циклов, но 20 очагов связанности и 20 файлов низкой связности |
Человеческий риск
Bus factor равен 2. За полгода в HyperFrames коммитили пятьдесят семь человек, но уход двух главных владельцев осиротил бы больше половины из 3 952 проанализированных файлов. Один участник — основной автор 1 831 файла при 8 748 коммитах; второй — 1 055 файлов. Острова знания, то есть файлы с единственным автором и активными изменениями, группируются в двух местах: UI редактора Studio и документация по промптингу, которая учит агентов пользоваться фреймворком.
Форма этого графика стоит отдельной фразы. Объём коммитов в июле вырос более чем вдвое, тогда как число участников снизилось с майского пика: работа сконцентрировалась, а не распределилась. Темп релизов достиг максимума в апреле-мае — около трёх тегов в день — и с тех пор спадает, что обычно означает переход проекта от захвата территории к консолидации.
Где живёт радиус поражения
Персонализированный PageRank по графу вызовов называет самым зависимым символом дерева replace в packages/parsers/src/gsapInline.ts — в него ведут 897 ссылок. На слое разбора GSAP стоит остановиться: gsapParserAcorn.ts и gsapWriterAcorn.ts вместе дают более 4 000 строк статического анализа и переписывания исходников на Acorn, чья задача — позволить Studio редактировать ключевые кадры прямо в JavaScript композиции и записывать изменения обратно. Это и есть машинерия, которая заставляет написанный руками таймлайн GSAP вести себя как структура данных, — и именно поэтому пакет парсеров попадает одновременно в рейтинг горячих символов и в список кандидатов на рефакторинг.
Анализ по Лувену отмечает run, result, track, basename и createElement как главные архитектурные мосты — один только run касается 265 сообществ при степени 473. Часть из них — родовые имена, которые графовый алгоритм всегда переоценит, и это справедливая претензия к сигналу; по-настоящему обозначают швы подсистем createElement с 93 сообществами и requestAnimationFrame с 47.
Часть V — Лицензия и что она на самом деле ограничивает
Apache-2.0 — самое громкое слово в README HyperFrames и самое тихое в большинстве обзоров о нём. Оно заслуживает отдельного раздела, потому что одновременно является сильнейшим конкурентным аргументом проекта и тем местом, где его граф зависимостей усложняется.
Что даёт Apache-2.0
Apache-2.0 — пермиссивная лицензия с зубами. Код можно использовать, изменять, сублицензировать и коммерциализировать, в том числе в закрытых продуктах, без копилефт-обязательств на собственную работу и без роялти с результата. Существенно, что §3 предоставляет явную безотзывную патентную лицензию от каждого контрибьютора — чего не делают MIT и BSD, — с оговоркой о прекращении, если вы подадите патентный иск против контрибьютора по поводу этой работы. В домене, соседствующем с видеокодеками, этот патентный грант не декоративен.
Обязательства лёгкие и необсуждаемые: сохранять текст лицензии и уведомления об авторских правах при любом распространении, указывать существенные изменения в файлах и передавать дальше файл NOTICE, если он есть. HyperFrames не поставляет NOTICE, и это корректно, а не небрежно — Apache-2.0 требует передавать лишь существующий. Товарные знаки явно не предоставляются, так что «HyperFrames» и марки HeyGen остаются их собственностью, как бы вы ни форкали код.
В сравнении с Remotion — признанным предшественником, поимённо упомянутым в CREDITS.md, — это и есть материальная разница. Remotion распространяется как source-available под собственной лицензией, требующей платной корпоративной подписки выше небольшого порога по числу мест. HyperFrames не берёт ничего, ничего не закрывает за числом мест и не взимает плату за рендер. Если вы рендерите объёмами, в этом и состоит весь коммерческий аргумент — и он настоящий.
Где становится сложнее: граф зависимостей
Лицензия самого фреймворка — только половина картины. Рендер видео означает вызов FFmpeg, а лицензирование FFmpeg зависит от того, как он собран.
Для локальных рендеров HyperFrames требует установленного в системе FFmpeg — он указан в предварительных требованиях рядом с Node 22+. Это чистая схема: фреймворк вызывает бинарник, который у вас и так был, и ничего не распространяется. С пакетом для AWS Lambda иначе. packages/aws-lambda/package.json объявляет ffmpeg-static, который публикуется в npm под GPL-3.0-or-later. Развёртывание этого стека упаковывает GPL-3.0 бинарник в ваш Lambda-артефакт.
Вызов отдельного бинарника через границу процесса — общепринятая схема «на расстоянии вытянутой руки», и она не делает ваш код GPL. Но распространение — вопрос отдельный от связывания: если этот артефакт развёртывания когда-либо покинет вашу организацию (уедет заказчику, будет запечён в образ продукта), обязательства GPL-3.0 по предоставлению исходников прикрепятся к находящемуся внутри бинарнику FFmpeg. Внутренние развёртывания — распространённый случай и проблемы не создают. Но пять минут времени вашего юриста стоит потратить до того, как это станет продуктом.
Вторая тонкость — GSAP. Это основной анимационный адаптер, тот самый, что используется в каждом быстром старте, и он объявлен зависимостью @hyperframes/studio, @hyperframes/player и SDK-площадки. GSAP не является open source. Поле лицензии в npm читается как «Standard 'no charge' license» и ведёт на gsap.com — бесплатно для подавляющего большинства применений, действительно щедро с тех пор, как Webflow открыл набор плагинов, но не одобрено OSI и регулируется собственными условиями, а не лицензией, под которую у вас уже есть политика. Композиция, использующая GSAP, наследует это независимо от того, под чем лицензирован сам HyperFrames.
Остальной граф ничем не примечателен и корректно указан в credits: puppeteer-core под Apache-2.0, ffprobe-static и onnxruntime-node под MIT, а mediabunny под MPL-2.0 — единственная сторонняя лицензия, которую CREDITS.md называет явно, и правильно делает, поскольку MPL-2.0 несёт копилефт на уровне файлов.
| Компонент | Лицензия | Практическое ограничение |
|---|---|---|
| Сам HyperFrames | Apache-2.0 | Коммерческое использование, закрытые форки, нет платы за рендер; сохраняйте уведомления, патентный грант включён |
| <code>puppeteer-core</code> | Apache-2.0 | Ничего, кроме атрибуции |
| FFmpeg (локальный рендер) | Системная установка | Вы предоставляете его сами; ничего не распространяется |
| <code>ffmpeg-static</code> (путь Lambda) | GPL-3.0-or-later | Бинарник упаковывается в артефакт развёртывания — обязательства по исходникам при внешнем распространении |
| GSAP (основной адаптер) | Standard «no charge» license | Не одобрена OSI; регулируется собственными условиями GSAP, а не Apache-2.0 |
| <code>mediabunny</code> (Studio) | MPL-2.0 | Копилефт на уровне изменённых MPL-файлов |
| <code>ffprobe-static</code>, <code>onnxruntime-node</code> | MIT | Ничего, кроме атрибуции |
Ничего из этого не является упрёком к выбору лицензии — он правильный и именно он обеспечит проекту внедрение. Это аргумент в пользу того, что «HyperFrames под Apache-2.0» — начало разговора о комплаенсе, а не его конец, и что тринадцать пакетов, публикуемых вовсе без лицензионных метаданных, делают этот разговор тяжелее, чем он должен быть.
Вердикт
HyperFrames — серьёзная инженерия, выпускаемая с необычной скоростью, и эти два факта связаны в обе стороны.
По-настоящему хорош здесь не заголовочный тезис — HTML вместо React это выбор позиционирования, — а исполнение под ним. Шим виртуального времени сделан аккуратно. Контракт адаптера достаточно узок, чтобы за ним поместилось двенадцать рантаймов, а основной занимал 32 строки. Кеш инжектора видеокадров письменно рассуждает о собственных режимах отказа. Слой захвата документирует стратегию, которую попробовали, измерили и откатили, с хешами коммитов. Ноль циклов зависимостей на 4 048 сообществах в возрасте пяти месяцев — не случайность. А 55 283 проверки без единого теста без проверок — это команда, которая относится к этому серьёзно.
Долг сконцентрирован и читаем. Одна функция несёт 3 195 строк и вдвое превышающую базовую плотность дефектов. Валидатор, существующий ради отлова ошибок LLM-субагентов, подключён к одному из четырёх родственных пайплайнов. В истории про детерминизм два задокументированных пробела — конвейер таймеров, который собственный комментарий шима объявляет покрытым, и гарантия случайности, работающая на распределённом пути и не работающая на локальном. Двенадцать опубликованных пакетов не несут лицензионных метаданных, а CI-страж, который это ловил бы, делает ранний return для всех, кроме одного. Ничто из перечисленного не архитектурно. Всё это — предсказуемый осадок от 352 релизов за 143 дня, и всё это закрывается за неделю работы.
Командам, которые его оценивают: фреймворк готов к продакшену для той задачи, под которую сделан — детерминированный рендер видео, написанного агентом, без стадии сборки. Держите композиции на перематываемых адаптерах, а не на таймерах, не пускайте в них Math.random и считайте локальный путь рендера воспроизводимым ровно настолько, насколько воспроизводима сама композиция. Большую часть этого за вас проконтролирует линтер — чем может похвастаться далеко не всякий фреймворк. Если вы проходите через закупки, заложите час на разговор о лицензиях: фреймворк чисто под Apache-2.0, но путь Lambda упаковывает GPL-3.0 бинарник FFmpeg, а основной анимационный адаптер вообще не open source.
Методология. Каждая цифра в этой статье вычислена Code Indexer по репозиторию на коммите 89e970f — 69 869 символов, 312 603 ссылки, 96 369 рёбер вызовов — либо измерена напрямую через git. Находки с первой по шестую воспроизведены по исходникам вручную: открыты диапазоны строк, сравнены файлы, сопоставлены MD5, перечислены места вызовов, а лицензионные метаданные проверены по живому реестру npm, а не по репозиторию. Три сигнала инструментария отвергнуты как ложноположительные и описаны выше. Код не исполнялся; поведение в рантайме лежит за границей этого анализа, и ничто здесь не является юридической консультацией.
Апдейт — что нашёл второй, независимый аудит
После публикации независимый разбор HyperFrames и Remotion — написанный с позиции соло-оператора, строящего конвейер коротких вертикальных роликов, — вскрыл класс ограничений, который этот аудит не искал. Он читает реестр и скрипты скиллов как продакшн-инструментарий, а не как кодовую базу. Каждое утверждение ниже перепроверено на том же коммите 89e970f, и две его цифры при этом пришлось поправить.
Реестр почти целиком горизонтальный. Из 113 устанавливаемых блоков 111 жёстко зашивают data-width="1920". Вертикальные 1080 объявляют ровно три: tiktok-follow, instagram-follow и spotify-card. Четвёртый кандидат, flowchart-vertical, вертикален по вёрстке, но объявляет холст 1440 — так что оценка независимого отчёта в четыре вертикальных блока на один щедрее реальности, верная цифра три. Тот же узор идёт по компонентам: из шестнадцати компонентов субтитров пятнадцать где-нибудь в разметке жёстко зашивают 1920 или 1080px, единственное исключение — caption-blend-difference.
Это важно, потому что переворачивает нашу же трактовку каталога. Мы описали 113 блоков и 25 компонентов как доказательство того, что настоящий продукт — линтер и скиллы, а не рендерер. Так и есть, но стоящий за ними каталог собран под 16:9, и команда, направившая этот фреймворк на Reels или TikTok, получает работу по портированию, о которой число блоков не предупреждает.
В конвейере субтитров есть более острый случай того же самого, и это уже настоящий дефект. В skills/product-launch-video/scripts/lib/dimensions.mjs объявлено CAPTION_BAND_FRACTION = 0.1667, и captionBand() кладёт полосу субтитров в нижние 16,67% кадра. Собственные справочные материалы проекта этому противоречат: skills/embedded-captions/references/aesthetic-principles.md говорит, что для 9:16 в TikTok, Instagram и Shorts зона субтитров — это y от 12% до 78%, потому что нижние 22% занимает интерфейс платформы. Дефолтная полоса, таким образом, целиком лежит внутри области, которую сам проект помечает как перекрытую кнопками платформы. Правильное число задокументировано — до кода пайплайна оно не доехало.
И эта константа объявлена не однажды. Идентичные копии dimensions.mjs с тем же CAPTION_BAND_FRACTION = 0.1667 лежат в product-launch-video, pr-to-video и faceless-explainer, а четвёртый воркфлоу вызывает captionBand. Читатель Находки 2 узнает форму: та же топология клонов-сиблингов, из-за которой валидатор frame-contract оказался подключён к одному ассемблеру из четырёх, управляет и геометрией субтитров. Починить безопасную зону — значит починить её в трёх местах, и ничто в сборке не следит за тем, чтобы они совпадали.
Независимый аудит сообщает также — уже по реальному рендеру на своей машине, — что пресет portrait меняет холст, но не перекомпоновывает вёрстку: текст обрезается по краям. При этом hyperframes check честно это поймал как container_overflow, panel_out_of_canvas и text_occluded, но на уровне warning и info, так что рендер не заблокировался. Это согласуется с тем, что мы нашли в устройстве линтера, и является практическим следствием: гейт существует, проблему определяет верно и по умолчанию пропускает. Разница между диагностикой и гейтом — флаг check --strict. Рендер мы не запускали и обрезку подтвердить самостоятельно не можем; механизм, однако, ровно тот, который предсказывает код.
На этом стоит остановиться, потому что это самое обобщаемое, что вскрыла вся серия. У HyperFrames теперь два подтверждённых дефекта, порождённых одним механизмом: валидатор, подключённый к одному ассемблеру из четырёх, и константа безопасной зоны, объявленная трижды со значением, которому противоречит собственная документация проекта. Оба живут в скопированных скриптах под skills/*/scripts/. Ни одна пара не импортирует другую, так что их не связывает ни граф импортов, ни граф вызовов: они связаны только именем файла и формой того, что внутри. Это ровно то слепое пятно, которое есть у любого инструмента, рассуждающего поверх графа зависимостей, — и именно поэтому оба дефекта уцелели в репозитории со 101 правилом линта, 55 283 ассертами и браузерным рантайм-гейтом.
При этом расхождения разные по типу, и вот эта деталь и важна тому, кто захочет всё это механизировать. В случае с ассемблерами предохранитель был у одного сиблинга и отсутствовал у остальных. В случае с субтитрами все сиблинги побайтово идентичны, а проблема — в значении, на котором они согласны. Детектор, который группирует файлы по имени и сравнивает предохранители, поймает первый случай и пройдёт мимо второго: ему нужно сравнивать ещё и объявленные константы-литералы. Честная сноска — как каждый из них нашёлся на самом деле: первый выпал из джойна с историей co-change, второй — только потому, что тот же репозиторий разобрали под совершенно другим углом, спросив не «как это устроено», а «можно ли на этом выпускать вертикальные ролики». Ни то ни другое детектором не является. И то и другое им быть должно.
Тому, кто сопровождает репозиторий такой же формы — параллельные директории воркфлоу, у каждой своя копия общего скрипта, — дешёвая защита выглядит как тест, который для ровно этого класса держит Remotion: перегенерировать сиблингов из одного источника и проверить совпадение. Remotion так синхронизирует два побайтово идентичных списка пакетов по 104 строки, в packages/it-tests/src/monorepo/package-sync.test.ts. У HyperFrames четыре ассемблера и три модуля размеров — и ни одного такого теста. Именно эта асимметрия, а не какой-то отдельный дефект, и есть то, вокруг чего на самом деле вращается сравнение этих двух кодовых баз.
📚 Источники и ссылки
| # | Source | Link |
|---|---|---|
| [1] | HyperFrames — открытый фреймворк HTML-нативного рендеринга видео |
|
| [2] | Code Indexer — движок семантического поиска и анализа кода |
|