Здравствуйте, меня зовут Дмитрий Карловский и я люблю рисовать шедевры, но у меня совсем не хватает терпения довести хоть один из них до конца.
Ранее я уже показывал вам самописного убийцу Гугл Поиска. Пользуюсь им до сих пор и доволен чистотой выдачи. Теперь же мы сделаем убийцу Artstation для творческих личностей, у которых терпения хватает лишь на несколько минут, за которые надо успеть создать настоящую красоту. И в этом нам помогут нейронные сети.
Далее вас ждёт реверс-инжениринг HuggingFace API для использования модели Kandinsky, поддержка запросов на 100 языках мира благодаря модели Small100, проектирование бесконечной виртуальной ленты в несколько строк кода на $mol и, конечно, примеры творчества Искусственного Художника.
Server API
Итак, первым делом идём на github.com huggingface.com. Там можно найти несколько сотен тысяч разных моделей на любой вкус. Нас же будет интересовать свежая ai-forever/Kandinsky_2.1:
Скачивать мы её, конечно, не будем, а будем использовать из виртуалки в облаке. К счастью, рядом можно найти так называемый спейс, на странице которого можно поиграться с этой моделью:
Ещё одно счастье - этот спейс гоняет нейронки на GPU. Его можно было бы форкнуть и настроить под себя, как я это сделал, например, тут, соединив вместе 3 нейронки:
Но тогда нейронки будут работать на CPU, что медленно и, как показывает практика, менее стабильно. Так что будем использовать то, что есть. Благо, спейс с Kandinsky - не такой звездолёт, как, например, этот:
Код спейса пишется с использованием питоновского фреймворка Gradio, в котором описываются интерфейсы, по которым автоматически строятся REST и WS API, а также формируется фронтенд на Svelte и Tailwind, доступный по ссылкам вида: https://ai-forever-kandinsky2-1.hf.space/.
Не смотря на легковесность Svelte, фронтенд получается весом в пол мегабайта. И до чего я миролюбивый, но того, кто придумал Tailwind, тянущий за собой 200КБ стилей, хочется кастрировать. Дважды, чтобы точно не размножился, ибо кастомизация его стилей - это просто !inhumane
.
Вот сделали бы они фронт на $mol - изменить дизайн можно было бы за считанные минуты вместо нескольких часов, упарываясь солями, чтобы не сойти с ума. Да и получился бы он куда легковесней.
Так же как в $mol, в Gradio интерфейс строится как композиция готовых компонент путём их кастомизации. Средства кастомизации беднее, конечно, зато там есть интересная фича: любой компонент может выступать как поток входных и выходных данных. То есть буквально, в качестве аргументов можно передавать не значения, а другие компоненты, и они будут связаны реактивной связью:
Prompt = gr.Textbox( label="Prompt" )
Image = gr.Image()
Imagine = gr.Button( "Imagine" )
Imagine.click(
imagine,
inputs=[ Prompt ],
outputs=[ Image ],
api_name="imagine"
)
Тут мы создаём текстовое поле, картинку и кнопку, а потом настраиваем кнопку, чтобы она вызывала питоновскую функцию с аргументом взятыми из текстового поля, а возвращаемые ею значения засовывала в картинку.
Если автор кода задал API-имя для кнопки, то привязанная к ней функция будет доступна по REST эндпоинту вида hyoo-translate.hf.space/run/translate, а в подвале фронтенда вы найдёте кнопку, открывающую неплохую автогенерируемую доку по API:
Но у этого API есть одна беда - если запрос будет слишком долгим, то он отвалится по таймауту. А так как это явление не редкое, то мы будем использовать WebSocket API, которым и пользуется сгенерированный Gradio фронтенд. Тут уже нет никаких таймаутов, зато есть сообщения о прогрессе выполнения запроса.
WebSocket API
К сожалению, на WS API никакой документации нет, так что воспользуемся отладчиком и методом научного тыка. Запустив несколько раз генерацию, заметим, что каждый раз устанавливается новое соединение на эндпоинт вида:
wss://ai-forever-kandinsky2-1.hf.space/queue/join
По завершении задачи соединение закрывается. То есть нельзя просто один раз поднять соединение и далее слать RPC запросы. Ок, не осилили, смотрим, что за сообщения там передаются. Первым делом сервер запрашивает у нас хеш:
{ "msg": "send_hash" }
Этот хеш на самом деле просто индентификатор RPC-запроса и может быть любой случайной строкой. Видимо это задел на возможность мультиплексирования множества запросов в одном соединении. И хоть мультиплексирование не поддерживается, нам всё равно придётся носиться с этим хешом в каждом сообщении, как курица с яйцом.
Что ж, клиент отсылает хеш и номер функции:
{
"session_hash": "jnjiyncjiub",
"fn_index": 2
}
Да, по сокету все функции, а точнее кнопки, а ещё точнее обработчики кликов по кнопкам, идентифицируются порядковым номером в коде, а не по имени как в случае REST. Надо ли говорить, что такой API очень хрупкий, и так делать не надо? Ну раз мы встречаем такое в софте от крупной компании, то видимо да, надо: