За последнее время я написал три статьи про систему, которая превращает длинные видео в вертикальные клипы: общий обзор «аниме-завода», разбор виртуальной камеры с face tracking и рассказ о том, почему связка «транскрипт + LLM» почти всегда даёт мусор.
Комментарий, который повторялся под всеми тремя, был один и тот же: «покажи код».
Показываю.
→ github.com/ialakey/shorts-factory
Это витринная ветка большого приватного проекта: MVP, в котором оставлен полный путь «взял длинное видео → получил готовый вертикальный клип», а всё остальное вырезано. Ниже — что именно внутри, как код соотносится с тремя предыдущими статьями и что пришлось выкинуть.

▶ Видеодемонстрация — «Аниме ЗАВОД»
Канал @AnimeFactorio, ради которого всё и затевалось
Что это в одном абзаце
На вход — эпизод целиком. На выходе — несколько готовых роликов 9:16 с динамическим кадрированием, пословными субтитрами, водяным знаком, обработанным звуком, вычищенными метаданными и отправкой в Telegram. Между этими двумя точками:
эпизод.mp4 → транскрипт → сигналы (аудио / лица / склейки / темп / hooks) → LLM-отбор моментов → монтажная сборка сегментов → виртуальная камера 9:16 → слои (субтитры, ватермарк, музыка) → пересборка метаданных → Telegram
Тринадцать этапов, каждый включается и выключается отдельно. Никаких флагов командной строки: поведение канала целиком описано одним config.yaml, потому что основная работа здесь — это подбор параметров, а подбору нужен файл, который можно диффать и откатывать.
По цифрам: ~7 000 строк основного кода, 249 тестов на ~3 000 строк, полностью прокомментированный демо-конфиг на 344 строки.
Как код соотносится со статьями
Три статьи описывали три разных слоя. Вот где они лежат в репозитории — это, собственно, и есть главная причина, по которой я пишу эту статью: связать текст с исполняемым кодом.
Статья 1 → архитектура целиком
«Как я построил аниме-завод» была про принципы: независимые модули вместо монолитной end-to-end модели, fail-soft вместо fail-fast, явные промежуточные артефакты.
В коде это:
Принцип из статьи | Где живёт |
|---|---|
Оркестрация по этапам |
|
Fail-soft | падение канала не роняет остальные; отсутствие |
Явные артефакты |
|
Отладка этапа в изоляции |
|
Отдельно про последнюю строку. Пайплайн, в котором падение на девятом этапе стоит тебе первых восьми, — это пайплайн, который никто не будет крутить итеративно. Поэтому в debug-режиме, если моменты ещё не считались, а пост-клиповые этапы включены, берётся весь ролик целиком: чтобы можно было отлаживать субтитры или ватермарк, не гоняя Whisper и LLM по кругу.
Статья 2 → rendering/face_detector.py
«Я научил виртуальную камеру быть оператором» — про то, почему центровой кроп выбрасывает половину кадра, а наивное следование за лицом даёт камеру, на которую физически неприятно смотреть.
Весь описанный алгоритм — в одном модуле:
Каскад детекторов: MediaPipe → YuNet (ONNX через OpenCV) → Haar Cascade. Отказ любого бэкенда не ломает этап.
Выбор героя по saliency (уверенность + крупность + центральность) с гистерезисом:
face_switch_marginиface_switch_hold_sне дают камере пинг-понговать между персонажами.Фильтрация: анти-джерк (жёсткий лимит скачка между кадрами) + low-pass.
Физика:
a = k·error − c·vс лимитами скорости и ускорения, мёртвой зоной, предиктивным упреждением и микролагом «живой руки».Композиция: eye-level lift, смещение по правилу третей, защитные поля до краёв.
Склейки: на монтажном резе камера не переезжает через кадр, а мгновенно пересобирается.
Ken Burns как fallback: без лиц камера не замирает, а мягко панорамирует вокруг последнего известного положения героя.
Плюс то, о чём в статье я не говорил: профили static / operator / action задают базовый темперамент камеры, а любой ключ в dynamic_shorts переопределяет профиль точечно. Анализ идёт на 8 к/с, состояния камеры интерполируются на полную частоту кадров — считать детекцию на каждом кадре незачем.
Статья 3 → analysis/
«Почему LLM + транскрипт почти всегда дают мусор» была самой спорной в комментариях, поэтому по ней покажу конкретику.
Тезис был такой: не надо требовать от одной модели правоты во всём. Сцена сначала описывается по нескольким независимым осям, кандидаты детерминированно ранжируются до того, как модель их увидит, а модель делает ровно то, в чём хороша — судит, интересен ли момент.
Скоринг живёт в analysis/moment_scorer.py, и веса вынесены в конфиг открытым текстом:
moment_scoring: cell_s: 0.5 # разрешение временной сетки сигналов step_s: 1.0 # шаг скользящего окна max_candidates: 12 # сколько остаётся после NMS prompt_limit: 8 # сколько из них реально уходит в промпт weights: # нормализуются к сумме 1 transcript: 0.24 # плотность и содержание реплик audio: 0.20 # эмоциональные пики звука face: 0.14 # есть ли на чём держать вертикальный фокус scene: 0.12 # смены планов и движение pacing: 0.14 # темп, отсутствие провисаний hook: 0.16 # сила входа в первые 1–2 секунды
score = Σ weight_i · signal_i, затем NMS по пересечению окон. В промпт уходит верхушка списка — восемь кандидатов вместо всего транскрипта. Это заметно стабильнее свободного отбора и попутно дешевле по токенам.
Вторая половина, которой в статье почти не было, — что происходит после ответа модели. analysis/moment_validator.py не принимает её слова на веру:
durationпересчитывается как сумма сегментов, а не берётся из ответа;пересечения сегментов разводятся, границы притягиваются к паузам речи и монтажным склейкам;
моменты вне диапазона
gpt.min_time–gpt.max_timeотбрасываются;если после фильтрации ничего не осталось — запрос повторяется (до трёх попыток) с уточняющим суффиксом, а если и это не помогло, список добивается эвристическими кандидатами.
То есть эпизод не падает из-за плохого ответа модели — он деградирует до чисто алгоритмического отбора. Это, пожалуй, главный практический вывод всей серии: качество тут делается не поиском идеального решения, а систематической фильтрацией плохих.
И момент — это не «вырезать кусок с 5:30 по 6:00». Это монтажная сборка: segments могут браться из разных частей эпизода, паузы вырезаются, hook всегда в первом сегменте, последний — клиффхэнгер или петля к началу.
Что в статьях не было, а в репозитории есть
Тесты. 249 штук, полностью герметичные: OpenAI, Telegram и Kodik замоканы, модуль whisper подменяется заглушкой в conftest.py — иначе CI тянул бы torch и качал модели. mediapipe и anime_parsers_ru в CI намеренно не ставятся: код обязан деградировать на fallback-детекторы, и это проверяется тестом, а не обещанием в README.
Отдельно есть test_render_stages.py, где каждый этап make_clips реально рендерит валидный mp4, и test_pipeline_e2e.py — сквозной прогон канала от видео до Telegram. CI гоняет быстрый слой и рендер-слой разными шагами, чтобы сразу было видно, сломалась логика или именно рендер.
Слои рендера. Субтитры рендерятся по словам, с опциональным переразпознаванием клипа более тяжёлой моделью Whisper и последующей LLM-чисткой текста: правится орфография и ровно одно ключевое слово оборачивается в <hl>…</hl>, которое рендерер красит отдельным стилем. Цветные эмодзи — через Pilmoji. Плюс soft-knee компрессор диалоговой дорожки, размытый фон из исходника, титры с автоподбором кегля.
Доставка. spoof_metadata пересобирает контейнер без перекодирования (-c copy), полностью стирает исходные метаданные (-map_metadata -1) и подставляет правдоподобные данные редактора. Оригинал удаляется только после успешной пересборки — при падении ffmpeg клип не теряется.
Что вырезано и почему
Честно, чтобы не было вопросов:
автозагрузка на YouTube и TikTok — осталась в приватном проекте;
планировщик публикаций;
контур аналитики (обратная связь по метрикам роликов в веса скоринга).
Витрина заканчивается на «готовый клип на диске, отправленный в Telegram». Все параметры конфигурации сброшены к нейтральным значениям — это дефолты для первого запуска, а не «правильные» числа. Ключей, доступов и чужих медиафайлов в репозитории нет: шрифты положены только те, что под SIL OFL, музыку и фоны нужно принести свои.
Запуск
git clone https://github.com/ialakey/shorts-factory cd shorts-factory python -m venv .venv && source .venv/bin/activate # Windows: .\.venv\Scripts\Activate.ps1 pip install -r requirements.txt cp .env.example .env # OPENAI_API_KEY обязателен python app.py
Нужны Python 3.12+ и ffmpeg с ffprobe в PATH — именно бинарники, потому что они вызываются напрямую как команды; того, что внутри imageio-ffmpeg, не хватит, там нет ffprobe. Есть Dockerfile, если возиться с системными зависимостями OpenCV не хочется.
Рекомендуемый порядок: прогнать отладочный запуск на test_data/ с debug: true, посмотреть промежуточные артефакты, покрутить веса — и только потом ставить реальные эпизоды.
Известные шероховатости тоже перечислю, они все в README: используется legacy-API openai==0.28.0, OPENAI_API_KEY требуется даже для сценариев без LLM (проверка стоит на импорте config.py), pilmoji 2.0.4 жёстко требует emoji 1.x, а Kodik у многих провайдеров заблокирован (этап это переживает и мягко пропускается).
Итого
Три статьи описывали идеи. Этот репозиторий — то же самое, но в виде кода, который запускается и покрыт тестами.
Сквозная мысль всей серии никуда не делась: ни один сигнал не является достаточным. Громкий момент ≠ интересный, красивая реплика ≠ понятная без контекста, идеальная детекция лица ≠ приятное движение камеры. Всё работающее здесь получилось из комбинации слабых сигналов и из аккуратной деградации, когда какой-то из них отвалился.
Если разбирать конкретный слой — вот навигация:
Хочу понять | Читать | Смотреть код |
|---|---|---|
Общую архитектуру и принципы |
| |
Как выбирается момент |
| |
Виртуальную камеру |
| |
Что вообще можно покрутить | — |
|
Английские версии первых двух статей: обзор системы, виртуальная камера.
Репозиторий: github.com/ialakey/shorts-factory Звёзды, issues и PR приветствуются. Особенно интересно, если кто-то приспособит это под не-аниме контент: подкасты, лекции, стримы — вся механика к домену не привязана, привязаны только веса, и они в конфиге.

