![](https://habrastorage.org/getpro/habr/upload_files/4d4/a9e/b26/4d4a9eb2669affa814c38cf7689e031f.png)
Введение
Эта статья продолжение первой части. Мы подробно рассмотрим систему модерации Midjourney, коснемся лимитов скорости (rate limits) Discord, двух самых сложных тем. В конце статьи приведен рабочий код.
Прежде чем мы начнем, убедитесь, что у вас есть учетная запись Discord с активной подпиской Midjourney. Базового плана за $10 будет достаточно.
Следуйте этим простым шагам, чтобы получить:
Идентификатор сервера Discord, который будет упомянут в этой статье как
server_id
Идентификатор канала Discord, который будет упомянут в этой статье как
channel_id
Токен Discord, который будет упомянут в этой статье как
discord_token
Лимит скорости (rate limits) Discord
Оригинальная документация Discord предлагает использовать HTTP заголовки для определения случаев, когда лимит скорости превышен, и соответствующе корректировать действия. Этот подход необходим для сложных или коммерческих приложений, в этой статье мы будем использовать более простой, но не менее эффективный метод.
Мы добавим паузу в 350 миллисекунд перед каждым вызовом API Discord. Поскольку наш код работает с одним потоком, это будет достаточно для того, чтобы оставаться в пределах лимита. Общая производительность нашей небольшой программы не пострадает, поскольку время ответа Midjourney гарантированно превосходит предложенные 350 миллисекунд.
Система модерации Midjourney
Midjourney выполняет как пре-модерацию так и пост-модерацию команд /imagine, вам потребуется надежный способ обнаружения обоих случаев.
При выполнении POST-запроса с командой /imagine в канале Discord (как описано в первой части), вы столкнетесь с семью возможными случаями:
Успешное выполнение
Через несколько секунд размещенное сообщение появится в канале с исходным идентификатором сообщенияid
, статусом(Waiting to start)
в полеcontent
и с полемtype
установленным в 0.GET
https://discord.com/api/v10/channels/channel_id/messages
[
{
"id": "<Discord original message id>",
"type": 0,
"content": "**Wonders of the World…** - <@Discord user id> (Waiting to start)",
"channel_id": "<Discord channel id>",
Как только Midjourney завершит генерацию, исходное сообщение будет удалено, Midjourney разместит окончательное сообщение с новым идентификатором id
содержащим результаты генерации.
![](https://habrastorage.org/getpro/habr/upload_files/8c8/eb9/ddf/8c8eb9ddf6669cf26fa79ac549427d0d.jpg)
GET https://discord.com/api/v10/channels/channel_id/messages
[
{
"id": "<Discord final message id>",
"type": 0,
"content": "**Wonders of the World…** - <@Discord user id> (fast)",
"channel_id": "<Discord channel id>",
Пре-модерация
Это самый простой случай: размещенное сообщение никогда не появится в канале. Вы увидите что-то подобное этому.
![](https://habrastorage.org/getpro/habr/upload_files/3d8/9b0/071/3d89b0071690320af300a8a69b94e53d.png)
Сообщения, помеченные как “Only you can see this” (Только вы можете видеть это), могут быть получены с помощью Discord Gateway API.
Пост-модерация
Пост будет появляться в канале так же, как в случае "Успешного выполнения".
![](https://habrastorage.org/getpro/habr/upload_files/ad9/bf0/3a6/ad9bf03a68cea9e579c0925c40a9f060.png)
GET https://discord.com/api/v10/channels/channel_id/messages
[
{
"id": "<Discord original message id>",
"type": 0,
"content": "**Beautiful lady swimming in the pool…** - <@Discord user id> (Waiting to start)",
"channel_id": "<Discord channel id>",
Генерация может даже начаться, и вы увидите прогресс, но внезапно остановится с сообщением, похожим на следующее.
![](https://habrastorage.org/getpro/habr/upload_files/4ea/4e7/1ae/4ea4e71aead8486cd3d363dfebdb1683.png)
GET https://discord.com/api/v10/channels/channel_id/messages
[
{
"id": "<Discord original message id>",
"type": 0,
"content": "**Beautiful lady swimming in the pool…** - <@Discord user id> (Stopped)",
"channel_id": "<Discord channel id>",
Эфемерная модерация
Некоторые слова в вашем запросе могут вызвать "мягкую модерацию". Генерация начинается и успешно завершается, но после завершения сообщение будет отображаться как "Original message was deleted" (Исходное сообщение было удалено). Сообщение больше не возвращается запрос GEThttps://discord.com/api/v10/channels/channel_id/messages
, даже если изначально оно было. Пример ниже.
![](https://habrastorage.org/getpro/habr/upload_files/f5a/1c4/9fc/f5a1c49fc607af99c137f249991a4802.png)
GET https://discord.com/api/v10/channels/channel_id/messages
[
{
"id": "<Discord original message id>",
"type": 0,
"content": "**Irresistibly beautiful woman, pinup…** - <@Discord user id> (Waiting to start)",
"channel_id": "<Discord channel id>",
Сгенерированное сообщение было удалено (Original message was deleted).
![](https://habrastorage.org/getpro/habr/upload_files/ca2/edc/63f/ca2edc63f5a34dd4b8f7e9e284c07c6a.jpg)
Сообщения, помеченные как “Only you can see this” (Только вы можете видеть это), могут быть получены с помощью Discord Gateway API.
Неверный запрос
Это происходит когда вы указываете неправильный параметр. Как и в случае пре-модерации размещенное сообщение не появится в канале. Рекомендуется добавить код для проверки синтаксиса запроса перед его размещением. Мы планируем рассмотреть тему проверки синтаксиса запроса в одной из наших последующих статей.
![](https://habrastorage.org/getpro/habr/upload_files/5ed/e0f/7d1/5ede0f7d1bd2cca64fe4e6aa0310da47.png)
Сообщения, помеченные как “Only you can see this” (Только вы можете видеть это), могут быть получены с помощью Discord Gateway API.
Сообщение в очереди
В зависимости от вашего плана подписки на Midjourney у вас может быть от 3 (Basic & Standard) до 15 (Pro & Mega) одновременных выполнений заданий. Как только этот лимит достигнут, запросы на выполнение заданий будут поставлены в очередь и сообщения, связанные с этими заданиями, не будут немедленно появляться в GEThttps://discord.com/api/v10/channels/channel_id/messages
до тех пока Midjourney не завершит выполнение одного из активных заданий. Фактически это означает, что задание было помещено во внутреннюю очередь Midjourney.
![](https://habrastorage.org/getpro/habr/upload_files/959/5c7/f36/9595c7f3668dc698d9496e1266823a06.png)
Сообщения, помеченные как “Only you can see this” (Только вы можете видеть это), могут быть получены с помощью Discord Gateway API.
Желательно минимизировать или полностью исключить случаи "Сообщение в очереди", поскольку существует значительная вероятность того, что Midjourney может ограничивать ваши запросы на генерацию в таких случаях и использовать это как индикатор нарушения условий использования Midjourney (…may not use automated tools to access, interact with, or generate Assets through the Services… | …нельзя использовать автоматические средства для доступа, взаимодействия или генерации ресурсов через услуги…).
Очередь переполнена
Внутренняя очередь задач Midjourney может вмещать до 10 задач, после чего вы получите сообщение "Очередь переполнена". Вам придется подождать и попробовать снова.
![](https://habrastorage.org/getpro/habr/upload_files/4b0/1c1/2d8/4b01c12d8c69d0bde19823baf8387216.png)
Сообщения, помеченные как “Only you can see this” (Только вы можете видеть это), могут быть получены с помощью Discord Gateway AP
Желательно минимизировать или полностью исключить случаи "Очередь переполнена", поскольку существует значительная вероятность того, что Midjourney может ограничивать ваши запросы на генерацию в таких случаях и использовать это как индикатор нарушения условий использования Midjourney (…may not use automated tools to access, interact with, or generate Assets through the Services… | …нельзя использовать автоматические средства для доступа, взаимодействия или генерации ресурсов через услуги…).
Discord REST API (HTTPS) vs Discord Gateway API (WebSocket)
Как вы заметили в предыдущем абзаце, некоторые сообщения можно извлечь с использованием Discord Gateway API. В отличие от Discord REST API, который мы используем в этой статье, Discord Gateway API требует использования WebSocket API для обмена данными в реальном времени между вашим клиентским приложением и сервером Discord. Это добавляет еще один уровень сложности и, что более важно, может увеличить вероятность бана.
В подавляющем большинстве случаев вы можете создать алгоритм без использования Discord Gateway API. Мы планируем рассмотреть крайние случаи, когда использование Discord Gateway API является единственным решением, в последующих статьях.
Логика генерации с помощью Midjourney /imagine
Учитывая информацию, полученную из предыдущих абзацев, вы можете реализовать следующую простую стратегию:
Убедитесь, что у вас есть хотя бы один свободный слот (job). Для этого вы можете использовать либо отдельную учетную запись Midjourney для всех ваших автоматизированных задач, либо тщательно управлять количеством вручную запущенных задач, ограничив их до максимум двух. Этот подход поможет избежать как случаев "Сообщение в очереди", так и случаев "Очередь переполнена".
Перед размещением /imagine запроса выполните GET
https://discord.com/api/v10/channels/channel_id/messages
, чтобы получить идентификаторid
первой записи. По умолчанию этот API возвращает 50 последних сообщений из канала, упорядоченных в порядке убывания, с самым свежим сообщением в начале списка.После размещения /imagine запроса с использованием POST
https://discord.com/api/v10/interactions
, продолжайте проверять канал каждые 3-5 секунд, выполняя GEThttps://discord.com/api/v10/channels/channel_id/messages
. Сравнивайте идентификаторid
первой записи с значением, полученным на предыдущем этапе. Если через 30 секунд вы не видите новой записи в канале, можно предположить, что у вас либо случай пре-модерации, либо "Неверного запроса", и ваш запрос, возможно, требует пересмотра.Как только вы определите, что новое сообщение действительно появилось в вашем канале, сохраните идентификатор
id
этого сообщения. Продолжайте выполнять GEThttps://discord.com/api/v10/channels/channel_id/messages
каждые 20-30 секунд, чтобы проверить следующие оставшиеся случаи:Успешное выполнение: когда генерация завершена, сообщение с исходным идентификатором
id
больше не присутствует в канале. Вместо него присутствует новое сообщение с непустым массивом компонентовcomponents
. Сгенерированное изображение можно получить из поляattachments[0].url
.Пост-модерация: когда сообщение с исходным идентификатором
id
все еще присутствует, и его полеcontent
заканчивается на(Stopped)
.Эфемерная модерация: когда идентификатор
id
первой записи совпадает с значением, полученным до размещения изображения-запроса.
Этот алгоритм позволит вам эффективно управлять процессом генерации с помощью Midjourney /imagine, учитывая различные все возможные сценарии.
JavaScript код реализующий алгоритм описанный выше
// bash
// USEAPI_DISCORD="..." USEAPI_SERVER="..." USEAPI_CHANNEL="..." node index.js
// Node 18+
const imagine_prompt = "Hologram cat in neon lights";
const discordAPI = `https://discord.com/api/v10`;
const MidjourneyAppId = `936929561302675456`;
// Load Discord settings from environment
const discord = process.env.USEAPI_DISCORD ?? 'Discord token';
const server = process.env.USEAPI_SERVER ?? 'Discord server id number';
const channel = process.env.USEAPI_CHANNEL ?? 'Discord channel id number';
console.info({ discord, server, channel, imagine_prompt });
const sleep = (ms = 0) => new Promise(resolve => setTimeout(resolve, ms));
const DiscordHeaders = (token) => ({
"Content-Type": "application/json",
"Authorization": `${token}`,
});
// https://discord.com/developers/docs/resources/channel#get-channel-messages
const GetDiscordChannelMessages = async (discord, channel) => {
const response = await fetch(
`${discordAPI}/channels/${channel}/messages`,
{ headers: DiscordHeaders(discord) });
return response;
}
// Midjourney Imagine https://discord.com/api/v10/channels/channel_id/application-commands/search?type=1&include_applications=true&query=imagine
const PostDiscordImagine = async (discord, server, channel, prompt) => {
const data = {
"type": 2,
"application_id": MidjourneyAppId,
"guild_id": server,
"channel_id": channel,
"session_id": (new Date()).getTime(),
"data": {
"version": "1166847114203123795",
"id": "938956540159881230",
"name": "imagine",
"type": 1,
"options": [
{
"type": 3,
"name": "prompt",
"value": prompt
}
],
"application_command": {
"id": "938956540159881230",
"application_id": MidjourneyAppId,
"version": "1166847114203123795",
"default_permission": true,
"default_member_permissions": null,
"type": 1,
"nsfw": false,
"name": "imagine",
"description": "Create images with Midjourney",
"dm_permission": true,
"options": [
{
"type": 3,
"name": "prompt",
"description": "The prompt to imagine",
"required": true
}
]
},
"attachments": []
}
};
const response = await fetch(`${discordAPI}/interactions`, {
method: "POST",
body: JSON.stringify(data),
headers: DiscordHeaders(discord)
});
return response;
}
const demo = async () => {
const getBeforeMessages = await GetDiscordChannelMessages(discord, channel);
if (getBeforeMessages.status !== 200) {
console.error(`Discord /messages status ${getBeforeMessages.status}`, await getBeforeMessages.json());
process.exit(1);
}
const beforeMessages = await getBeforeMessages.json();
const beforeIds = new Set();
beforeMessages.forEach(msg => beforeIds.add(msg.id));
await sleep(350);
const postImagine = await PostDiscordImagine(discord, server, channel, imagine_prompt);
if (postImagine.status != 204) {
console.error(`Discord /interactions status ${postImagine.status}`, await postImagine.json());
process.exit(1);
}
const maxPostedAttempts = 10;
let attempt = 1;
let postedMessage;
// Check for message to appear in the channel for 20 seconds total
do {
await sleep(2000);
const getMessages = await GetDiscordChannelMessages(discord, channel);
if (getMessages.status !== 200) {
console.error(`Discord /messages status ${getMessages.status}`, await getMessages.body());
process.exit(1);
}
const messages = await getMessages.json();
// New (Waiting to start) imagine interaction
postedMessage = messages.find(msg => !beforeIds.has(msg.id));
if (postedMessage) {
console.log(`Found new message ${postedMessage.id}`, postedMessage.content);
break;
}
attempt++;
} while (attempt <= maxPostedAttempts);
if (postedMessage === undefined) {
console.error(`Posted message not found due to moderation or an invalid prompt`);
process.exit(1);
}
const termStopped = `> (Stopped)`;
const maxGeneratedAttempts = 60;
attempt = 1;
let generatedMessage;
// Check for posted message to appear in the channel with new id for 10 minutes max
do {
// Wait for 10 seconds before checking on message progress
await sleep(10000);
const getMessages = await GetDiscordChannelMessages(discord, channel);
if (getMessages.status !== 200) {
console.error(`Discord /messages status ${getMessages.status}`, await getMessages.body());
process.exit(1);
}
const messages = await getMessages.json();
const progress = messages.find(message =>
message.id == postedMessage.id &&
// Not completed
!message.components?.length &&
// Not stopped
!message.content.endsWith(termStopped));
if (progress) {
console.log(`#${attempt} ${progress.id} progress`, progress.content);
} else {
const completed = messages.find(message =>
!beforeIds.has(message.id) &&
message.id !== postedMessage.id &&
// Either completed or stopped
(!!message.components?.length || message.content.endsWith(termStopped))
);
generatedMessage = (completed && !!completed.components?.length) ? completed : undefined;
break;
}
attempt++;
} while (attempt <= maxGeneratedAttempts);
if (generatedMessage === undefined) {
console.error(`Message not found due to post-moderation or ephemeral moderation`);
process.exit(1);
}
// Successful generation
console.info(`Completed`, generatedMessage.content);
console.info(`Download URL`, generatedMessage.attachments[0].url);
}
demo();