За последнее время я написал три статьи про систему, которая превращает длинные видео в вертикальные клипы: общий обзор «аниме-завода», разбор виртуальной камеры с 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, явные промежуточные артефакты.

В коде это:

Принцип из статьи

Где живёт

Оркестрация по этапам

core/channel_processor.py — один канал, список включённых этапов

Fail-soft

падение канала не роняет остальные; отсутствие mediapipe не роняет детекцию; недоступность Kodik мягко пропускает этап

Явные артефакты

_transcript.txt, _moments.json, _candidates.json, _signals.json, *_chatgpt_payload.txt — всё пишется рядом с клипами

Отладка этапа в изоляции

debug: true: вход из test_data/, выход в output_test_data/, дампы всех промежуточных данных

Отдельно про последнюю строку. Пайплайн, в котором падение на девятом этапе стоит тебе первых восьми, — это пайплайн, который никто не будет крутить итеративно. Поэтому в debug-режиме, если моменты ещё не считались, а пост-клиповые этапы включены, берётся весь ролик целиком: чтобы можно было отлаживать субтитры или ватермарк, не гоняя Whisper и LLM по кругу.

Статья 2 → rendering/face_detector.py

«Я научил виртуальную камеру быть оператором» — про то, почему центровой кроп выбрасывает половину кадра, а наивное следование за лицом даёт камеру, на которую физически неприятно смотреть.

Весь описанный алгоритм — в одном модуле:

  1. Каскад детекторов: MediaPipe → YuNet (ONNX через OpenCV) → Haar Cascade. Отказ любого бэкенда не ломает этап.

  2. Выбор героя по saliency (уверенность + крупность + центральность) с гистерезисом: face_switch_margin и face_switch_hold_s не дают камере пинг-понговать между персонажами.

  3. Фильтрация: анти-джерк (жёсткий лимит скачка между кадрами) + low-pass.

  4. Физика: a = k·error − c·v с лимитами скорости и ускорения, мёртвой зоной, предиктивным упреждением и микролагом «живой руки».

  5. Композиция: eye-level lift, смещение по правилу третей, защитные поля до краёв.

  6. Склейки: на монтажном резе камера не переезжает через кадр, а мгновенно пересобирается.

  7. 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_timegpt.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 у многих провайдеров заблокирован (этап это переживает и мягко пропускается).

Итого

Три статьи описывали идеи. Этот репозиторий — то же самое, но в виде кода, который запускается и покрыт тестами.

Сквозная мысль всей серии никуда не делась: ни один сигнал не является достаточным. Громкий момент ≠ интересный, красивая реплика ≠ понятная без контекста, идеальная детекция лица ≠ приятное движение камеры. Всё работающее здесь получилось из комбинации слабых сигналов и из аккуратной деградации, когда какой-то из них отвалился.

Если разбирать конкретный слой — вот навигация:

Хочу понять

Читать

Смотреть код

Общую архитектуру и принципы

статья 1

core/channel_processor.py, docs/ARCHITECTURE.md

Как выбирается момент

статья 3

analysis/moment_scorer.py, analysis/moment_validator.py, analysis/gpt_analyzer.py

Виртуальную камеру

статья 2

rendering/face_detector.py

Что вообще можно покрутить

channels/DemoChannel/config.yaml, docs/CONFIG_REFERENCE.md

Английские версии первых двух статей: обзор системы, виртуальная камера.

Репозиторий: github.com/ialakey/shorts-factory Звёзды, issues и PR приветствуются. Особенно интересно, если кто-то приспособит это под не-аниме контент: подкасты, лекции, стримы — вся механика к домену не привязана, привязаны только веса, и они в конфиге.