2023-04-19 16:14:20 +00:00
import logging
2023-03-06 15:20:37 +00:00
import os
2023-04-19 16:14:20 +00:00
import tempfile
from functools import wraps
from io import BytesIO
2023-03-06 15:20:37 +00:00
import openai
2023-04-19 16:14:20 +00:00
from aiogram import Bot , Dispatcher , types
from aiogram . contrib . middlewares . logging import LoggingMiddleware
from aiogram . types import InlineKeyboardButton , InlineKeyboardMarkup , ParseMode
from aiogram . types . input_file import InputFile
from aiogram . utils import executor
2023-03-22 11:44:08 +00:00
from dotenv import load_dotenv
2023-04-19 16:14:20 +00:00
from gtts import gTTS
import pyttsx3
2023-03-06 15:20:37 +00:00
from pydub import AudioSegment
2023-04-19 16:14:20 +00:00
import database
2023-04-19 10:53:18 +00:00
2023-03-06 15:20:37 +00:00
logging . basicConfig (
2023-04-19 16:14:20 +00:00
format = ' %(asctime)s - %(levelname)s - %(message)s ' ,
2023-03-06 15:20:37 +00:00
level = logging . INFO
)
logger = logging . getLogger ( __name__ )
2023-04-19 10:53:18 +00:00
# Envrionment Variables Load
2023-03-22 11:44:08 +00:00
load_dotenv ( )
if os . environ . get ( " OPENAI_API_KEY " ) is None :
print ( " OpenAI_API_KEY is not set in.env file or OPENAI_API_KEY environment variable is not set " )
exit ( 1 )
2023-04-19 16:14:20 +00:00
BOT_TOKEN = os . getenv ( " BOT_TOKEN " )
bot = Bot ( token = BOT_TOKEN )
dp = Dispatcher ( bot )
dp . middleware . setup ( LoggingMiddleware ( ) )
ALLOWED_USERS = os . environ . get ( " BOT_ALLOWED_USERS " ) . split ( " , " )
SYSTEM_PROMPT = os . environ . get ( " CHATGPT_SYSTEM_PROMPT " )
TEMPERATURE = os . environ . get ( " CHATGPT_TEMPERATURE " )
MODEL = os . environ . get ( " OPENAI_MODEL " )
WHISPER_TO_CHAT = bool ( int ( os . environ . get ( " WHISPER_TO_CHAT " ) ) )
ENABLE_GOOGLE_TTS = bool ( int ( os . environ . get ( " ENABLE_GOOGLE_TTS " ) ) )
MAX_USER_CONTEXT = int ( os . environ . get ( " CHATGPT_MAX_USER_CONTEXT " ) )
2023-03-22 11:44:08 +00:00
openai . api_key = os . environ . get ( " OPENAI_API_KEY " )
2023-04-19 10:53:18 +00:00
2023-04-19 16:14:20 +00:00
async def getUserData ( chat_id ) :
2023-04-19 10:53:18 +00:00
user_data = database . get_user ( chat_id )
if not user_data :
user_data = {
" context " : [ ] ,
" usage " : { " chatgpt " : 0 , " whisper " : 0 , " dalle " : 0 } ,
" options " : {
" whisper_to_chat " : WHISPER_TO_CHAT ,
" assistant_voice_chat " : False ,
" temperature " : float ( TEMPERATURE ) ,
" max-context " : MAX_USER_CONTEXT
}
2023-03-06 15:20:37 +00:00
}
2023-04-19 10:53:18 +00:00
database . add_user ( chat_id , user_data )
user_data = database . get_user ( chat_id )
return user_data
2023-03-06 15:20:37 +00:00
2023-04-19 16:14:20 +00:00
def generate_settings_markup ( chat_id : str ) - > InlineKeyboardMarkup :
keyboard = [
[
InlineKeyboardButton ( " Increase Temperature " , callback_data = f " setting_inc_temp_ { chat_id } " ) ,
InlineKeyboardButton ( " Decrease Temperature " , callback_data = f " setting_dec_temp_ { chat_id } " )
] ,
[
InlineKeyboardButton ( " Enable Whisper " , callback_data = f " setting_en_whisper_ { chat_id } " ) ,
InlineKeyboardButton ( " Disable Whisper " , callback_data = f " setting_dis_whisper_ { chat_id } " )
] ,
[
InlineKeyboardButton ( " Enable assistant voice " , callback_data = f " setting_en_voice_ { chat_id } " ) ,
InlineKeyboardButton ( " Disable assistant voice " , callback_data = f " setting_dis_voice_ { chat_id } " )
] ,
[
InlineKeyboardButton ( " Increase Context " , callback_data = f " setting_inc_context_ { chat_id } " ) ,
InlineKeyboardButton ( " Decrease Context " , callback_data = f " setting_dec_context_ { chat_id } " )
]
]
return InlineKeyboardMarkup ( inline_keyboard = keyboard )
async def text_to_voice ( text : str ) - > BytesIO :
if ENABLE_GOOGLE_TTS :
tts = gTTS ( text )
with tempfile . NamedTemporaryFile ( mode = ' wb ' , suffix = ' .mp3 ' , delete = False ) as mp3_file :
temp_mp3_filename = mp3_file . name
tts . save ( temp_mp3_filename )
else :
engine = pyttsx3 . init ( ) # PyTTSX3 Engine
engine . setProperty ( ' rate ' , 150 )
with tempfile . NamedTemporaryFile ( mode = ' wb ' , suffix = ' .mp3 ' , delete = False ) as mp3_file :
temp_mp3_filename = mp3_file . name
engine . save_to_file ( text , temp_mp3_filename )
engine . runAndWait ( )
mp3_audio = AudioSegment . from_file ( temp_mp3_filename , format = " mp3 " )
with tempfile . NamedTemporaryFile ( mode = " wb " , suffix = " .ogg " , delete = False ) as ogg_file :
temp_ogg_filename = ogg_file . name
mp3_audio . export ( temp_ogg_filename , format = " ogg " )
with open ( temp_ogg_filename , " rb " ) as audio_file :
_ = InputFile ( audio_file )
voice_data = BytesIO ( audio_file . read ( ) )
os . remove ( temp_mp3_filename )
os . remove ( temp_ogg_filename )
voice_data . seek ( 0 )
return voice_data
2023-03-06 15:20:37 +00:00
def restricted ( func ) :
@wraps ( func )
2023-04-19 16:14:20 +00:00
async def wrapped ( message , * args , * * kwargs ) :
user_id = str ( message . chat . id )
if user_id not in ALLOWED_USERS :
2023-03-06 15:20:37 +00:00
if " * " != ALLOWED_USERS [ 0 ] :
2023-04-19 16:14:20 +00:00
print ( f " Unauthorized access denied for { user_id } . " )
2023-03-06 15:20:37 +00:00
return
else :
2023-04-19 16:14:20 +00:00
_ = await getUserData ( user_id )
return await func ( message , * args , * * kwargs )
2023-03-06 15:20:37 +00:00
return wrapped
2023-04-19 16:14:20 +00:00
2023-04-19 10:53:18 +00:00
async def messageGPT ( text : str , chat_id : str , user_name = " User " ) :
2023-04-19 16:14:20 +00:00
await bot . send_chat_action ( chat_id , action = types . ChatActions . TYPING )
2023-04-19 10:53:18 +00:00
user_data = await getUserData ( chat_id )
user_data [ ' context ' ] . append ( { " role " : " user " , " content " : text } )
if len ( user_data [ ' context ' ] ) > user_data [ " options " ] [ " max-context " ] :
user_data [ ' context ' ] . pop ( 0 )
2023-04-12 10:42:28 +00:00
try :
response = openai . ChatCompletion . create (
model = MODEL ,
2023-04-19 10:53:18 +00:00
messages = [ { " role " : " system " , " content " : f " You are chatting with { user_name } . { SYSTEM_PROMPT } " } ] + user_data [ ' context ' ] ,
temperature = user_data [ " options " ] [ " temperature " ] ,
2023-04-12 10:42:28 +00:00
)
2023-04-19 10:53:18 +00:00
except Exception as e :
print ( e )
2023-04-19 16:14:20 +00:00
return f " There was a problem with OpenAI, so I can ' t answer you: \n \n { e } "
2023-04-12 10:42:28 +00:00
2023-04-19 16:14:20 +00:00
assistant_message = response . get ( ' choices ' , [ { } ] ) [ 0 ] . get ( ' message ' , { " content " : None } ) . get ( " content " , " There was a problem with OpenAI. Maybe your prompt is forbidden? They like to censor a lot! " )
2023-04-12 10:42:28 +00:00
2023-04-19 10:53:18 +00:00
user_data [ ' context ' ] . append ( { " role " : " assistant " , " content " : assistant_message } )
if len ( user_data [ ' context ' ] ) > user_data [ " options " ] [ " max-context " ] :
user_data [ ' context ' ] . pop ( 0 )
2023-04-12 10:42:28 +00:00
2023-04-19 16:14:20 +00:00
user_data [ " usage " ] [ ' chatgpt ' ] + = int ( response . get ( ' usage ' , { " total_tokens " : 0 } ) [ " total_tokens " ] )
2023-04-19 10:53:18 +00:00
database . update_user ( chat_id , user_data )
2023-04-12 10:42:28 +00:00
return assistant_message
2023-04-19 16:14:20 +00:00
@dp.message_handler ( commands = [ ' start ' ] )
2023-03-06 15:20:37 +00:00
@restricted
2023-04-19 16:14:20 +00:00
async def start ( message : types . Message ) :
_ = await getUserData ( message . chat . id )
await message . reply ( " Hello, how can I assist you today? " )
@dp.message_handler ( commands = [ ' clear ' ] , content_types = [ ' text ' ] )
@restricted
async def clear ( message : types . Message ) - > None :
chat_id = str ( message . chat . id )
user_data = await getUserData ( chat_id )
if user_data :
user_data [ " context " ] = [ ]
database . update_user ( chat_id , user_data )
print ( f " Cleared context for { message . from_user . full_name } " )
await message . reply ( " Your message context history was cleared. " )
@dp.message_handler ( commands = [ ' usage ' ] )
@restricted
async def usage ( message : types . Message ) - > None :
chat_id = str ( message . chat . id )
user_data = database . get_user ( chat_id )
user_usage = user_data [ " usage " ]
total_usage = database . get_total_usage ( )
user_spent = round ( ( ( ( user_usage [ ' chatgpt ' ] / 750 ) * 0.002 ) + ( float ( user_usage [ ' dalle ' ] ) * 0.02 ) + ( ( user_usage [ ' whisper ' ] / 60.0 ) * 0.006 ) ) , 4 )
total_spent = round ( ( ( ( total_usage [ ' chatgpt ' ] / 750 ) * 0.002 ) + ( float ( total_usage [ ' dalle ' ] ) * 0.02 ) + ( ( total_usage [ ' whisper ' ] / 60.0 ) * 0.006 ) ) , 4 )
user_percentage = ( user_spent / total_spent ) * 100 if total_spent > 0 else 0
info_message = f """ User: { message . from_user . full_name }
- Used ~ { user_usage [ " chatgpt " ] } tokens with ChatGPT .
- Generated { user_usage [ " dalle " ] } images with DALL - E .
- Transcribed { round ( float ( user_usage [ " whisper " ] ) / 60.0 , 2 ) } min with Whisper .
Total spent : $ { user_spent } ( { user_percentage : .2 f } % of total )
Total usage :
- ChatGPT tokens : { total_usage [ " chatgpt " ] }
- DALL - E images : { total_usage [ " dalle " ] }
- Whisper transcription : { round ( float ( total_usage [ " whisper " ] ) / 60.0 , 2 ) } min
Total spent : $ { total_spent } """
await message . reply ( info_message )
2023-03-06 15:20:37 +00:00
2023-04-19 16:14:20 +00:00
@dp.message_handler ( lambda message : message . chat . type == types . ChatType . PRIVATE , content_types = [ ' text ' ] , regexp = ' ^/imagine ' )
@restricted
async def imagine ( message : types . Message ) :
await bot . send_chat_action ( message . chat . id , action = types . ChatActions . TYPING )
user_data = await getUserData ( message . chat . id )
2023-04-19 10:53:18 +00:00
user_data [ " usage " ] [ ' dalle ' ] + = 1
2023-04-19 16:14:20 +00:00
database . update_user ( message . chat . id , user_data )
2023-03-23 08:14:32 +00:00
2023-03-06 15:20:37 +00:00
response = openai . Image . create (
2023-04-19 16:14:20 +00:00
prompt = message . text ,
2023-03-06 15:20:37 +00:00
n = 1 ,
size = " 1024x1024 "
)
try :
image_url = response [ ' data ' ] [ 0 ] [ ' url ' ]
2023-04-19 16:14:20 +00:00
await message . reply ( image_url )
2023-04-19 10:53:18 +00:00
except Exception as e :
print ( e )
2023-04-19 16:14:20 +00:00
await message . reply ( " Error generating. Your prompt may contain text that is not allowed by OpenAI safety system. " )
2023-03-06 15:20:37 +00:00
2023-04-19 16:14:20 +00:00
@dp.message_handler ( content_types = [ ' photo ' , ' video ' , ' audio ' , ' voice ' ] )
2023-03-06 15:20:37 +00:00
@restricted
2023-04-19 16:14:20 +00:00
async def attachment ( message : types . Message ) :
chat_id = message . chat . id
2023-04-19 10:53:18 +00:00
user_data = await getUserData ( chat_id )
2023-04-19 16:14:20 +00:00
await bot . send_chat_action ( chat_id , action = types . ChatActions . TYPING )
2023-03-22 11:44:08 +00:00
transcript = { ' text ' : ' ' }
2023-04-12 10:42:28 +00:00
audioMessage = False
2023-04-19 16:14:20 +00:00
if message . voice :
user_data [ " usage " ] [ ' whisper ' ] + = message . voice . duration
file_id = message . voice . file_id
2023-03-22 11:44:08 +00:00
file_format = " ogg "
2023-04-12 10:42:28 +00:00
audioMessage = True
2023-04-19 16:14:20 +00:00
elif message . video :
user_data [ " usage " ] [ ' whisper ' ] + = message . video . duration
file_id = message . video . file_id
2023-03-22 11:44:08 +00:00
file_format = " mp4 "
2023-04-19 16:14:20 +00:00
elif message . audio :
user_data [ " usage " ] [ ' whisper ' ] + = message . audio . duration
file_id = message . audio . file_id
2023-03-22 11:44:08 +00:00
file_format = " mp3 "
else :
2023-04-19 16:14:20 +00:00
await message . reply ( " Can ' t handle such file. Reason: unknown. " )
2023-03-22 11:44:08 +00:00
return
2023-04-19 16:14:20 +00:00
file = await bot . get_file ( file_id )
2023-04-19 19:27:25 +00:00
user_id = message . chat . id
2023-04-19 16:14:20 +00:00
await file . download ( f " { user_id } . { file_format } " )
2023-03-22 11:44:08 +00:00
if file_format == " ogg " :
ogg_audio = AudioSegment . from_file ( f " { user_id } .ogg " , format = " ogg " )
ogg_audio . export ( f " { user_id } .mp3 " , format = " mp3 " )
os . remove ( f " { user_id } .ogg " )
file_format = " mp3 "
with open ( f " { user_id } . { file_format } " , " rb " ) as audio_file :
2023-03-07 20:06:14 +00:00
try :
2023-04-19 16:14:20 +00:00
await bot . send_chat_action ( chat_id , action = types . ChatActions . TYPING )
2023-03-22 11:44:08 +00:00
transcript = openai . Audio . transcribe ( " whisper-1 " , audio_file )
2023-04-19 10:53:18 +00:00
except Exception as e :
print ( e )
2023-04-19 16:14:20 +00:00
await message . reply ( " Transcript failed. " )
2023-03-22 11:44:08 +00:00
os . remove ( f " { user_id } . { file_format } " )
return
os . remove ( f " { user_id } . { file_format } " )
if transcript [ ' text ' ] == " " :
transcript [ ' text ' ] = " [Silence] "
2023-04-12 10:42:28 +00:00
2023-04-19 16:14:20 +00:00
chatGPT_response = False
2023-04-19 10:53:18 +00:00
if audioMessage and user_data [ " options " ] [ " whisper_to_chat " ] :
2023-04-19 16:14:20 +00:00
chatGPT_response = await messageGPT ( transcript [ ' text ' ] , str ( chat_id ) , message . from_user . full_name )
2023-04-12 15:03:09 +00:00
transcript [ ' text ' ] = " > " + transcript [ ' text ' ] + " \n \n " + chatGPT_response
2023-03-22 11:44:08 +00:00
2023-04-19 16:14:20 +00:00
await message . reply ( transcript [ ' text ' ] )
if user_data [ " options " ] [ " assistant_voice_chat " ] and chatGPT_response :
await bot . send_chat_action ( chat_id , action = types . ChatActions . TYPING )
voice_data = await text_to_voice ( chatGPT_response )
await message . reply_voice ( voice_data )
2023-04-19 10:53:18 +00:00
database . update_user ( str ( chat_id ) , user_data )
2023-03-22 11:44:08 +00:00
2023-03-06 15:20:37 +00:00
@restricted
2023-04-19 16:14:20 +00:00
@dp.message_handler ( commands = [ ' settings ' ] )
async def settings ( message : types . Message ) :
chat_id = str ( message . chat . id )
settings_markup = generate_settings_markup ( chat_id )
await message . reply ( text = ' Settings: ' , reply_markup = settings_markup )
2023-03-06 15:20:37 +00:00
2023-04-19 16:14:20 +00:00
async def settings_callback ( callback_query : types . CallbackQuery ) :
user_data = await getUserData ( callback_query . message . chat . id )
action , chat_id = callback_query . data . rsplit ( " _ " , 1 )
options = user_data [ " options " ]
2023-04-19 10:53:18 +00:00
2023-04-19 16:14:20 +00:00
if action . startswith ( " setting_inc_temp " ) :
options [ " temperature " ] = min ( options [ " temperature " ] + 0.1 , 1 )
elif action . startswith ( " setting_dec_temp " ) :
options [ " temperature " ] = max ( options [ " temperature " ] - 0.1 , 0 )
2023-04-19 10:53:18 +00:00
2023-04-19 16:14:20 +00:00
elif action . startswith ( " setting_en_whisper " ) :
options [ " whisper_to_chat " ] = True
elif action . startswith ( " setting_dis_whisper " ) :
options [ " whisper_to_chat " ] = False
2023-04-19 10:53:18 +00:00
2023-04-19 16:14:20 +00:00
elif action . startswith ( " setting_en_voice " ) :
options [ " assistant_voice_chat " ] = True
elif action . startswith ( " setting_dis_voice " ) :
options [ " assistant_voice_chat " ] = False
2023-04-19 10:53:18 +00:00
2023-04-19 16:14:20 +00:00
elif action . startswith ( " setting_inc_context " ) :
options [ " max-context " ] = min ( options [ " max-context " ] + 1 , MAX_USER_CONTEXT )
elif action . startswith ( " setting_dec_context " ) :
options [ " max-context " ] = max ( options [ " max-context " ] - 1 , 1 )
2023-04-19 10:53:18 +00:00
2023-04-19 16:14:20 +00:00
settings_markup = generate_settings_markup ( chat_id )
await callback_query . message . edit_text ( text = ' Choose a setting option: ' , reply_markup = settings_markup )
2023-04-19 10:53:18 +00:00
2023-04-19 16:14:20 +00:00
database . update_user ( chat_id , user_data )
settings_txt = f " Updated settings: \n \n Temperature: { options [ ' temperature ' ] } \n Whisper to Chat: { options [ ' whisper_to_chat ' ] } \n Assistant voice: { options [ ' assistant_voice_chat ' ] } \n Context Length: { options [ ' max-context ' ] } "
await callback_query . answer ( )
await callback_query . message . reply ( text = settings_txt )
2023-04-19 10:53:18 +00:00
2023-04-19 16:14:20 +00:00
@dp.message_handler ( lambda message : message . chat . type == types . ChatType . PRIVATE and not message . text . startswith ( " / " ) , content_types = [ ' text ' ] )
async def chat ( message : types . Message ) :
chat_id = str ( message . chat . id )
2023-03-06 15:20:37 +00:00
2023-04-19 16:14:20 +00:00
user_prompt = message . text
await bot . send_chat_action ( chat_id , action = types . ChatActions . TYPING )
assistant_message = await messageGPT ( user_prompt , chat_id , message . from_user . full_name )
2023-04-19 10:53:18 +00:00
2023-04-19 16:14:20 +00:00
await message . reply ( assistant_message , parse_mode = ParseMode . MARKDOWN )
2023-04-19 10:53:18 +00:00
2023-04-19 16:14:20 +00:00
user_data = await getUserData ( chat_id )
if user_data [ " options " ] [ " assistant_voice_chat " ] :
await bot . send_chat_action ( chat_id , action = types . ChatActions . TYPING )
voice_data = await text_to_voice ( assistant_message )
2023-04-19 10:53:18 +00:00
2023-04-19 16:14:20 +00:00
await message . reply_voice ( voice_data )
2023-04-12 10:42:28 +00:00
2023-03-06 15:20:37 +00:00
if __name__ == ' __main__ ' :
2023-04-19 10:53:18 +00:00
database . init_database ( )
2023-04-19 16:14:20 +00:00
2023-03-06 15:20:37 +00:00
try :
2023-04-19 16:14:20 +00:00
ALLOWED_USERS = os . environ . get ( " BOT_ALLOWED_USERS " ) . split ( " , " )
2023-04-19 10:53:18 +00:00
except ( Exception ) :
2023-04-19 16:14:20 +00:00
ALLOWED_USERS = ALLOWED_USERS
2023-03-06 15:20:37 +00:00
print ( f " Allowed users: { ALLOWED_USERS } " )
print ( f " System prompt: { SYSTEM_PROMPT } " )
2023-04-19 16:14:20 +00:00
print ( f " Google TTS: { ENABLE_GOOGLE_TTS } " )
2023-03-06 15:20:37 +00:00
2023-04-19 16:14:20 +00:00
# Register message handler and callback query handler for settings
dp . register_message_handler ( settings , commands = [ ' settings ' ] )
dp . register_callback_query_handler ( settings_callback , lambda c : c . data . startswith ( ' setting_ ' ) )
executor . start_polling ( dp , skip_updates = True )