Translations with DeepL

Kaveen Kumarasinghe 1 year ago
parent ea5f95800b
commit 3627d32e54

@ -11,7 +11,7 @@
# Overview
A robust, all-in-one GPT3 interface for Discord. Chat just like ChatGPT right inside Discord! Generate beautiful AI art using DALL-E 2! Automatically moderate your server using AI! A thorough integration with permanent conversation memory, automatic request retry, fault tolerance and reliability for servers of any scale, and much more.
SUPPORT SERVER FOR BOT SETUP: https://discord.gg/WvAHXDMS7Q (You can NOT use the bot here, it is for setup support ONLY)
SUPPORT SERVER FOR BOT SETUP: https://discord.gg/WvAHXDMS7Q (You can try out the bot here also in a limited fashion)
# Screenshots
<p align="center">
@ -23,14 +23,12 @@ SUPPORT SERVER FOR BOT SETUP: https://discord.gg/WvAHXDMS7Q (You can NOT use the
</p>
# Recent Notable Updates
- **Edit Requests** - Ask GPT to edit a piece of text with a given instruction using a specific OpenAI edits model! `/gpt edit`!
- **Translations with DeepL** - DeepL integration for translations. `/translate`
- **Context menu commands** - Allow people to prompt GPT and DALL-E directly by right clicking a message:
<img src="https://i.imgur.com/fkfnJQ0.png"/>
- **Automatic retry on API errors** - The bot will automatically retry API requests if they fail due to some issue with OpenAI's APIs, this is becoming increasingly important now as their APIs become under heavy load.
<center>
<img src="https://i.imgur.com/fkfnJQ0.png"/></center>
- **AI-BASED SERVER MODERATION** - GPT3Discord now has a built-in AI-based moderation system that can automatically detect and remove toxic messages from your server. This is a great way to keep your server safe and clean, and it's completely automatic and **free**! Check out the commands section to learn how to enable it!
@ -51,6 +49,8 @@ SUPPORT SERVER FOR BOT SETUP: https://discord.gg/WvAHXDMS7Q (You can NOT use the
- **Edit Requests** - Ask GPT to edit a piece of text or code with a given instruction. `/gpt edit <instruction> <text>`
- **DeepL Translations** - Translate text with DeepL. `/translate <text>`
- **Redo Requests** - A simple button after the GPT3 response or DALL-E generation allows you to redo the initial prompt you asked. You can also redo conversation messages by just editing your message!
- **Automatic AI-Based Server Moderation** - Moderate your server automatically with AI!
@ -145,7 +145,7 @@ If you'd like to help us test and fine tune our thresholds for the moderation se
**The above server is NOT for support or discussions about GPT3Discord**
# Permanent Memory
# Permanent Memory and Conversations
Permanent memory has now been implemented into the bot, using the OpenAI Ada embeddings endpoint, and Pinecone DB.
PineconeDB is a vector database. The OpenAI Ada embeddings endpoint turns pieces of text into embeddings. The way that this feature works is by embedding the user prompts and the GPT3 responses, storing them in a pinecone index, and then retrieving the most relevant bits of conversation whenever a new user prompt is given in a conversation.
@ -161,17 +161,15 @@ To get a pinecone token, you can sign up for a free pinecone account here: https
After signing up for a free pinecone account, you need to create an index in pinecone. To do this, go to the pinecone dashboard and click "Create Index" on the top right.
<img src="https://i.imgur.com/L9LXVE0.png"/>
<center><img src="https://i.imgur.com/L9LXVE0.png"/></center>
Then, name the index `conversation-embeddings`, set the dimensions to `1536`, and set the metric to `DotProduct`:
<img src="https://i.imgur.com/zoeLsrw.png"/>
Moreover, an important thing to keep in mind is: pinecone indexes are currently not automatically cleared by the bot, so you will eventually need to clear the index manually through the pinecone website if things are getting too slow (although it should be a very long time until this happens). Pinecone indexes are keyed on the `metadata` field using the thread id of the conversation thread.
<center><img src="https://i.imgur.com/zoeLsrw.png"/></center>
Permanent memory using pinecone is still in alpha, I will be working on cleaning up this work, adding auto-clearing, and optimizing for stability and reliability, any help and feedback is appreciated (**add me on Discord Kaveen#0001 for pinecone help**)! If at any time you're having too many issues with pinecone, simply remove the `PINECONE_TOKEN` line in your `.env` file and the bot will revert to using conversation summarizations.
# Permanent overrides in threads
### Permanent overrides in threads
This bot now supports having overrides be permanent in an entire conversation if you use an opener file which includes them. The new opener files should be .json files formatted like this. `text` corresponds to what you want the conversational opener to be and the rest map 1:1 to the appropriate model settings. An example .json file is included by the name of `english_translator.json` in the `openers` folder
```json
{
@ -183,6 +181,24 @@ This bot now supports having overrides be permanent in an entire conversation if
}
```
# Translations with DeepL
This bot supports and uses DeepL for translations (optionally). If you want to enable the translations service, you can add a line in your `.env` file as follows:
```
DEEPL_TOKEN="your deepl token"
```
You can get a DeepL token by signing up at https://www.deepl.com/pro-api?cta=header-pro-api/ and clicking on the *free plan* to start. The DeepL translation service unlocks some new commands for your bot:
`/translate <text> <language>` - Translate any given piece of text into the language that you provide
`/languages` - See a list of all supported languages
Using DeepL also adds a new app menu button (when you right click a message) to the bot which allows you to quickly translate any message in a channel into any language you want:
<img src="https://i.imgur.com/MlNVWKu.png"/>
# User-Input API Keys (Multi-key tenancy)
This bot supports multi-user tenancy in regards to API keys. This means that, if you wanted, you could make it such that each user needs to enter their own API key in order to use commands that use GPT3 and DALLE.
@ -196,7 +212,8 @@ Then, restart the bot, and it will set up the system for everyone to input their
The bot will use SQLite to store API keys for the users, each user's key will be saved with a USER_ID <> API_KEY mapping in SQLite, and will be persistent across restarts. All the data will be saved in a file called `user_key_db.sqlite` in the current working directory of the bot.
With this feature enabled, any attempt to use a GPT3 or DALL-E command without a valid API key set for the user will pop up the following modal for them to enter their API key:
<img src="https://i.imgur.com/ZDScoWk.png"/>
<center><img src="https://i.imgur.com/ZDScoWk.png"/></center>
Once the user enters their key, the bot will send a small test request to OpenAI to validate that the key indeed works, if not, it will tell the user to try again and tell them why it did not work.

@ -3,11 +3,10 @@ from pycord.multicog import add_to_group
from services.environment_service import EnvService
from models.check_model import Check
from models.autocomplete_model import Settings_autocompleter, File_autocompleter
from models.autocomplete_model import Settings_autocompleter, File_autocompleter, Translations_autocompleter
ALLOWED_GUILDS = EnvService.get_allowed_guilds()
class Commands(discord.Cog, name="Commands"):
"""Cog containing all slash and context commands as one-liners"""
@ -22,6 +21,7 @@ class Commands(discord.Cog, name="Commands"):
image_draw_cog,
image_service_cog,
moderations_cog,
translations_cog=None,
):
super().__init__()
self.bot = bot
@ -33,6 +33,7 @@ class Commands(discord.Cog, name="Commands"):
self.image_draw_cog = image_draw_cog
self.image_service_cog = image_service_cog
self.moderations_cog = moderations_cog
self.translations_cog = translations_cog
# Create slash command groups
dalle = discord.SlashCommandGroup(
@ -502,3 +503,37 @@ class Commands(discord.Cog, name="Commands"):
)
async def draw_action(self, ctx, message: discord.Message):
await self.image_draw_cog.draw_action(ctx, message)
"""
Translation commands and actions
"""
@discord.slash_command(name="translate", description="Translate text to a given language", guild_ids=ALLOWED_GUILDS,
checks=[Check.check_translator_roles()],
)
@discord.option(name="text", description="The text to translate", required=True)
@discord.option(name="target_language", description="The language to translate to", required=True, autocomplete=Translations_autocompleter.get_languages)
@discord.guild_only()
async def translate(self, ctx: discord.ApplicationContext, text: str, target_language: str):
if self.translations_cog:
await self.translations_cog.translate_command(ctx, text, target_language)
else:
await ctx.respond("Translations are disabled on this server.", ephemeral=True)
@discord.slash_command(name="languages", description="View the supported languages for translation", guild_ids=ALLOWED_GUILDS,
checks=[Check.check_translator_roles()],)
@discord.guild_only()
async def languages(self, ctx: discord.ApplicationContext):
if self.translations_cog:
await self.translations_cog.languages_command(ctx)
else:
await ctx.respond("Translations are disabled on this server.", ephemeral=True)
@discord.message_command(
name="Translate", guild_ids=ALLOWED_GUILDS, checks=[Check.check_translator_roles()]
)
async def translate_action(self, ctx, message: discord.Message):
if self.translations_cog:
await self.translations_cog.translate_action(ctx, message)
else:
await ctx.respond("Translations are disabled on this server.", ephemeral=True)

