Этот 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, которые ведут себя таким образом в будущем!

А вы?!