見出し画像

スマホでAIイラスト制作:DiscordからローカルStable Diffusionを操作する方法

全国のにじジャーニーで元絵をつくりSDモデルでimg2imgしてAIイラストを制作している皆さんこんばんは。花笠万夜です。

本日はDiscord上で、その「にじジャーニーで元絵をつくりSDモデルでimg2imgしてAIイラストを制作する」工程を完結させる方法を編み出したので紹介しますね。img2imgだけじゃなくてtxt2img機能も含めてるから、にじ使いじゃない人でも使えると思いますよ!

作ってるうちに機能が変わってきたので結論として
・Discordでテキストを入力するとテキスト画像生成(txt2img)
・Discordで画像とテキストを入力すると画像から画像生成(img2img)

ができるようになるよって記事です。

こういうのがスマホのDiscord内で作れる

※にじジャーニーの使い方は全世界民がすでに知ってると思うから省略するよ。
※コマンドプロンプトの基本的な使い方は先に理解すると良いよ!
※自宅PCをつけっぱなしにするよ。PCにはStable Diffusion webUIがいるよ。
※公開しないbot、個人利用のDiscord鯖を想定してるのでセキュリティはザルだよ!
※相変わらず、コマンドプロンプトをかろうじて使えるbotなんか作ったこともない人間がChatGPTに聞きまくってなんとか実現させる記事だよ。たぶんもっとスマートなやり方はたくさんあるよ。


Discordボットの基本的なセットアップ

さあ、Discordボット作りの旅に出発しましょう!
最初はめんどくさいけど、その後の達成感は最高だからね!

ステップ1: Discord Developer Portalにアクセス

最初にやることは、Discord Developer Portalにブラウザでアクセスすること。URLはこちら→ Discord Developer Portal。ログインしてないなら、さっさとログインしよう。

ステップ2: 新しいボットを作成

画面右上の「New Application」を押してね。ボットにテンションの上がる名前をつけて、「Create」をクリック。
General Informationの情報を適当に埋めよう。
(注:URL系は入力しなくても大丈夫じゃないかな、とりあえず私のは埋めてないけど動いてるので)

やる気のないアイコン

ステップ3: ボットを追加

作成したアプリケーションの設定画面に移動して、「Bot」タブを選択。
ここからTOKENを取得!出てこない人は「Reset Token」を押す!

初回、TOKENでなくない?とりあえずReset Token押しちゃえ〜

ステップ4: トークンをゲット

そのトークンをコピーするんだ。これが大事!
でも絶対に他人には見せないように。
ここがボットの心臓部だからね。

同じ画面のAuthorization Flowの設定はこうだ。
・PUBLIC BOTはオフ。この用途は普通、他人に使われると困るので。
・REQUIRES OAUTH2 CODE GRANTはオフ。
Privileged Gateway Intentsの設定はこう。
・PRESENCE INTENT:オン
・SERVER MEMBERS INTENT:オン
・MESSAGE CONTENT INTENT:オン
(注:この設定まわりはよくわかってません、このへんは他の人の技術ブログも目を通してください)

ステップ5: botをDiscordに招待

  • OAuth2 > URL Generator" タブに移動!

  • 「scopes」下にある「bot」チェックボックスを選択

  • 「Bot Permissions」からBotの機能に必要な権限を選択
    (私はあんまりわかってないのでadministratorにしたけどちゃんと調べたほうがいいかも)

  • 一番下に生成されたURLでBotをサーバーに追加できる!
    URLをブラウザで表示して、招待したいサーバーを選択してね。

Administratorでいいのかは自信がない…

ステップ6: Discord.jsをインストール

PCにNode.jsが入ってないなら、先にインストールしておこう。それから、使い勝手の良いコードエディターで新しいプロジェクトを作成。コマンドプロンプトを開いて、次のコマンドでDiscord.js(最新版 v.14)をインストール。

