PH
Paul Hetherington
11 Oct 2022

How to make a Stable Diffusion discord bot

This post demonstrates how to create a discord bot to generate images with Stable Diffusion.

This post demonstrates how to create a discord bot to generate images with Stable Diffusion. We will be using the Stable Diffusion API provided by pipeline.ai to handle the ML compute. The bot will be created in python using the discord interactions library and can run on your laptop.

Figure 1. Demo of the bot using the /paint command.

If you don't want to make this yourself, you can add the bot I made to your server (for free) by clicking here or checkout the git repo here.

Initial setup 

Discord developer setup

Before we can hookup to the Discord API you will first have to create a Discord developer account. Below is a basic set of steps to do this (or you can view a more in-depth guide here).

  1. Sign in to the discord developer portal (you can use your regular discord account)
  2. Click the New Application on the top right
  3. Once the Application has been created click on Bot on the left menu and click Add Bot
  4. To add this bot to your server navigate to OAuth2/URL Generator on the left menu and select bot on the first scope and Use Slash Commands under the Bot Permissions. You'll see a Generated URL field populated at the bottom of the page, copy and click on it to add it to your server!
  5. Finally, you need to collect your discord bot token under the Token tab on your under the Bot tab on the left menu.

Dependencies

There are two main libraries for working with Discord in python:

We will be using interactions.py, but they are relatively interchangable. The full list of dependencies can be found here requirements.txtThis post uses python 3.9 and should work for later python versions.

You can install the dependencies in your python environment using

1pip install -r requirements.txt
2# OR
3python -m pip install -r requirements.txt

Making the bot

Generating an image with Stable Diffusion

The core code to generate the image with the pipeline.ai library is straight forward as it has native asyncio support for use in production APIs:

1pipeline_api = PipelineCloud(token="...")
2
3async def handle_generation(image_prompt: str) -> io.BytesIO:
4    response = pipeline_api.run_pipeline(
5        "pipeline_67d9d8ec36d54c148c70df1f404b0369",
6        [[image_prompt], {"width": 512, "height": 512, "num_inference_steps": 50}],
7    )
8    image_b64 = response.result_preview[0][0][0]
9
10    image_decoded = base64.decodebytes(image_b64.encode())
11    buffer = io.BytesIO(image_decoded)
12    new_uid = str(uuid.uuid4())
13    buffer.name = new_uid
14    return buffer

Bot client

To add a command to your bot with the interactions.py library you create an async function with the @bot.command(...) decorator:

1# For our '/paint' command we only want to take in a single text prompt, 
2# this is included in the 'options' field below and the name of the option 
3# is what's used as a key word argument to our function
1@bot.command(
2    name="paint",
3    description="Paint an image that has never existed...",
4    options=[
5        interactions.Option(
6            name="prompt",
7            description="Image generation prompt",
8            type=interactions.OptionType.STRING,
9            required=True,
10        ),
11    ],
12)
13async def paint(ctx: interactions.CommandContext, prompt: str) -> None:
14    # You have to send back a response quickly otherwise 
15    # Discord thinks that the bot has died.
16    sent_response = await ctx.send("Generating image...")
17
18    try:
19        image_buffer = await handle_generation(prompt)
20
21        # Edit the original message sent to now include the image and the prompt
22        await sent_response.edit(
23            files=[
24                interactions.api.models.misc.File(
25                    filename=prompt + ".jpeg", fp=image_buffer
26                )
27            ],
28            content=prompt
29            # You can add another argument 'ephemeral=True' to only show the 
30            # result to the user that sent the request.
31        )
32    except:
33        # If the image generation (or anything else) fails 
34        # for any reason it's best to let the user know
35        await sent_response.edit(
36            content="Generation failed, please try again!",
37        )
38
39        # With asyncio you have to call the 'flush=True' on print
40        print(traceback.format_exc(), flush=True)
41Finally we need to add a few things to run the bot and complete our script:
1import os
2import interactions
3import traceback
4import base64
5import io
6import uuid
7
8from pipeline.api.asyncio import PipelineCloud
9
10# The token here is the one we collected earlier from the discord bot
11discord_token = os.getenv("DISCORD_TOKEN")
12pipeline_token = os.getenv("PIPELINE_API_TOKEN")
13bot = interactions.Client(token=discord_token)
14pipeline_api = PipelineCloud(token=pipeline_token)
15
16# As defined earlier
17async def handle_generation(...) -> None:
18    ...
19
20# As defined earlier
21@bot.command(...)
22async def paint(...) -> None:
23    ...
24
25bot.start()  

This code was saved on my system as sd_discord_bot.py, and the environment variables can be passed in as follows:

1env DISCORD_TOKEN=... PIPELINE_API_TOKEN=... python bot.py

You can now navigate to your discord server and run /paint!

Docker & docker compose

The bot will run for as long as you have your terminal open, but to run this system continually Docker is a great solution. Docker has a quick start guide here but for this post you will only need it installed and not much further understanding.

br> This section uses the following project directory layout (the requirements.txt is the one described above):
1project_dir/
2    ./bot.py
3    ./requirements.txt
4    ./Dockerfile
5    ./docker-compose.yml
6    ./secrets.env
All the bot requires to run is the standard python docker image with our secrets and bot.py copied into it. Here is the Dockerfile used for the project:
1FROM python:3.9-slim
2
3WORKDIR /code
4
5COPY ./requirements.txt /code/
6
7ENV PYTHONDONTWRITEBYTECODE=1
8
9RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt
10
11COPY ./bot.py /code/
12
13CMD ["python", "bot.py"]
This can run standalone via the following commands in the project directory:
1sudo docker build . -t stable-diffusion-discord-bot:main
2sudo docker run --env DISCORD_TOKEN=... --env PIPELINE_API_TOKEN=... stable-diffusion-discord-bot:main
Alternatively, if you'd like to run the Docker image through docker compose you can use the following docker-compose.yml file:
1version: "3.9"
2services:
3stable-diffusion-discord-bot:
4    container_name: stable-diffusion-discord-bot
5    image: 
6    build:
7      context: .
8      dockerfile: ./Dockerfile
9    env_file:
10      - secrets.env
To run this you will have to populate the secrets.env with the DISCORD_TOKEN & PIPELINE_API_TOKEN variables. You can run this simply with:
1sudo docker-compose run --build -d

------------------------------------------------------

If you run into issues, the Pipeline team are always willing to help in any way. Contact us here or join us on Discord!