@ -0,0 +1,115 @@
import asyncio
import os
import traceback
import aiohttp
import discord
# We don't use the converser cog here because we want to be able to redo for the last images and text prompts at the same time
from sqlitedict import SqliteDict
from models.deepl_model import TranslationModel
from services.environment_service import EnvService
from services.image_service import ImageService
from services.text_service import TextService
ALLOWED_GUILDS = EnvService.get_allowed_guilds()
def build_translation_embed( text, target_language):
"""Build an embed for the translation"""
embed = discord.Embed(
title=f"Translation results",
color=0x311432,
)
embed.add_field(name="Original text", value=text, inline=False)
embed.add_field(name="Target language", value=target_language, inline=False)
return embed
class TranslationService(discord.Cog, name="TranslationService"):
"""Cog containing a draw commands and file management for saved images"""
def __init__(
self, bot, translation_model,
):
super().__init__()
self.bot = bot
self.translation_model = translation_model
# Make a mapping of all the country codes and their full country names:
def build_supported_language_embed(self):
"""Build an embed for the translation"""
embed = discord.Embed(
title=f"Translator supported languages",
color=0x311432,
)
# Add the list of supported languages in a nice format
embed.add_field(
name="Languages",
value=", ".join(
[
f"{name}"
for name in TranslationModel.get_all_country_names()
]
),
inline=False,
)
return embed
async def translate_command(self, ctx, text, target_language):
"""Delete all local images"""
await ctx.defer()
# TODO Add pagination!
if target_language.lower().strip() not in TranslationModel.get_all_country_names(lower=True):
await ctx.respond(
f"The language {target_language} is not recognized or supported. Please use `/languages` to see the list of supported languages."
)
return
try:
response = await self.translation_model.send_translate_request(text, TranslationModel.get_country_code_from_name(target_language))
except aiohttp.ClientResponseError as e:
await ctx.respond(f"There was an error with the DeepL API: {e.message}")
return
await ctx.respond(embed=build_translation_embed(text, response))
async def translate_action(self, ctx, message):
await ctx.defer(ephemeral=True)
selection_message = await ctx.respond("Select language", ephemeral=True, delete_after=60)
await selection_message.edit(view=TranslateView(self.translation_model, message, selection_message))
async def languages_command(self, ctx):
"""Show all languages supported for translation"""
await ctx.defer()
await ctx.respond(embed=self.build_supported_language_embed())
class TranslateView(discord.ui.View):
def __init__(self, translation_model, message, selection_message):
super().__init__()
self.translation_model = translation_model
self.message = message
self.selection_message = selection_message
@discord.ui.select( # the decorator that lets you specify the properties of the select menu
placeholder = "Language", # the placeholder text that will be displayed if nothing is selected
min_values = 1, # the minimum number of values that must be selected by the users
max_values = 1, # the maximum number of values that can be selected by the users
options = [ # the list of options from which users can choose, a required field
discord.SelectOption(
label=name,
) for name in TranslationModel.get_all_country_names()
]
)
async def select_callback(self, select, interaction): # the function called when the user is done selecting options
try:
response = await self.translation_model.send_translate_request(self.message.content, TranslationModel.get_country_code_from_name(select.values[0]))
await self.message.reply(mention_author=False, embed=build_translation_embed(self.message.content, response))
await self.selection_message.delete()
except aiohttp.ClientResponseError as e:
await interaction.response.send_message(f"There was an error with the DeepL API: {e.message}", ephemeral=True, delete_after=15)
return
except Exception as e:
await interaction.response.send_message(f"There was an error: {e}", ephemeral=True, delete_after=15)
return