Node.js等各インストール:

  • Node.jsの公式サイト(https://nodejs.org/)からインストーラーをダウンロードし、インストールを行いましょう。基本的に安定版を意味する「LTS」で良いかと思います。

  • その後、今回の環境をいれる場所にまでコマンドプロンプトのカレントディレクトリを移動します。cd 〜ってやつね。ここ分からなければ「コマンドプロンプト カレントディレクトリ」ググっとくと良いよ。
    変なところにインストールするとあとで泣くよ。私は泣いた。

  • その後下記を順に実行して、
    ・typescript(コードを書くのが楽になる!いらんかも…)
    ・canvas(画像サイズ取得など画像系に便利)
    ・node-fetch(nodeでfetchが使える!何のことかわからん!)
    ・discord.js(本日のメイン)
    を使えるようにしましょう。

npm init -y
npm install typescript --save-dev
npm install node-fetch
npm install canvas
npm install discord.js

TypeScriptのコンパイル設定:

  • プロジェクトのルートディレクトリで、新しいテキストファイルを作成し、ファイル名をtsconfig.jsonとする。ここでTypeScriptのコンパイルオプションを設定。

{
  "compilerOptions": {
    "target": "es6", // コンパイルされるJavaScriptのバージョン
    "module": "commonjs", // モジュールシステム
    "outDir": "./dist", // コンパイルされたファイルが出力されるディレクトリ
    "strict": true, // 厳格な型チェック
    "esModuleInterop": true, // ESモジュールとの互換性を持たせる
    "skipLibCheck": true, // 定義ファイルのチェックをスキップ
    "forceConsistentCasingInFileNames": true // ファイル名の大文字小文字を一貫させる
  },
  "include": ["./src/**/*"], // コンパイルするファイルを指定
  "exclude": ["node_modules"] // 除外するファイルを指定
}

それを保存してターミナルでtscコマンドを実行。

tsc

これにより、TypeScriptファイル(.ts)がJavaScript(.js)にコンパイルされる!
(注:GPTに言われるまま作業してるが、今考えると結局TypeScriptファイル使ってない気がするな…)


Discord botを動かす基本コードを書く
※検証だけなので飛ばしていいよ

プロジェクトフォルダにsd-discord-bot.js(名前は何でもいいけど)を作って、以下のような基本的なコードを書く。これでボットの骨組みは完成!
BOTのTOKENは打ち替えてください。

// Discordライブラリをインポート
const { Client, GatewayIntentBits } = require('discord.js');
// 新しいDiscordクライアントを作成し、必要なIntentsを指定
const client = new Client({
    intents: [
        GatewayIntentBits.Guilds,
        GatewayIntentBits.GuildMessages,
        GatewayIntentBits.MessageContent // メッセージ内容にアクセスするために必要
    ]
});

// ボットが準備完了したらコンソールにメッセージを出力
client.once('ready', () => {
    console.log('Ready!');
});

// メッセージイベントをリッスン
client.on('messageCreate', message => {
    // メッセージの内容が "!ping" なら "Pong!" と返答
    if (message.content === '!ping') {
        message.channel.send('Pong!');
    }
});
// ここにあなたのDiscordボットのトークンを入れます
client.login('あなたのボットトークン');

こいつを動かすためにコマンドプロンプトで下記を入力。

node sd-discord-bot.js

これでコマンドプロンプトでready!と表示され、
botがDiscord上でオンラインになるはず〜!

こういう感じ

そしてbotを招待したDiscord鯖で「!ping」とだけ投稿しよう。
うまくいってればbotが「Pong!」と返すはず〜。

こういう感じ

動かなければDiscord botのページに移動しIntents(「Bot」セクション)あたりをオンにしましょう。「PRESENCE INTENT」「MESSAGE CONTENT INTENT」「SERVER MEMBERS INTENT」あたりを改めてチェック。

これらがうまく行けば、もう少し検証を深めましょう!
下記のコードに変更することでbot名のメンション付きの投稿だけ反応するはずです。
テキストだけならコマンドプロンプトに「メッセージが投稿されました」
画像を含んでいたら「画像が投稿されました」と表示される。
これはDiscord上ではなくコマンドプロンプトに表示されるから注意!

client.on('messageCreate', message => {
    // ボット自身へのメンションであるか確認する
    if (message.mentions.has(client.user.id)) {
        // メッセージに添付ファイルがあるか確認
        if (message.attachments.size > 0) {
            message.attachments.forEach(attachment => {
                // 画像URLをログに出力
                console.log(`画像が投稿されました: ${attachment.url}`);
            });
        }

        // メッセージのテキスト内容をログに出力
        console.log(`メッセージが投稿されました: ${message.content}`);
    }
});

コードを修正した際は、既存の「node sd-discord-bot.js」を「Ctrl + C」で一度終了し、sd-discord-bot.jsを保存してから再度下記を実行です。

node sd-discord-bot.js

Discordのテキスト投稿で画像生成
※webUI API有効化以外飛ばしていいよ

Stable Diffusion webUI APIを機能させる

Stable Diffusionのtxt2img APIを使って、投稿されたプロンプトから画像を生成していきましょう!

その前にStable Diffusion webUIのAPIを使うのでStable Diffusion webUIのwebui-user.batの「COMMANDLINE_ARGS」に「--api」のオプションを設定しよう!
(下記は設定例:COMMANDLINE_ARGSは人によっていっぱいオプションが付いてると思うので、ソコに追記だ!)

@echo off

set PYTHON=
set GIT=
set VENV_DIR=
set COMMANDLINE_ARGS=--xformers --api

call webui.bat

テキストメッセージからのプロンプト抽出

まず始めに、Discordから送られてくるテキストメッセージをうまく扱うための仕組みを作るよ。ユーザーがDiscordに書き込んだメッセージから、必要な情報(プロンプト)を抜き出して、これを画像生成の元ネタとして使うんです。
※<@!?[0-9]+>/gで2回リプレイスしてるのは投稿時のロールIDとかを抜いてる系の処理だった記憶がうっすらありますよ。

client.on('messageCreate', async message => {
    if (message.mentions.has(client.user.id) && !message.author.bot) {
        const prompt = message.content.replace(/<@!?[0-9]+>/g, '').trim().replace(/<@&[0-9]+>/g, '').trim();
        // この prompt を使って画像を生成する
    }
});

Stable Diffusion txt2img APIとの連携

そしてStable Diffusionのtxt2img APIを使って、抽出したプロンプトから画像を生成するよ。APIへのリクエストには、プロンプトのテキスト、画像のサイズ、その他の設定値を含めておこう。
その設定値は適宜変更してね。
※モデルは多分今webUIで選ばれてるのが使われると思います〜

Stable Diffusionのtxt2img APIは普段開いてるwebUIのURLに「/sdapi/v1/txt2img」がつくので
http://XXX.X.X.X:XXXX/sdapi/v1/txt2img
となるはず。これをメモって下記コードを調整しよう。

// APIリクエストのペイロード
const payload = {
    prompt: prompt,
    steps: 30,
    cfg_scale: 7,
    width: 512,
    height: 512
};

const apiUrl = 'http://XXX.X.X.X:XXXX/sdapi/v1/txt2img';
const apiResponse = await fetch(apiUrl, {
    method: 'POST',
    body: JSON.stringify(payload),
    headers: { 'Content-Type': 'application/json' }
});

生成された画像をDiscordに返信

APIからのレスポンスを受け取り、そこに含まれる画像データ(base64エンコードされた形式)をDiscordに送り返しちゃうぞ!
これでユーザーは、自分の書いたテキストに基づいて生成された画像を見ることができるね。

base64とは
この手の作業してるとよく出てくる。
なんか画像を文字化したやつ。またはその逆。
プログラムの世界では画像を文字で扱えるので便利なんだそうだ。
詳しくはChatGPTに聞こう。

const apiData = await apiResponse.json();
if (apiData.images && apiData.images.length > 0) {
    const outputImageBuffer = Buffer.from(apiData.images[0], 'base64');
    const attachment = new AttachmentBuilder(outputImageBuffer, { name: 'output.png' });
    message.reply({ files: [attachment] });
} else {
    message.reply('画像を生成できませんでした。');
}

以下全文。途中のコピーだから間違ってたらごめん…。

// Discordとfetchライブラリをインポート
const { Client, GatewayIntentBits, AttachmentBuilder } = require('discord.js');
const fetch = require('node-fetch');

// 新しいDiscordクライアントを作成し、必要なIntentsを指定
const client = new Client({
    intents: [
        GatewayIntentBits.Guilds,
        GatewayIntentBits.GuildMessages,
        GatewayIntentBits.MessageContent
    ]
});

client.once('ready', () => {
    console.log('Ready!');
});

client.on('messageCreate', async message => {
    if (message.mentions.has(client.user.id) && !message.author.bot) {
        try {
            const prompt = message.content.replace(/<@!?[0-9]+>/g, '').trim().replace(/<@&[0-9]+>/g, '').trim();

            const payload = {
                prompt: prompt,
                steps: 30,
                cfg_scale: 7,
                width: 640,
                height: 960
            };

            console.log('テキストから画像を生成するリクエストを送信しています...');
            message.reply('画像生成リクエストを送信しました。');

            const apiUrl = 'http://XXX.X.X.X:XXXX/sdapi/v1/txt2img';
            const apiResponse = await fetch(apiUrl, {
                method: 'POST',
                body: JSON.stringify(payload),
                headers: { 'Content-Type': 'application/json' }
            });

            const apiData = await apiResponse.json();
            console.log('APIからのレスポンス:', apiData);

            if (apiData.images && apiData.images.length > 0) {
                console.log('画像データが存在します。');
                const outputImageBuffer = Buffer.from(apiData.images[0], 'base64');
                console.log('Bufferオブジェクトが生成されました。');

                const attachment = new AttachmentBuilder(outputImageBuffer, { name: 'output.png' });
                console.log('MessageAttachmentが生成されました。');

                message.reply({ files: [attachment] });
                console.log('画像がDiscordに送信されました。');
            } else {
                message.reply('画像を生成できませんでした。');
                console.log('APIから画像データが返されませんでした。');
            }
        } catch (error) {
            console.error('APIリクエストでエラーが発生しました:', error);
            message.reply(`APIリクエスト中にエラーが発生しました: ${error.message}`);
            console.log(`APIリクエスト中にエラーが発生しました: ${error.message}`);
        }
    }
});

// ここにあなたのDiscordボットのトークンを入れます
client.login('あなたのボットトークン');

このようにして、Discordボットはユーザーのテキスト入力から画像を生成し、その結果をリアルタイムでユーザーに提供することができます。これはただのチャットボットではなく、クリエイティブなアイデアを視覚化する強力なツールになりますよ!

そして、Discordでメンションをつけて「(masterpiece), cute 1girl, white hair, short bob, upper body」って投稿すると、その画像が返ってくるよ!

かわいい

ついに!Discordに画像投稿でimg2img!
※飛ばしていいよ

Discordからの画像の取得と処理

Discordボットの次のステップは、ユーザーが送信した画像をキャッチして、それをimg2imgの元素材として使うことです。
Discord APIを通じて送信された画像を取得し、それをStable Diffusionのimg2img APIに送る準備をしましょう。
投稿された画像サイズも取得してるので、その縦横をこれから生成するサイズにも適応できますぞ〜。

const { Client, GatewayIntentBits, AttachmentBuilder } = require('discord.js');
const { createCanvas, loadImage } = require('canvas');
const fetch = require('node-fetch');

// ...
client.on('messageCreate', async message => {
    if (message.mentions.has(client.user.id) && !message.author.bot && message.attachments.size > 0) {
        try {
            const imageAttachment = message.attachments.first();
            const imageUrl = imageAttachment.url;
            const imageResponse = await fetch(imageUrl);
            const imageBuffer = await imageResponse.buffer();

            // 画像のサイズを取得
            const image = await loadImage(imageBuffer);
            const width = image.width;
            const height = image.height;
            console.log(`画像サイズ: ${width}x${height}`);

        // APIリクエストのペイロード処理とか

    }
});

Stable Diffusion img2img APIとの連携

次に、取得した画像とユーザーからのテキスト入力(プロンプト)を使って、Stable Diffusion img2img APIにリクエストを送信。
デノイズ等の設定値は適切に設定してくれ!
※私はデノイズの強さは0.4〜0.5あたりが好きだ!

// APIリクエストのペイロード
const payload = {
    prompt: message.content.replace(/<@!?[0-9]+>/g, '').trim(),
    init_images: [imageBuffer.toString('base64')],
    steps: 30,
    cfg_scale: 7,
    denoising_strength:0.5,
    width: width,    // 取得した幅
    height: height   // 取得した高さ
};
console.log('APIリクエストを送信しています...');
message.reply('画像生成リクエストを送信しました。');

const apiUrl = 'http://XXX.X.X.X:XXXX/sdapi/v1/img2img';
const apiResponse = await fetch(apiUrl, {
    method: 'POST',
    body: JSON.stringify(payload),
    headers: { 'Content-Type': 'application/json' }
});

生成された画像をDiscordに返信

APIからのレスポンスを受け取り、そこに含まれる新しい画像データ(base64エンコードされた形式)をDiscordに送り返すよ。
これにより、ユーザーは自分が送信した画像がどのようにimg2imgしたかを目の当たりにすることができます!刮目せよ!

const apiData = await apiResponse.json();

if (apiData.images && apiData.images.length > 0) {
    console.log('生成された画像を処理しています...');
    const outputImageBuffer = Buffer.from(apiData.images[0], 'base64');
    const attachment = new AttachmentBuilder(outputImageBuffer, { name: 'output.png' });
    message.reply({ files: [attachment] });
    console.log('生成された画像を送信しました。');
} else {
    message.reply('画像を生成できませんでした。');
    console.log('画像を生成できませんでした。');
}

以下全文。途中のコピーだから間違ってたらごめん…。

const { Client, GatewayIntentBits, AttachmentBuilder } = require('discord.js');
const { createCanvas, loadImage } = require('canvas');
const fetch = require('node-fetch');


client.on('messageCreate', async message => {
    if (message.mentions.has(client.user.id) && !message.author.bot && message.attachments.size > 0) {
        try {
            const imageAttachment = message.attachments.first();
            const imageUrl = imageAttachment.url;
            const imageResponse = await fetch(imageUrl);
            const imageBuffer = await imageResponse.buffer();

            // 画像のサイズを取得
            const image = await loadImage(imageBuffer);
            const width = image.width;
            const height = image.height;
            console.log(`画像サイズ: ${width}x${height}`);

            // APIリクエストのペイロード
            const payload = {
                prompt: message.content.replace(/<@!?[0-9]+>/g, '').trim(),
                init_images: [imageBuffer.toString('base64')],
                steps: 30,
                cfg_scale: 7,
                width: width,    // 取得した幅
                height: height   // 取得した高さ
            };

            console.log('APIリクエストを送信しています...');
            message.reply('画像生成リクエストを送信しました。');

            const apiUrl = 'http://XXX.X.X.X:XXXX/sdapi/v1/img2img';
            const apiResponse = await fetch(apiUrl, {
                method: 'POST',
                body: JSON.stringify(payload),
                headers: { 'Content-Type': 'application/json' }
            });

            console.log('APIからのレスポンスを受信しました。');
            const apiData = await apiResponse.json();

            if (apiData.images && apiData.images.length > 0) {
                console.log('生成された画像を処理しています...');
                const outputImageBuffer = Buffer.from(apiData.images[0], 'base64');
                const attachment = new AttachmentBuilder(outputImageBuffer, { name: 'output.png' });
                message.reply({ files: [attachment] });
                console.log('生成された画像を送信しました。');
            } else {
                message.reply('画像を生成できませんでした。');
                console.log('画像を生成できませんでした。');
            }
        } catch (error) {
            console.error('APIリクエストでエラーが発生しました:', error);
            message.reply(`APIリクエスト中にエラーが発生しました: ${error.message}`);
            console.log(`APIリクエスト中にエラーが発生しました: ${error.message}`);
        }
    }
});

// ここにあなたのDiscordボットのトークンを入れます
client.login('あなたのボットトークン');

このプロセスを通じて、Discordはあなたのお絵描き道具となります!
あなたのアイデアやイマジネーションをビジュアライズし、クリエイティビティなコミュニケーションを促進しレボリューションでイリュージョンです!

下記は画像とプロンプトを投稿した例。デノイズ強度が低いので、あんまり変わってないですね…。

i2iしても変わってない図

より便利な機能の追加!
txt2imgもimg2imgも両方対応させればいいじゃん

添付画像があるかどうかに基づくimg2imgとtxt2imgの動的な切り替え

ここまで来たらDiscordボットにもう少し賢さを加えましょう!

ボットは、ユーザーが送信したメッセージに添付画像があるかどうかに基づいて、img2imgかtxt2imgのどちらを実行するかを決めます。
・画像があれば、その画像をベースに新しい画像を生成(img2img)
・画像がなければ、テキストプロンプトから新しい画像を作り出します(txt2img)

client.on('messageCreate', async message => {
    // ...
    if (message.attachments.size > 0) {
        // img2imgの処理
    } else {
        // txt2imgの処理
    }
    // ...
});

ネガティブプロンプトの設定

画像生成の際に、ネガティブプロンプトも設定できるようにしましょう!
私は「EasyNegativeV2」を入れています。EasyNegativeV2の使い方はググってくれぃ。webUIにはいってないと使えないぞ。

const payload = {
    // ...
    negative_prompt: "EasyNegativeV2",
    // ...
};

直近のリクエストを再実行する「retry」コマンドの追加

さらに、便利な機能を一つ追加!
ユーザーが「retry」と入力したら、
ボットは直近のリクエストを再実行します。
シード値は変わるので何度でもガチャができます。

 } else if (prompt.toLowerCase() === 'retry' && lastPayload) {
    // 最後のペイロードを再利用して処理
    payload = lastPayload;
    payload.seed = -1; // シード値をリセット
}

以下全文。もし見えたらあかん文字列が入ってたら即座に教えて…

const { Client, GatewayIntentBits, AttachmentBuilder } = require('discord.js');
const { createCanvas, loadImage } = require('canvas');
const fetch = require('node-fetch');

// 新しいDiscordクライアントを作成し、必要なIntentsを指定
const client = new Client({
    intents: [
        GatewayIntentBits.Guilds,
        GatewayIntentBits.GuildMessages,
        GatewayIntentBits.MessageContent
    ]
});

let lastPayload = null;

client.once('ready', () => {
    console.log('Ready!');
});

client.on('messageCreate', async message => {
    if (message.mentions.has(client.user.id) && !message.author.bot) {
        let payload;
        const prompt = message.content.replace(/<@!?[0-9]+>/g, '').trim().replace(/<@&[0-9]+>/g, '').trim();

        if (message.attachments.size > 0) {
            // img2img処理
            const imageAttachment = message.attachments.first();
            const imageUrl = imageAttachment.url;
            const imageResponse = await fetch(imageUrl);
            const imageBuffer = await imageResponse.buffer();

            // 画像のサイズを取得
            const image = await loadImage(imageBuffer);
            const width = image.width;
            const height = image.height;
            console.log(`画像サイズ: ${width}x${height}`);

            payload = {
                prompt: prompt,
                negative_prompt: "EasyNegativeV2",
                init_images: [imageBuffer.toString('base64')],
                steps: 30,
                cfg_scale: 7,
                denoising_strength:0.5,
                width: width,
                height: height
            };
        } else if (prompt.toLowerCase() === 'retry' && lastPayload) {
            // 最後のペイロードを再利用して処理
            payload = lastPayload;
            payload.seed = -1; // シード値をリセット
        } else {
            // txt2img処理
            payload = {
                prompt: prompt,
                negative_prompt: "EasyNegativeV2",
                steps: 30,
                cfg_scale: 7,
                width: 640,
                height: 960
            };
        }

        try {
            const apiUrl = payload.init_images ? 'http://XXX.X.X.X:XXXX/sdapi/v1/img2img' : 'http://127.0.0.1:7860/sdapi/v1/txt2img';
            const apiResponse = await fetch(apiUrl, {
                method: 'POST',
                body: JSON.stringify(payload),
                headers: { 'Content-Type': 'application/json' }
            });

            const apiData = await apiResponse.json();
            lastPayload = payload; // 最後に実行したペイロードを保存

            if (apiData.images && apiData.images.length > 0) {
                const outputImageBuffer = Buffer.from(apiData.images[0], 'base64');
                const attachment = new AttachmentBuilder(outputImageBuffer, { name: 'output.png' });
                message.reply({ files: [attachment] });
            } else {
                message.reply('画像を生成できませんでした。');
            }
        } catch (error) {
            console.error('APIリクエストでエラーが発生しました:', error);
            message.reply(`APIリクエスト中にエラーが発生しました: ${error.message}`);
        }
    }
});
// ここにあなたのDiscordボットのトークンを入れます
client.login('あなたのボットトークン');


にじジャーニーの元絵
これがスマホのDiscord操作で実現できる!

とりあえず私は動いた。君はどうだ?
寝ます。


いいなと思ったら応援しよう!