Демонстрации ИИ повсюду. Один Python-скрипт может заменить лицо на статичном фото, и результат выглядит магически. Но создать что-то, куда реальные люди смогут загружать файлы, отходить от компьютера и возвращаться — это совсем другая задача. Недавно я разработал веб-инструмент, который принимает анимированный GIF и референсное лицо, а затем возвращает ту же анимацию, где лицо заменено на каждом кадре. Модель берет на себя основную визуальную работу, но основные инженерные усилия ушли на архитектуру вокруг нее: поддержание соединений, обработку обновлений страницы и обеспечение того, чтобы двухминутный процесс инференса не исчез из-за ошибки 504 Gateway Timeout.

В этом и заключается разрыв между прототипом и продуктом. Инженеры обожают обсуждать диффузионные архитектуры и параметры инференса. Но когда пользователь загружает тяжелый GIF на двести кадров, никого не волнует ваша модель, если вкладка браузера «отваливается» через тридцать секунд. Инфраструктура вокруг инференса важна так же сильно, как и сам инференс.

Проблема ожидания

Стандартная веб-архитектура предполагает быстрые ответы. Пользователь нажимает кнопку, сервер отвечает, и страница обновляется. Замена лица на десятках кадров GIF мгновенно разрушает это предположение. Модель работает на GPU от Replicate, а не на моем сервере, и обработка длинного GIF может легко занять минуту или больше. Если вы попытаетесь держать HTTP-запрос открытым так долго, вы навлечете на себя неприятности. Балансировщики нагрузки обрывают простаивающие соединения. Браузеры считают, что сеть упала, и пытаются повторить запрос. Пользователи смотрят на застывший индикатор загрузки и думают, что приложение сломалось.

Я полностью избежал этого, отделив процесс отправки от получения результата. Когда пользователь загружает GIF и изображение лица, мой бэкенд на Next.js запускает предсказание на Replicate и мгновенно возвращает job ID. Пользователь сразу получает подтверждение. Реальный результат приходит позже через вебхук, как только завершится работа GPU. Этот паттерн не является экзотическим, но для длительных задач с медиафайлами он абсолютно необходим. Он превращает непредсказуемое ожидание в уверенное «рукопожатие»: задача принята, и вы получите уведомление, когда она будет готова.

Создание надежного конечного автомата

Как только вы переходите на асинхронность, вам нужна прозрачность. Пользователи будут обновлять страницу. Они будут закрывать вкладку и открывать ее снова. Они будут копировать ссылку и отправлять ее коллеге, который проверит ее через два часа. Без надежной записи о том, что произошло, наступит хаос.

В качестве единого источника истины я использовал Supabase. Каждая загрузка создает строку с уникальным job ID, и эта строка проходит через определенные состояния: queued (в очереди), processing (в процессе), succeeded (успешно), failed (ошибка) или expired (истекло). Когда пользователь впервые нажимает «отправить», строка переходит в состояние queued. Как только Replicate принимает предсказание, статус меняется на processing. Вебхук переводит его в succeeded или failed. Я добавил состояние expired для задач, которые слишком долго висят без обратного вызова, чтобы система не преследовала «призраков» бесконечно.

Фронтенд, написанный на TypeScript, опрашивает Supabase через короткие интервалы и отрисовывает то, что говорит текущее состояние. Такой опрос выглядит примитивно, но он полностью решает проблему обновления страницы. Пользователь может закрыть ноутбук, открыть его завтра и увидеть, на каком этапе находится процесс, потому что прогресс хранится в базе данных, а не в памяти браузера. Supabase также отслеживает кредиты, поэтому учет привязан к той же записи задачи, которая отслеживает состояние. Все находится в одном месте.

Работа с файлами без перегрузки браузера

GIF-файлы тяжелее, чем кажется. Файл, идеально подходящий для конвейера ИИ, все равно может весить несколько мегабайт. Рендеринг такого файла в виде зацикленного превью внутри браузера уничтожит производительность, особенно на слабых устройствах. Мне понадобились два совершенно разных конвейера обработки файлов: один для ИИ, другой — для пользовательского интерфейса.

Для превью я конвертирую большие GIF в анимированный WebP с помощью FFmpeg, скомпилированного в WebAssembly. Это работает полностью на стороне клиента в браузере. Результатом является легкое превью, которое сохраняет интерфейс быстрым, не затрагивая оригинальный файл. GIF, отправляемый в Replicate, остается нетронутым. Это разделение имеет значение. Вы не захотите, чтобы артефакты сжатия из превью попали в ваши обучающие данные или финальный рендер, и вы не захотите, чтобы пользовательский интерфейс зависал на многомегабайтном объекте, пока ИИ еще «думает».

FFmpeg WASM также выполняет другие задачи с GIF еще до того, как файл покинет браузер. Я использую его для подсчета количества кадров, проверки размеров и раннего обнаружения поврежденных файлов. Обнаружение проблемы до того, как она потребит кредиты GPU, экономит и деньги, и терпение пользователя.

Как превратить демо-версию в программное обеспечение, которому доверяют

Здесь кроется более важный урок, применимый почти к любому инструменту генеративного ИИ на рынке. Сама модель может составлять лишь тридцать процентов работы. Остальные семьдесят процентов — это неброская «внутренняя механика», о которой никто не пишет в Твиттере: восстановление состояния, подписи вебхуков, конвертация файлов, отслеживание кредитов и корректная обработка ошибок.

Мой стек прост и продуман. Next.js и TypeScript отвечают за интерфейс и API-маршруты. Replicate запускает модели. Supabase управляет состояниями, данными и кредитами. FFmpeg WASM выполняет работу с медиафайлами на стороне клиента. Анимированный WebP обеспечивает быстродействие интерфейса. У каждого компонента одна задача, и они взаимодействуют через явные переходы состояний, а не через хрупкие, длительные запросы.

Когда пользователь тратит кредиты на замену лица, он ожидает надежности, а не научной статьи. Если генерация не удалась, система должна это понять и сообщить об этом. Если пользователю приходится ждать, у него должно быть легкое превью для просмотра и статус, который сохраняется даже после перезапуска браузера. Эти детали незаметны, когда они работают, и фатальны, когда нет.

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