@ -13,6 +13,8 @@ from cogs.image_service_cog import DrawDallEService
from cogs.prompt_optimizer_cog import ImgPromptOptimizer
from cogs.moderations_service_cog import ModerationsService
from cogs.commands import Commands
from cogs.translation_service_cog import TranslationService
from models.deepl_model import TranslationModel
from services.pinecone_service import PineconeService
from services.deletion_service import Deletion
@ -23,7 +25,7 @@ from services.environment_service import EnvService
from models.openai_model import Model
__version__ = "7.1"
__version__ = "8.0"
if sys.platform == "win32":
@ -56,7 +58,6 @@ if PINECONE_TOKEN:
pinecone_service = PineconeService(pinecone.Index(PINECONE_INDEX))
print("Got the pinecone service")
#
# Message queueing for the debug service, defer debug messages to be sent later so we don't hit rate limits.
#
@ -146,6 +147,10 @@ async def main():
)
)
if EnvService.get_deepl_token():
bot.add_cog(TranslationService(bot, TranslationModel()))
print("The translation service is enabled.")
bot.add_cog(
Commands(
bot,
@ -157,9 +162,11 @@ async def main():
bot.get_cog("DrawDallEService"),
bot.get_cog("ImgPromptOptimizer"),
bot.get_cog("ModerationsService"),
bot.get_cog("TranslationService"),
)
)
apply_multicog(bot)
await bot.start(os.getenv("DISCORD_TOKEN"))

