0
votes

I'm working on a Discord bot in which I mainly process images. So far it's working but when multiple images are sent at once, I experience a lot of blocking and inconsistency.

It goes like this:

User upload image > Bot places 'eyes' emoji on the message > bot processes the image > bot responds with result.

However, sometimes it can handle multiple images at once (the bot places the eyes emoji on the first few images) but usually it just puts emoji on the first image and then after finishing that one it will process the next 2-3 images etc.

The process which takes most of the time is the OCR reading the image.

Here is some abstract code:

main.py

@client.event
async def on_message(message):
  ...
  if len(message.attachments) > 0: await message_service.handle_image(message)
  ...

message_service.py

  async def handle_image(self, message):
    supported_attachments = filter_out_unsupported(message.attachments)
    images = []
    await message.reply(f"{random_greeting()} {message.author.mention}, I'm processing your upload(s) please wait a moment, this could take up to 30 seconds.")
    await message.add_reaction('????')
    for a in supported_attachments:
      async with aiohttp.ClientSession() as session:
        async with session.get(a) as res:
          if res.status == 200:
            buffer = io.BytesIO(await res.read())
            arr = np.asarray(bytearray(buffer.read()), dtype=np.uint8)
            images.append(cv2.imdecode(arr, -1))

          for image in images:
            result = await self.image_handler.handle_image(image, message.author)
            await message.remove_reaction('????', message.author)
            if result == None:
              await message.reply(f"{message.author.mention} I can't process your image. It's incorrect, unclear or I'm just not smart enough... :(")
              await message.add_reaction('❌')
            else:
              await message.reply(result)

image_handler

async def handle_image(self, image, author):
    try:
      if image is None: return None

      governor_id = str(self.__get_governor_id_by_discord_id(author.id))
      if governor_id == None:
        return f"{author.mention} there was no account registered under your discord id, please register by using this format: `$register <governor_id> <in game name>`, for example: `$register ... ...`. After that repost the screenshot.\n As for now multiple accounts are not supported."
      
      # This part is most likely the bottleneck !!
      read_result = self.reader.read_image_task(image)
      if self.__no_values_are_found(...):
        return None

      return self.sheets_client.update_player_row_in_sheets(...)
    except:
      return None
    
  def __no_values_are_found(self, *args):
    return all(v is None for v in [*args])


  def __get_governor_id_by_discord_id(self, id):
    return self.sheets_client.get_governor_id_by_discord_id(id)

I'm new to Python and Discord bots in general, but is there a clean way to handle this?

I was thinking about threading but can't seem to find many solutions within this context, which makes me believe I am missing something or doing something inefficiently.

1

1 Answers

1
votes

There is actually a clean way, you can create your own to_thread decorator and decorate your blocking functions (though they cannot be coroutines, they must be normal, synchronous functions)

import asyncio
from functools import partial, wraps

def to_thread(func):
    @wraps(func)
    async def wrapper(*args, **kwargs):
        loop = asyncio.get_event_loop()
        callback = partial(func, *args, **kwargs)
        return await loop.run_in_executor(None, callback)  # if using python 3.9+ use `await asyncio.to_thread(callback)`
    return wrapper


# usage
@to_thread
def handle_image(self, image, author):  # notice how it's *not* an async function
    ...


# calling
await handle_image(...)  # not a coroutine, yet I'm awaiting it (cause of the wrapper function)