Как я написал AI-генератор коротких видео (Shorts/Reels) на Python с Clean Architecture
Привет, Хабр! ?
В этой статье я хочу поделиться опытом разработки пет-проекта, который превратился в полноценный инструмент для автоматической генерации коротких видео (Shorts, Reels, TikTok). Идея проста: на входе — тема (например, "История Римской Империи за 1 минуту"), на выходе — готовый видеоролик с озвучкой, субтитрами и сгенерированным видеорядом.
Но вместо того, чтобы накидать "спагетти-код" в одном файле main.py, я решил подойти к задаче как инженер и построить систему на принципах Clean Architecture.
? Зачем?
Генерация видео — это сложный пайплайн:
1. Написать сценарий.
2. Придумать визуальный стиль.
3. Сгенерировать картинки (Midjourney, Flux).
4. Оживить картинки в видео (Kling, Runway, Sora).
5. Озвучить текст (TTS).
6. Собрать всё вместе с субтитрами.
API меняются, модели выходят новые каждую неделю. Сегодня лучший визуал у Flux, завтра у Midjourney v7. Сегодня видео делаем в Runway, завтра в Kling. Жесткая привязка к конкретным API убила бы проект через месяц.
Поэтому Clean Architecture здесь не роскошь, а необходимость.
? Архитектура
Проект разбит на слои, следуя классической "луковой" архитектуре:
1. Domain (Entities): Pydantic-модели, описывающие суть (VideoScript, Scene, Character). Они ничего не знают о внешнем мире.
2. Interfaces: Абстрактные классы (VideoGenerator, ScriptGenerator). Контракты, которые должны соблюдать внешние сервисы.
3. Services (Use Cases): Бизнес-логика. Здесь живут "Агенты": Сценарист, Арт-директор, Режиссер монтажа.
4. Infrastructure: Реализации интерфейсов (API клиентов Comet, Yandex, OpenAI и т.д.).
Стек технологий
* Python 3.11+
* Pydantic: Для строгой типизации данных между слоями.
* Asyncio: Генерация контента — долгий процесс, все запросы к API должны быть асинхронными.
* LangChain: Для удобной работы с LLM.
* MoviePy: Для финальной сборки видео.
? Команда Агентов
Вместо одного "умного" промта, который делает всё, я разделил обязанности между специализированными агентами.
1. Screenwriter (Сценарист)
Его задача — только текст. Он не думает о том, как это будет выглядеть. Он выдает JSON со сценами, диалогами и таймингами.
class Scene(BaseModel):
scene_number: int
narration: str # Текст для озвучки
duration_seconds: int = 5
# Визуальные поля пока пустые
image_prompt: str | None = None
video_prompt: str | None = None
2. Art Director (Арт-директор)
Вот здесь начинается магия. Арт-директор получает сухой сценарий и превращает его в визуальное описание.
Недавно я разделил логику так, что Арт-директор может генерировать промты с нуля, если Сценарист их не предоставил.
Он использует специальные промты для LLM, чтобы добавить "кинематографичности": cinematic lighting, 8k, dramatic atmosphere.
# app/services/art_director.py
async def enhance_script(self, script: VideoScript) -> VideoScript:
"""Арт-директор проходит по всем сценам и создает визуальный стиль."""
for scene in script.scenes:
if not scene.image_prompt:
# Если сценарист не дал визуал, придумываем сами
scene.image_prompt = await self._generate_scene_visuals(scene)
else:
# Иначе улучшаем то, что есть
scene.image_prompt = await self._enhance_prompt(scene.image_prompt)
return scriptt
3. Motion Director (Постановщик движения)
Статичная картинка — это скучно. Motion Director добавляет инструкции для видео-генератора.
* "Camera zoom in"
* "Character turns head left"
* "Wind blowing through hair"
Он использует шаблон motion_director.txt, который заставляет LLM выдавать строгие механические инструкции, понятные нейросетям типа Kling или Runway.
## ? Pipeline (Оркестратор)
Все это связывается в классе VideoPipeline. Это фасад, который управляет потоком данных.
# app/services/pipeline.py
async def generate_video(self, topic: str):
# 1. Сценарий
script = await self.script_gen.generate_script(topic)
# 2. Визуальный стиль
script = await self.art_director.enhance_script(script)
# 3. Движение
script = await self.motion_director.enhance_script(script)
# 4. Параллельная генерация ассетов
await asyncio.gather(
self.generate_images(script),
self.generate_voiceovers(script)
)
# 5. Оживление (Image-to-Video)
await self.generate_videos(script)
# 6. Монтаж
final_video = await self.video_editor.edit_video(script)
return final_video
Интересный момент: мы используем asyncio.gather для параллельной генерации. Пока генерируются картинки для 10 сцен, параллельно может идти озвучка. Это экономит кучу времени.
? Адаптеры и Интерфейсы
Благодаря интерфейсам, я могу менять провайдеров видео "на лету".
class VideoGenerator(ABC):
@abstractmethod
async def generate_video(self, image_path: str, prompt: str) -> str:
pass
У меня есть CometVideoGenerator (наш внутренний сервис), KieVideoGenerator (для Grok), и я легко могу добавить RunwayVideoGenerator.
В dependencies.py я просто проверяю конфиг и подставляю нужную реализацию:
if "grok" in settings.VIDEO_GENERATION_MODEL:
video_gen = KieVideoGenerator(...)
else:
video_gen = CometVideoGenerator(...)
? CLI и новые фичи
Недавно добавил возможность генерировать видео только по картинке, игнорируя текстовые промты. Это полезно, когда нейросеть (например, Kling) лучше понимает контекст из самой картинки, а текст её только сбивает.
В cli.py добавил флаг --video-image-only, который прокидывается в пайплайн:
# В пайплайне
if not self.use_video_prompt:
prompt = None # Генератор получит только image_path
? Результат
В итоге получилась гибкая система. Я могу:
1. Поменять LLM для сценария (OpenAI -> Mistral -> Local Llama).
2. Поменять генератор картинок (Midjourney -> Flux).
3. Поменять генератор видео.
И всё это без переписывания бизнес-логики.
Что дальше?
* Веб-интерфейс (FastAPI + React).
* Интерактивный режим (правка сценария человеком перед генерацией).
* Поддержка вертикального/горизонтального форматов на уровне кропа.
Ссылка на проект: GitHub
Пишите в комментариях, какие инструменты для AI видео используете вы!