@ -3,6 +3,8 @@ import os
import re
import discord
from models.deepl_model import TranslationModel
from services.usage_service import UsageService
from models.openai_model import Model
from services.environment_service import EnvService
@ -72,6 +74,17 @@ class Settings_autocompleter:
if channel.name.startswith(ctx.value.lower())
]
class Translations_autocompleter:
"""autocompleter for the translations command"""
async def get_languages(ctx: discord.AutocompleteContext):
"""gets valid values for the language option"""
return [
language
for language in TranslationModel.get_all_country_names()
if language.lower().startswith(ctx.value.lower())
]
class File_autocompleter:
"""Autocompleter for the opener command"""

@ -6,6 +6,7 @@ from typing import Callable
ADMIN_ROLES = EnvService.get_admin_roles()
DALLE_ROLES = EnvService.get_dalle_roles()
GPT_ROLES = EnvService.get_gpt_roles()
TRANSLATOR_ROLES = EnvService.get_translator_roles()
ALLOWED_GUILDS = EnvService.get_allowed_guilds()
@ -61,3 +62,20 @@ class Check:
return True
return inner
@staticmethod
def check_translator_roles() -> Callable:
async def inner(ctx: discord.ApplicationContext):
if TRANSLATOR_ROLES == [None]:
return True
if not any(role.name.lower() in TRANSLATOR_ROLES for role in ctx.user.roles):
await ctx.defer(ephemeral=True)
await ctx.respond(
f"You don't have permission, list of roles is {TRANSLATOR_ROLES}",
ephemeral=True,
delete_after=10,
)
return False
return True
return inner

