import math import os import tempfile import uuid from typing import Tuple, List, Any import discord import openai # An enum of two modes, TOP_P or TEMPERATURE import requests from PIL import Image from discord import File class Mode: TOP_P = "top_p" TEMPERATURE = "temperature" class Models: DAVINCI = "text-davinci-003" CURIE = "text-curie-001" class ImageSize: LARGE = "1024x1024" MEDIUM = "512x512" SMALL = "256x256" class Model: def __init__(self, usage_service): self._mode = Mode.TEMPERATURE self._temp = 0.6 # Higher value means more random, lower value means more likely to be a coherent sentence self._top_p = 0.9 # 1 is equivalent to greedy sampling, 0.1 means that the model will only consider the top 10% of the probability distribution self._max_tokens = 4000 # The maximum number of tokens the model can generate self._presence_penalty = ( 0 # Penalize new tokens based on whether they appear in the text so far ) self._frequency_penalty = 0 # Penalize new tokens based on their existing frequency in the text so far. (Higher frequency = lower probability of being chosen.) self._best_of = 1 # Number of responses to compare the loglikelihoods of self._prompt_min_length = 12 self._max_conversation_length = 25 self._model = Models.DAVINCI self._low_usage_mode = False self.usage_service = usage_service self.DAVINCI_ROLES = ["admin", "Admin", "GPT", "gpt"] self._image_size = ImageSize.MEDIUM self._num_images = 2 try: self.IMAGE_SAVE_PATH = os.environ["IMAGE_SAVE_PATH"] self.custom_image_path = True except: self.IMAGE_SAVE_PATH = "dalleimages" # Try to make this folder called images/ in the local directory if it doesnt exist if not os.path.exists(self.IMAGE_SAVE_PATH): os.makedirs(self.IMAGE_SAVE_PATH) self.custom_image_path = False self._hidden_attributes = [ "usage_service", "DAVINCI_ROLES", "custom_image_path", "custom_web_root", "_hidden_attributes", ] openai.api_key = os.getenv("OPENAI_TOKEN") # Use the @property and @setter decorators for all the self fields to provide value checking @property def image_size(self): return self._image_size @image_size.setter def image_size(self, value): if value in ImageSize.__dict__.values(): self._image_size = value else: raise ValueError( "Image size must be one of the following: SMALL(256x256), MEDIUM(512x512), LARGE(1024x1024)" ) @property def num_images(self): return self._num_images @num_images.setter def num_images(self, value): value = int(value) if value > 4 or value <= 0: raise ValueError("num_images must be less than 4 and at least 1.") self._num_images = value @property def low_usage_mode(self): return self._low_usage_mode @low_usage_mode.setter def low_usage_mode(self, value): try: value = bool(value) except ValueError: raise ValueError("low_usage_mode must be a boolean") if value: self._model = Models.CURIE self.max_tokens = 1900 else: self._model = Models.DAVINCI self.max_tokens = 4000 @property def model(self): return self._model @model.setter def model(self, model): if model not in [Models.DAVINCI, Models.CURIE]: raise ValueError( "Invalid model, must be text-davinci-003 or text-curie-001" ) self._model = model @property def max_conversation_length(self): return self._max_conversation_length @max_conversation_length.setter def max_conversation_length(self, value): value = int(value) if value < 1: raise ValueError("Max conversation length must be greater than 1") if value > 30: raise ValueError( "Max conversation length must be less than 30, this will start using credits quick." ) self._max_conversation_length = value @property def mode(self): return self._mode @mode.setter def mode(self, value): if value not in [Mode.TOP_P, Mode.TEMPERATURE]: raise ValueError("mode must be either 'top_p' or 'temperature'") if value == Mode.TOP_P: self._top_p = 0.1 self._temp = 0.7 elif value == Mode.TEMPERATURE: self._top_p = 0.9 self._temp = 0.6 self._mode = value @property def temp(self): return self._temp @temp.setter def temp(self, value): value = float(value) if value < 0 or value > 1: raise ValueError( "temperature must be greater than 0 and less than 1, it is currently " + str(value) ) self._temp = value @property def top_p(self): return self._top_p @top_p.setter def top_p(self, value): value = float(value) if value < 0 or value > 1: raise ValueError( "top_p must be greater than 0 and less than 1, it is currently " + str(value) ) self._top_p = value @property def max_tokens(self): return self._max_tokens @max_tokens.setter def max_tokens(self, value): value = int(value) if value < 15 or value > 4096: raise ValueError( "max_tokens must be greater than 15 and less than 4096, it is currently " + str(value) ) self._max_tokens = value @property def presence_penalty(self): return self._presence_penalty @presence_penalty.setter def presence_penalty(self, value): if int(value) < 0: raise ValueError( "presence_penalty must be greater than 0, it is currently " + str(value) ) self._presence_penalty = value @property def frequency_penalty(self): return self._frequency_penalty @frequency_penalty.setter def frequency_penalty(self, value): if int(value) < 0: raise ValueError( "frequency_penalty must be greater than 0, it is currently " + str(value) ) self._frequency_penalty = value @property def best_of(self): return self._best_of @best_of.setter def best_of(self, value): value = int(value) if value < 1 or value > 3: raise ValueError( "best_of must be greater than 0 and ideally less than 3 to save tokens, it is currently " + str(value) ) self._best_of = value @property def prompt_min_length(self): return self._prompt_min_length @prompt_min_length.setter def prompt_min_length(self, value): value = int(value) if value < 10 or value > 4096: raise ValueError( "prompt_min_length must be greater than 10 and less than 4096, it is currently " + str(value) ) self._prompt_min_length = value def send_request( self, prompt, message, temp_override=None, top_p_override=None, best_of_override=None, frequency_penalty_override=None, presence_penalty_override=None, max_tokens_override=None, ): # Validate that all the parameters are in a good state before we send the request if len(prompt) < self.prompt_min_length: raise ValueError( "Prompt must be greater than 25 characters, it is currently " + str(len(prompt)) ) print("The prompt about to be sent is " + prompt) prompt_tokens = self.usage_service.count_tokens(prompt) print(f"The prompt tokens will be {prompt_tokens}") print(f"The total max tokens will then be {self.max_tokens - prompt_tokens}") response = openai.Completion.create( model=Models.DAVINCI if any(role.name in self.DAVINCI_ROLES for role in message.author.roles) else self.model, # Davinci override for admin users prompt=prompt, temperature=self.temp if not temp_override else temp_override, top_p=self.top_p if not top_p_override else top_p_override, max_tokens=self.max_tokens - prompt_tokens if not max_tokens_override else max_tokens_override, presence_penalty=self.presence_penalty if not presence_penalty_override else presence_penalty_override, frequency_penalty=self.frequency_penalty if not frequency_penalty_override else frequency_penalty_override, best_of=self.best_of if not best_of_override else best_of_override, ) print(response.__dict__) # Parse the total tokens used for this request and response pair from the response tokens_used = int(response["usage"]["total_tokens"]) self.usage_service.update_usage(tokens_used) return response def send_image_request(self, prompt, vary=None) -> tuple[File, list[Any]]: # Validate that all the parameters are in a good state before we send the request words = len(prompt.split(" ")) if words < 3 or words > 75: raise ValueError( "Prompt must be greater than 3 words and less than 75, it is currently " + str(words) ) print("The prompt about to be sent is " + prompt) self.usage_service.update_usage_image(self.image_size) if not vary: response = openai.Image.create( prompt=prompt, n=self.num_images, size=self.image_size, ) else: response = openai.Image.create_variation( image=open(vary, "rb"), n=self.num_images, size=self.image_size, ) print(response.__dict__) image_urls = [] for result in response["data"]: image_urls.append(result["url"]) # For each image url, open it as an image object using PIL images = [Image.open(requests.get(url, stream=True).raw) for url in image_urls] # Save all the images with a random name to self.IMAGE_SAVE_PATH image_names = [f"{uuid.uuid4()}.png" for _ in range(len(images))] for image, name in zip(images, image_names): image.save(f"{self.IMAGE_SAVE_PATH}/{name}") # Update image_urls to include the local path to these new images image_urls = [f"{self.IMAGE_SAVE_PATH}/{name}" for name in image_names] widths, heights = zip(*(i.size for i in images)) # Calculate the number of rows and columns needed for the grid num_rows = num_cols = int(math.ceil(math.sqrt(len(images)))) # If there are only 2 images, set the number of rows to 1 if len(images) == 2: num_rows = 1 # Calculate the size of the combined image width = max(widths) * num_cols height = max(heights) * num_rows # Create a transparent image with the same size as the images transparent = Image.new("RGBA", (max(widths), max(heights))) # Create a new image with the calculated size new_im = Image.new("RGBA", (width, height)) # Paste the images and transparent segments into the grid x_offset = y_offset = 0 for im in images: new_im.paste(im, (x_offset, y_offset)) x_offset += im.size[0] if x_offset >= width: x_offset = 0 y_offset += im.size[1] # Fill the remaining cells with transparent segments while y_offset < height: while x_offset < width: new_im.paste(transparent, (x_offset, y_offset)) x_offset += transparent.size[0] x_offset = 0 y_offset += transparent.size[1] # Save the new_im to a temporary file and return it as a discord.File temp_file = tempfile.NamedTemporaryFile(suffix=".png") new_im.save(temp_file.name) # Print the filesize of new_im, in mega bytes image_size = os.path.getsize(temp_file.name) / 1000000 # If the image size is greater than 8MB, we can't return this to the user, so we will need to downscale the # image and try again safety_counter = 0 while image_size > 8: safety_counter += 1 if safety_counter >= 2: break print( f"Image size is {image_size}MB, which is too large for discord. Downscaling and trying again" ) new_im = new_im.resize( (int(new_im.width / 1.05), int(new_im.height / 1.05)) ) temp_file = tempfile.NamedTemporaryFile(suffix=".png") new_im.save(temp_file.name) image_size = os.path.getsize(temp_file.name) / 1000000 print(f"New image size is {image_size}MB") return (discord.File(temp_file.name), image_urls)