Этот API мало что может сделать
Когда дело доходит до стабильной диффузии, модель важна, но фактическое золото всегда под рукой. Чем лучше вы напишете подсказку, тем больше у вас будет контроля над результирующим изображением и, как правило, тем выше качество.
Вот почему некоторые пользовательские интерфейсы, которые работают поверх Stable Diffusion, пытаются упростить это, упрощая пользователям написание своих подсказок, а затем запуская множество оптимизаций за кулисами. Предоставление пользователям лучших результатов с меньшими усилиями с их стороны.
В моем случае я создавал VirtualSnap (студию для фотосъемки продуктов на SD-карте) уже пару месяцев и заметил, что небольшая оперативная оптимизация, которую я сделал, не урезала его.
Поэтому я решил добавить немного «интеллектуальности» в смесь, и вот как я это сделал.
Первоначальная оптимизация
VirtualSnap — это приложение NextJS, вы можете увидеть полный код здесь, это открытый исходный код.
И первоначальная «оптимизация подсказки», которую я добавил, предназначалась для того, чтобы пользователи не думали о фактической подсказке. Вместо этого я просто хотел, чтобы они описали продукт, который фотографировали.
Как видно на скриншоте ниже, пользователь просто описывает товар как «серебряное кольцо с тиснением в виде солнца». Однако это не та подсказка, которую получает модель SD.
Фактическая подсказка - это что-то вроде строк:
«шедевр, фотография продукта серебряного кольца с тиснением в виде солнца, (крупный план: 1,8), по центру: 1,9 (в кадре: 1,9), боке, 8k uhd, dslr, мягкое освещение (высокое качество: 1,8), Fujifilm XT3”
Обратите внимание на раздел, выделенный жирным шрифтом, это то, что на самом деле ввел пользователь, остальное добавляется следующим кодом:
let PROMPT_TEMPLATES = { 'normal': "masterpiece, product photography of [input], [environment], [shot_type], centered:1.9 ,(in frame:1.9),bokeh, 8k uhd, dslr, soft lighting, (high quality:1.8), Fujifilm XT3", 'normal_wide_shot': "masterpiece, high quality photo of [input], [environment], [shot_type], centered:1.9 ,(in frame:1.9),bokeh, 8k uhd, dslr, soft lighting, (high quality:1.8), Fujifilm XT3", 'person': "masterpiece, detailed photo of (a person using [input]):1.8,[environment], product photography, [shot_type] , centered:1.9 ,(in frame:1.9), bokeh, 8k uhd, dslr, soft lighting, high quality, film grain, Fujifilm XT3", } const FIXED_NEGATIVES = "blur, haze, nsfw, naked, low quality" function parseEnvRequirements(env) { const envMapping = { "random": "", "livingroom": "inside the livingroom of a house", "bedroom": "inside the bedroom of a house", "backyard": "in the backyard of a house, trees, fence", "nature": "out in the woods, trail in nature", "table": "on top of a table", "cube": "on top of a cube", "plain": "no environment, plain background", } if(envMapping[env]) { return envMapping[env] } return "" } function getShotType(shotType) { if(shotType == 'shot-type-wide') { return '(wide shot:1.8)' } if(shotType == 'shot-type-close') { return '(closeup shot:1.8)' } if(shotType == 'shot-type-extrawide') { return '(extra wide shot:1.8)' } } function getPromptTemplateIndex(usedByPerson, shotType) { if(usedByPerson) { return 'person' } if(!shotType) return 'normal'; if(shotType.indexOf("wide") != -1) { return 'normal_wide_shot' } return 'normal' } async function getFinalPrompt({prompt, usedByPerson, shotType, env}, user_id) { const productNameRegExp = /\{([a-zA-Z 0-9]+)\}/g; let index = getPromptTemplateIndex(usedByPerson, shotType) let text = PROMPT_TEMPLATES[index] .replace("[input]", prompt) .replace("[environment]", parseEnvRequirements(env)) .replace("[shot_type]", getShotType(shotType)) let ret = { prompt: text, lora: [] } let count = 1; let matches = null; while( (matches = productNameRegExp.exec(text)) !== null) { let lora_url = await getLoraURL(matches[1], user_id) ret.prompt = ret.prompt.replace(matches[0], `<${count}>`) ret.lora.push(lora_url) count++; } return ret; }
Там много всего происходит, но, по сути, я оборачиваю подсказку пользователя своей собственной, более подробной. Я рассказываю о таких вещах, как тип снимка, тип фотографии и среда (если есть), в которой находится продукт.
Но этого недостаточно, это оставляет продукт во власти качества модели, которую я использую. Я уже использую отличную SD-модель, но опять же, модели можно дополнительно настроить с помощью LORA.
Но вы должны выбрать прямо сейчас, поэтому давайте посмотрим, как мы можем это сделать.
Что же такое LORA?
Не вдаваясь в подробности — потому что, в конце концов, я здесь не эксперт — LORA — это небольшие файлы (по сравнению с файлами модели размером в ГБ, LORA, как правило, имеют размер всего несколько КБ), которые очень конкретная информация о человеке, объекте или стиле.
Таким образом, вы можете использовать эти LORA, чтобы, например, представить себя на картинке (например, если вы тренируете LORA на своем лице) или взять существующий объект и поместить его в другое место, как я сделал с этой игрушечной машиной, которая внезапно теперь припаркован посреди шоссе в окружении природы.
Я хочу попробовать использовать определенные LORA для улучшения качества фотографий, которые генерирует VirtualSnap, используя LORA определенного стиля, например, специально обученные для изображений еды, другие для ювелирных изделий и т. д.
Проблема здесь заключается в том, чтобы понять, как анализировать ввод пользователя, чтобы понять, что он пытается показать, таким образом, чтобы он мог адаптироваться к множеству различных способов сказать одно и то же.
Вот где OpenAI API очень пригодится!
Общая категоризация подсказок с помощью нескольких строк кода
Не буду врать, моей первой попыткой было попробовать использовать NLP-библиотеку для Node, что-то вроде Natural. Но результаты не были хорошими из коробки, мне нужно было много настраивать его, что было неприятно, учитывая, что я мог просто дать подсказку ChatGPT и попросить ее сказать мне, о чем подсказка.
И тут меня осенило. Почему бы мне не использовать для этого ChatGPT! Или, скорее, следующая лучшая вещь: один из LLM, которые предоставляет OpenAI.
Я использовал API в прошлом, и я знал, насколько это просто, поэтому быстро запустить пример кода не составило труда.
Все, что мне нужно было сделать, это:
- Установите пакет OpenAI npm с помощью
npm i openai
- Получите новый ключ API в разделе «Ключи API» OpenAI на странице настроек учетной записи:
- Используйте следующий код с API:
const { Configuration, OpenAIApi } = require("openai"); const configuration = new Configuration({ apiKey: process.env.OPENAI_API_KEY, }); const openai = new OpenAIApi(configuration); async function analyzePrompt(prompt) { const response = await openai.createCompletion({ model: "text-davinci-003", prompt: <the actual prompt for the API> temperature: 0, max_tokens: 60, top_p: 1.0, frequency_penalty: 0.5, presence_penalty: 0.0, }); let resp = response.data.choices[0].text const topics = resp.split("\n")[2].replace("Topics: ", "").split(",").map( t => t.trim().toLowerCase()) return topics; }
По сути, именно так вы используете OpenAI API. Я сохранил ключ API в переменной env с именем OPENAI_API_KEY
(которую в Next.js можно легко сохранить в файле .env
, который загружается автоматически).
Затем вы используете метод createCompletion
, который используется, чтобы задавать вопросы или просить модель завершить ваш текст.
Выбрана модель text-davinci-003
, которая представляет собой модель типа GPT3, которая идеально подходит для завершения текста.
Обратите внимание: если вы делаете что-то вроде чат-бота, модель
gpt-3.5-turbo
лучше и дешевле.
Из всех остальных параметров (имеющих значения по умолчанию) нас больше всего волнует temperature
, потому что, установив его на 0, мы делаем модель максимально детерминированной. Это означает, что для одного и того же ввода мы в основном получим один и тот же вывод. Эти модели не являются детерминированными по своей природе, поэтому получить 100% детерминированное поведение практически невозможно. Тем не менее, мы достаточно близки к этому 0.
Теперь остальная часть волшебства происходит по фактической подсказке, которую мы даем.
Помните, что я сказал в начале, подсказка — это самое важное, и это также относится к LLM.
Поскольку у меня есть ограниченное количество LORA, которые я хочу применить в зависимости от темы, я собираюсь попросить эту модель попробовать и выбрать одну из потенциальных тем на основе ввода пользователя.
И я собираюсь написать это так:
«Проанализируйте следующую подсказку и постарайтесь сопоставить как можно больше из следующих категорий: животные, украшения, мебель, еда, портрет, брошенный и выстрел в голову. Если вы не можете понять это, сообщите мне об этом и используйте следующий шаблон для своего ответа: \n Темы: [список тем здесь] \n Элементы: [список элементов здесь] . \n Предложение: "+ подсказка + " ",
Это большое приглашение, но, по сути, я делаю следующее:
- Я контролирую именно то, что дает мне модель (потому что она дает мне только одну из известных категорий).
- Я даю ему формат для ответа, поэтому я могу легко разобрать его позже.
Вывод для этой модели примерно такой:
Вы можете увидеть предложение там, моя подсказка была "золотое кольцо с мордой льва, крупный план, блестящие отражения".
И он смог сопоставить его с «ювелирными изделиями» и «животными». Затем я могу использовать это, чтобы выбрать подходящие LORA для стиля.
Если вы вернетесь к функции analyzePrompt
, то заметите, что последние несколько ее строк анализируют свойство text
. По сути, я получаю строку для «Темы» и превращаю ее в массив тем.
С помощью этого массива я могу изменить функцию getFinalPrompt
, которую мы уже видели, чтобы она выглядела следующим образом:
const _LORAS_ = { "jewelry": { url: "https://replicate.delivery/pbxt/k7QiKIESozpwOBeOSGS00sqlxmVzeuruvmlg8yfpx2ieE5hDB/tmpubksipo2training-jewelryzip.safetensors", //url: "https://civitai.com/api/download/models/32976", weight: 1.0 }, "abandoned": { url: "https://civitai.com/api/download/models/58490", weight: 1.0 }, "food": { url: "https://civitai.com/api/download/models/49946", weight: 1.0 }, "portrait": { url: "https://civitai.com/api/download/models/53221", weight: 1.0 }, "headshot": { url: "https://civitai.com/api/download/models/53221", weight: 1.0 } } export async function getFinalPrompt({prompt, usedByPerson, shotType, env}, user_id) { const productNameRegExp = /\{([a-zA-Z 0-9]+)\}/g; let index = getPromptTemplateIndex(usedByPerson, shotType) let text = PROMPT_TEMPLATES[index] .replace("[input]", prompt) .replace("[environment]", parseEnvRequirements(env)) .replace("[shot_type]", getShotType(shotType)) let ret = { prompt: text, lora: [] } let count = 1; let matches = null; while( (matches = productNameRegExp.exec(text)) !== null) { let lora_url = await getLoraURL(matches[1], user_id) ret.prompt = ret.prompt.replace(matches[0], `<${count}>`) ret.lora.push({url: lora_url, weight: DEFAULT_LORA_SCALE}) count++; } //ADDED CODE HERE let promptTopics = await analyzePrompt(prompt) promptTopics.forEach( t => { if(_LORAS_[t]) { ret.prompt += `,in the style of <${count}>,` ret.lora.push({url: _LORAS_[t].url, weight: _LORAS_[t].weight}) count++; } }) return ret; }
Я определил константу _LORAS_
, которая содержит всю информацию о LORA, которые я хочу использовать. И теперь последние несколько строк функции будут проходить через обнаруженные темы, и если они определены внутри этой новой константы, они будут добавлены в массив LORA (свойство lora
объекта ret
).
Мы будем использовать их, чтобы позже отправить запрос в Replicate, чтобы модель могла работать на их платформе.
Примечание: параметр
weight
здесь важен, потому что он влияет на то, насколько новый стиль перезапишет стиль модели по умолчанию. Вам придется играть с числом от 0,1 до 1, пока не найдете правильную комбинацию.
Понравилось ли вам то, что вы прочитали? Подпишитесь на мой БЕСПЛАТНЫЙ информационный бюллетень, где я делюсь со всеми своим 20-летним опытом работы в ИТ-индустрии. Присоединяйтесь к Бродяге старого разработчика!
Вот и все.
API OpenAI решил проблему, которая в противном случае заняла бы у меня больше времени, чем я был готов потратить, пробуя различные библиотеки и методы НЛП, пока не нашел идеальную комбинацию.
Если подумать, этот тип интерфейса является революционным в том смысле, что с одной конечной точкой они могут делать практически все. Вместо того, чтобы предоставлять предварительно определенный набор конечных точек, хорошо задокументированных и ограниченных в своей области, они предоставляют единую конечную точку, которая может понимать любые ваши запросы и делать все, что вы от нее хотите.
Я с нетерпением жду возможности увидеть больше API, которые ведут себя таким образом в будущем!
А вы?!