@ -0,0 +1,98 @@
import os
import traceback
import aiohttp
import backoff
COUNTRY_CODES = {
"BG": "Bulgarian",
"CS": "Czech",
"DA": "Danish",
"DE": "German",
"EL": "Greek",
"EN": "English",
"ES": "Spanish",
"FI": "Finnish",
"FR": "French",
"HU": "Hungarian",
"ID": "Indonesian",
"IT": "Italian",
"JA": "Japanese",
"LT": "Lithuanian",
"LV": "Latvian",
"NL": "Dutch",
"PL": "Polish",
"PT": "Portuguese",
"RO": "Romanian",
"RU": "Russian",
"SK": "Slovak",
"SV": "Swedish",
"TR": "Turkish",
"UK": "Ukrainian",
"ZH": "Chinese (simplified)",
}
class TranslationModel:
def __init__(self):
self.deepl_token = os.getenv("DEEPL_TOKEN")
def backoff_handler(details):
print(
f"Backing off {details['wait']:0.1f} seconds after {details['tries']} tries calling function {details['target']} | "
f"{details['exception'].status}: {details['exception'].message}"
)
@backoff.on_exception(
backoff.expo,
aiohttp.ClientResponseError,
factor=3,
base=5,
max_tries=4,
on_backoff=backoff_handler,
)
async def send_translate_request(self, text, translate_language):
print("The text is: ", text)
print("The language is: ", translate_language)
print("The token is ", self.deepl_token)
async with aiohttp.ClientSession(raise_for_status=True) as session:
payload = {
"text": text,
"target_lang": translate_language,
}
# Instead of sending as json, we want to send as regular post params
headers = {
"Authorization": f"DeepL-Auth-Key {self.deepl_token}",
}
async with session.post(
"https://api-free.deepl.com/v2/translate", params=payload, headers=headers
) as resp:
response = await resp.json()
print(response)
try:
return response["translations"][0]["text"]
except Exception:
print(response)
traceback.print_exc()
return response
@staticmethod
def get_all_country_names(lower=False):
"""Get a list of all the country names"""
return list(COUNTRY_CODES.values()) if not lower else [name.lower() for name in COUNTRY_CODES.values()]
@staticmethod
def get_all_country_codes():
"""Get a list of all the country codes"""
return list(COUNTRY_CODES.keys())
@staticmethod
def get_country_name_from_code(code):
"""Get the country name from the code"""
return COUNTRY_CODES[code]
@staticmethod
def get_country_code_from_name(name):
"""Get the country code from the name"""
for code, country_name in COUNTRY_CODES.items():
if country_name.lower().strip() == name.lower().strip():
return code

@ -138,6 +138,32 @@ class EnvService:
)
return dalle_roles
@staticmethod
def get_translator_roles():
# DALLE_ROLES is a comma separated list of string roles
# It can also just be one role
# Read these allowed roles and return as a list of strings
try:
translator_roles = os.getenv("TRANSLATOR_ROLES")
except Exception:
translator_roles = None
if translator_roles is None:
print(
"TRANSLATOR_ROLES is not defined properly in the environment file!"
"Please copy your server's role and put it into TRANSLATOR in the .env file."
'For example a line should look like: `TRANSLATOR="Translate"`'
)
print("Defaulting to allowing all users to use Translator commands...")
return [None]
translator_roles = (
translator_roles.lower().split(",")
if "," in translator_roles
else [translator_roles.lower()]
)
return translator_roles
@staticmethod
def get_gpt_roles():
# GPT_ROLES is a comma separated list of string roles
@ -204,3 +230,11 @@ class EnvService:
return Path(user_key_db_path)
except Exception:
return None
@staticmethod
def get_deepl_token():
try:
deepl_token = os.getenv("DEEPL_TOKEN")
return deepl_token
except Exception:
return None

Loading…
Cancel
Save