— Расскажи нам, папа, сказочку... — Ой, да когда мне вам рассказывать, у меня работы невпроворот. Послушайте лучше аудиокнижку. — Аудиокнижки скучные. Вот если бы нам их лисичка почитала... или там Айвенго, или Мэри Поппинс. А так — сиди и слушай...

Вот с этого примерно и началось.

Задача казалась простой: взять картинку персонажа, взять аудио — и получить видео, где персонаж говорит. Технология-то должна существовать, мы живём в эпоху искусственного интеллекта, ChatGPT рисует котиков и пишет диссертации.

Технология существовала. Но работала в десять раз медленнее реального времени. На игровой видеокарте.

Проблема первая: нейросети медленные

Wav2Lip, SadTalker, FasterLivePortrait — всё это отличные штуки. Серьёзные модели, годы исследований, впечатляющие демо. Но когда доходит до практики, выясняется несколько неприятных вещей.

GPU обязателен. Без него — слайдшоу. С ним — всё равно тормоза: десять секунд обработки на секунду видео. Мы хотели рендерить аудиокниги в реальном времени, а получали очередь на рендерферму.

Помимо скорости — артефакты. Нейросеть придумывает движения губ. Иногда придумывает странно: мыльные губы, плывущие зубы, лицо которое как будто немного не то. Модель не знает, как именно этот персонаж двигает ртом — она галлюцинирует правдоподобно, но не точно.

И тут возникла мысль: а почему, собственно, надо каждый раз генерировать мимику? Что если записать её один раз — пусть даже медленно, пусть даже с GPU — а потом воспроизводить быстро?

Граммофон вместо синтезатора.

Синтезатор может сыграть любую ноту в любой момент — но звучит синтетически. Граммофон воспроизводит то, что записал живой исполнитель — и звучит живо, потому что так оно и есть.

Как это работает

Снимаем человека на видео — несколько минут речи, желательно фонетически разнообразной, чтобы покрыть все основные звуки языка. Нарезаем видео на короткие перекрывающиеся сегменты: десять кадров, ~0.4 секунды, шаг два кадра. Для каждого сегмента вычисляем акустические признаки аудио — 16-мерный вектор: MFCC-коэффициенты, энергия, спектральный центроид.

Получается библиотека: вот этот кусочек видео соответствует вот такому звуку.

При рендере новой аудиодорожки мы просто ищем в этой библиотеке подходящие кусочки и склеиваем их. Никакого inference. Никаких весов. Человек всегда выглядит как человек — потому что мы показываем настоящего человека, а не нейросетевую фантазию о нём.

Проблема вторая: иногда человека нет

Лисичка из детской книжки не может прийти на съёмку. Айвенго тоже недоступен — он вообще персонаж романа двухсотлетней давности. А заказчику нужен именно он, а не актёр в рыцарских доспехах.

Здесь на сцену выходит FasterLivePortrait — и уже в совершенно другой роли. Не как инструмент рендера (он для этого слишком медленный, мы помним), а как инструмент подготовки данных.

Берём картинку персонажа. Запускаем FLP — он генерирует видео, где персонаж произносит специально подобранный текст: фонетически разнообразный, покрывающий все основные звуки. Да, это занимает время. Да, тут нужен GPU. Но это происходит один раз.

Дальше — тот же пайплайн: нарезаем на сегменты, вычисляем признаки, строим библиотеку. И рендерим любое количество видео с любым аудио без всякого GPU.

FLP здесь — не костыль и не противоречие. Это поставщик обучающих данных. Медленный, дорогой, но одноразовый.

Лисичка читает про трёх поросят. Айвенго рассказывает про турниры. Чингачгук — про прерии. Каждый своим лицом, своей мимикой. Достаточно, чтобы лицо было антропоморфным — глаза, нос, рот более-менее на своих местах. Совсем абстрактные персонажи пока не работают, но лисички из детских книжек — вполне.

Как склеивать кусочки так, чтобы не было швов

Это главная техническая проблема. Два кусочка видео, выбранные из разных мест библиотеки, могут сильно отличаться по положению лица на стыке. Наивная склейка даёт рывок.

Выбор кандидата

Сначала — выбрать правильный кусочек. Планировщик смотрит вперёд на две секунды, берёт среднее по будущим акустическим признакам и ищет сегмент под будущую фонетику — не под текущую. Это lookahead, и он заметно улучшает синхронизацию: сегмент подобран под то, что будет звучать, а не под то, что уже прозвучало.

Из всей библиотеки берём топ-20 по акустическому сходству. Дальше выбираем лучший стык: у каждого сегмента хранятся миниатюры первого и последнего кадра (64×64 пикселя). Считаем пиксельное расстояние между концом текущего сегмента и началом каждого из двадцати кандидатов. Берём ближайший. Быстро — никаких полных кадров, только крошечные картинки.

Морф через оптический поток

Даже лучший кандидат даст небольшой рывок. Между сегментами строим переход через двунаправленный поток Фарнебека:

flow_AB = Farneback(последний_кадр_A, первый_кадр_B)
flow_BA = Farneback(первый_кадр_B, последний_кадр_A)
for t in [0, 1/N, 2/N, ...]:    warped_A = remap(A, flow_AB * t)    warped_B = remap(B, flow_BA * (1-t))    output   = blend(warped_A, warped_B, t)

Почему двунаправленный? Простой crossfade размывает — пиксели в середине перехода буквально усредняются, получается каша. Двунаправленный поток двигает каждый пиксель по траектории из A в B и одновременно из B в A, а потом смешивает. Переход выглядит как движение, а не как растворение.

Длина морфа адаптивная: чем сильнее отличаются кадры — тем дольше переход, от 5 до 12 кадров. Планировщик минимизирует разницу, поэтому большинство переходов укладываются в 5–7 кадров и почти незаметны.

Что делать в тишине

Персонаж молчит, а мы должны что-то показывать. Случайный сегмент из речевой библиотеки — рот будет двигаться без звука, жуть. Заморозить последний кадр — неестественно.

Решение: отдельная библиотека для пауз. При построении модели генерируем короткое видео, где анимируются только глаза — моргание, движения взгляда, рот закрыт. Во время пауз персонаж переключается на эти сегменты и просто естественно моргает.

Почему это работает на CPU

Каждый шаг намеренно выбран так, чтобы не требовать GPU:

  • Косинусный поиск по тысячам сегментов — умножение 16-мерных векторов, мгновенно

  • Выбор лучшего стыка — MAD по 20 картинкам 64×64, ~80 тысяч операций

  • Оптический поток Фарнебека — классический CV-алгоритм, OpenCV считает на CPU без вопросов

  • Декодирование JPEG и remap — пиксельные операции

Самое тяжёлое — предвычисление оптических потоков между всеми соседними кадрами — можно вынести в отдельный этап подготовки модели. Тогда рендер только применяет готовые карты.

Итог: реальное время, обычный ноутбук, GPU не нужен.

Вместо выводов

Две проблемы — одно решение.

Нейросети медленные? Записываем мимику один раз, воспроизводим быстро. Человека нет? FasterLivePortrait генерирует обучающее видео из картинки — один раз, медленно, с GPU. Дальше тот же быстрый retrieval.

Retrieval вместо generation. Граммофон вместо синтезатора.

Лисичка читает сказки. Дети довольны. Папа может вернуться к работе.