— Расскажи нам, папа, сказочку... — Ой, да когда мне вам рассказывать, у меня работы невпроворот. Послушайте лучше аудиокнижку. — Аудиокнижки скучные. Вот если бы нам их лисичка почитала... или там Айвенго, или Мэри Поппинс. А так — сиди и слушай...
С этого всё и началось.
Задача казалась простой: взять изображение персонажа и аудио — и получить видео, где он говорит. В эпоху ИИ, когда ChatGPT пишет диссертации и рисует котиков, такая технология должна существовать.
Она существовала. Но работала в десять раз медленнее реального времени — даже на игровой видеокарте.
Проблема первая: нейросети медленные
Wav2Lip, SadTalker, FasterLivePortrait — всё это сильные модели, результат многолетних исследований, с впечатляющими демо. Но на практике возникают проблемы.
GPU обязателен. Без него — слайд-шоу. С ним — всё равно тормоза: десять секунд обработки на секунду видео. Мы хотели рендерить аудиокниги в реальном времени, а получали очередь на рендерферму.
Ещё одна проблема — артефакты. Нейросети сами придумывают движения губ. Иногда — странно: мыльные губы, плывущие зубы, лицо, которое будто не до конца своё. Модель не знает, как именно этот персонаж двигает ртом — она галлюцинирует правдоподобно, но не точно.
Тогда возникла идея: а зачем генерировать мимику каждый раз? Что если записать её один раз — медленно, с GPU — а потом воспроизводить быстро?
Граммофон вместо синтезатора.
Синтезатор может сыграть любую ноту — но звучит синтетически. Граммофон воспроизводит запись живого исполнителя — и звучит живо, потому что так оно и есть.
Как это работает
Снимаем человека на видео — несколько минут речи, фонетически разнообразной, чтобы покрыть все основные звуки языка. Нарезаем видео на короткие перекрывающиеся сегменты: по десять кадров (~0.4 секунды), шаг — два кадра. Для каждого сегмента вычисляем акустические признаки: 16-мерный вектор из MFCC-коэффициентов, энергии и спектрального центроида.
Получается библиотека: этот кусок видео соответствует такому звуку.
При рендере новой аудиодорожки мы просто ищем в этой библиотеке подходящие сегменты и склеиваем их. Никакого inference. Никаких весов. Человек всегда выглядит как человек — потому что мы показываем реальное видео, а не нейросетевую фантазию.
Проблема вторая: иногда человека нет
Лисичка из детской книжки не придёт на съёмку. Айвенго — персонаж романа двухсотлетней давности. А заказчику нужен именно он, а не актёр в доспехах.
Здесь на помощь приходит FasterLivePortrait — но уже в новой роли. Не как инструмент рендера (он слишком медленный), а как генератор обучающих данных.
Берём изображение персонажа. Запускаем FLP — он генерирует видео, где персонаж произносит специально подобранный фонетически насыщенный текст. Да, это долго. Да, нужен GPU. Но это происходит один раз.
Дальше — тот же пайплайн: нарезаем на сегменты, вычисляем признаки, строим библиотеку. И рендерим любое количество видео с любым аудио — уже без GPU.
FLP здесь — не костыль. Это поставщик данных. Медленный, дорогой, но одноразовый.
Лисичка читает про трёх поросят. Айвенго рассказывает про турниры. Чингачгук — про прерии. Каждый — своим лицом, своей мимикой. Главное, чтобы персонаж был антропоморфным: глаза, нос, рот — примерно на своих местах. Совсем абстрактные образы пока не работают, но лисички из книжек — вполне.
Как склеивать кусочки так, чтобы не было швов
Это главная техническая сложность. Два сегмента могут сильно отличаться по положению лица на стыке. Наивная склейка даёт рывок.
Выбор кандидата
Сначала — выбираем правильный сегмент. Планировщик смотрит вперёд на две секунды, берёт среднее по будущим акустическим признакам и ищет сегмент под будущую фонетику — не под текущую. Это lookahead, и он улучшает синхронизацию: сегмент подобран под то, что будет звучать.
Из всей библиотеки берём топ-20 по акустическому сходству. Затем выбираем лучший стык: у каждого сегмента хранятся миниатюры первого и последнего кадра (64×64 пикселя). Считаем пиксельное расстояние между концом текущего сегмента и началом каждого кандидата. Берём ближайший. Операция быстрая — используются только крошечные изображения.
Морф через оптический поток
Даже лучший кандидат даёт небольшой рывок. Чтобы сгладить переход, используем двунаправленный оптический поток Фарнебека.
Почему двунаправленный? Простой crossfade размывает кадры — пиксели усредняются, получается каша. Двунаправленный поток двигает каждый пиксель по траектории из A в B и из B в A, затем смешивает. Переход выглядит как движение, а не как растворение.
Длина морфа адаптивная — от 5 до 12 кадров, в зависимости от различий между кадрами. Планировщик минимизирует разницу, поэтому большинство переходов — 5–7 кадров — почти незаметны.
Что делать в тишине
Когда персонаж молчит, нужно показывать что-то естественное. Случайный сегмент из речевой библиотеки — рот двигается без звука, что выглядит жутковато. Заморозка последнего кадра — неестественно.
Решение: отдельная библиотека для пауз. При подготовке модели генерируется короткое видео, где анимируются только глаза — моргание, движения взгляда, рот закрыт. Во время тишины персонаж переключается на эти сегменты и естественно моргает.
Почему это работает на CPU
Каждый шаг выбран так, чтобы не требовать GPU:
- Косинусный поиск по тысячам сегментов — умножение 16-мерных векторов, выполняется мгновенно
- Выбор лучшего стыка — MAD по 20 картинкам 64×64, ~80 тысяч операций
- Оптический поток Фарнебека — классический алгоритм компьютерного зрения, OpenCV считает его на CPU
- Декодирование JPEG и remap — пиксельные операции
Самый тяжёлый этап — предвычисление оптических потоков между соседними кадрами — можно вынести в подготовку модели. Тогда рендер просто применяет готовые карты.
Итог: работа в реальном времени на обычном ноутбуке. GPU не нужен.
Вместо выводов
Две проблемы — одно решение.
Нейросети медленные? Записываем мимику один раз, воспроизводим быстро. Человека нет? FasterLivePortrait генерирует обучающее видео из картинки — один раз, медленно, с GPU. Дальше — быстрый retrieval.
Retrieval вместо generation. Граммофон вместо синтезатора.
Лисичка читает сказки. Дети довольны. Папа может вернуться к работе.