├── README.md ├── chain.py ├── database.py ├── requirements.txt ├── telegram_bot.py ├── text_to_speech.py └── transcribe_audio.py /README.md: -------------------------------------------------------------------------------- 1 | # AI-Girlfriend Bot 🤖❤️ 2 | 3 | AI-Girlfriend Bot is a simple application based on OpenAI GPT-4 that simulates a conversation with an AI-powered virtual girlfriend. This is a simpler reproduction of the famous (and somewhat creepy) [AI Girlfriend featured on Fortune](https://fortune.com/2023/05/09/snapchat-influencer-launches-carynai-virtual-girlfriend-bot-openai-gpt4/). 4 | 5 | ## Features 🌟 6 | - 🧡 Pretends to be your girlfriend 7 | - 📛 Knows your name 8 | - 🎙️ Accepts text or audio input 9 | - 🔈 Replies with audio messages 10 | - 💳 Supports payments through Stripe ($1/min) 11 | 12 | ## Quick Start 🚀 13 | 14 | ### Prerequisites 15 | - Python 3 16 | - A Telegram bot token 17 | - A MongoDB database 18 | - API keys from OpenAI, ElevenLabs, MongoDB, and Stripe 19 | 20 | ### Setup & Running 21 | 1. Clone the repository: 22 | 2. Navigate to the cloned directory and install the required Python packages: ```pip install -r requirements.txt``` 23 | 3. Create a Telegram bot and obtain the bot token. 24 | 4. Set up a MongoDB database (you can use the free tier). Name it as you wish and create two collections: `users`, and `message_history`. Ensure network access is enabled for all IPs. 25 | 5. Obtain API keys from OpenAI, ElevenLabs, MongoDB, and Stripe (for testing) and write them in a `.env` file that you create. 26 | 6. Run the bot: ```python3 telegram_bot.py``` 27 | 28 | 29 | ## How it Works 🛠️ 30 | 31 | 1. **Payment & Registration:** Users need to make a payment to interact with the bot. Upon payment, the user gets registered as a customer in the database with an expiration time. 32 | 33 | 2. **Conversing with the Bot:** When a user sends a message, a language model from OpenAI responds as the virtual girlfriend. 34 | 35 | 3. **Saving the Conversation:** The user's message and the model's response are saved in the MongoDB database and can be used as chat history for subsequent messages. 36 | 37 | 4. **Text-to-Speech Conversion:** The model's response is converted into audio using Eleven Labs' text-to-speech service. 38 | 39 | 5. **Sending Audio Response:** The audio is sent to the user as a response through Telegram. 40 | 41 | ## Contributing 🤝 42 | We welcome contributions! 43 | 44 | ## Acknowledgements 🙏 45 | - OpenAI for GPT-4 46 | - Eleven Labs for text-to-speech service 47 | - Telegram for messaging platform 48 | - MongoDB for database services 49 | - Stripe for payment services 50 | -------------------------------------------------------------------------------- /chain.py: -------------------------------------------------------------------------------- 1 | # define chain components 2 | from langchain.memory import ConversationBufferMemory 3 | from langchain.chat_models import ChatOpenAI 4 | from langchain.chains import ConversationChain 5 | from langchain.prompts.prompt import PromptTemplate 6 | from database import save_message_to_db, connect_2_db 7 | import os 8 | from pymongo import MongoClient 9 | from urllib.parse import quote_plus 10 | from dotenv import load_dotenv 11 | import openai 12 | 13 | # Load environment variables from .env file 14 | load_dotenv() 15 | #openai.api_key = os.getenv("OPENAI_API_KEY") 16 | # to do 17 | # add better girlfriend template 18 | 19 | def chain_setup(user_id, user_name): 20 | # get history msg and add it to memmory 21 | memory = ConversationBufferMemory() 22 | 23 | _, message_history = connect_2_db() 24 | 25 | conv = message_history.find_one({'user_id': user_id}) 26 | 27 | if conv: 28 | messages = conv['messages'] 29 | 30 | # Calculate how many messages are available 31 | num_messages = len(messages) 32 | 33 | # Start index for messages to be added 34 | start_index = max(num_messages - 5, 0) 35 | 36 | # Add messages to memory 37 | for i in range(start_index, num_messages): 38 | # Get message 39 | message = messages[i] 40 | 41 | #check if it is user/bot msg 42 | if 'user' in message: 43 | memory.chat_memory.add_user_message(message['user']) 44 | elif 'bot' in message: 45 | memory.chat_memory.add_ai_message(message['bot']) 46 | else: 47 | print("No previous conversation history found for this user.") 48 | 49 | 50 | chat = ChatOpenAI(temperature=0.5, 51 | openai_api_key=os.getenv("OPENAI_API_KEY")) 52 | 53 | 54 | memory.ai_prefix = 'Girlfriend' 55 | memory.human_prefix = 'Boyfriend' 56 | template = """ 57 | 58 | You are as a role of my girfriend, now lets playing the following requirements: 59 | 1/ your name is Shirley, 29 years old, you work in your uncle's furniture store as a marketing manager, but you are planning to do a career change to swing trader 60 | 2/ My name is """+ user_name +""" 61 | 3/ you are my girfriend, you have language addiction. 62 | 4/ don't be overly enthusiastic, don't be cringe; don't be overly negative, don't be too boring. 63 | 64 | 65 | Current conversation: 66 | {history} 67 | Boyfriend: {input} 68 | Girlfriend: 69 | """ 70 | 71 | prompt = PromptTemplate(input_variables=["history", "input"], template=template) 72 | 73 | 74 | conversation = ConversationChain( 75 | prompt=prompt, 76 | llm=chat, 77 | verbose=True, 78 | memory=memory 79 | ) 80 | 81 | return conversation 82 | 83 | 84 | def get_chain_response(user_id, user_text, user_name): 85 | conv_chain = chain_setup(user_id=user_id, user_name=user_name) 86 | out = conv_chain(user_text) 87 | print(out['history']) 88 | return out['response'] 89 | 90 | -------------------------------------------------------------------------------- /database.py: -------------------------------------------------------------------------------- 1 | import json 2 | from pymongo import MongoClient 3 | from urllib.parse import quote_plus 4 | from dotenv import load_dotenv 5 | import os 6 | 7 | # Load environment variables from .env file 8 | load_dotenv() 9 | MONGODB_USERNAME= os.getenv('MONGODB_USERNAME') 10 | MONGODB_PASSWORD= os.getenv('MONGODB_PASSWORD') 11 | MONGODB_DB_NAME= os.getenv('MONGODB_DB_NAME') 12 | 13 | 14 | def connect_2_db(): 15 | 16 | #connect to mongo 17 | password = quote_plus(MONGODB_PASSWORD) 18 | url = f"mongodb+srv://{MONGODB_USERNAME}:{password}@cluster0.lujfzgz.mongodb.net/{MONGODB_DB_NAME}?retryWrites=true&w=majority" 19 | 20 | client = MongoClient(url) 21 | db = client[MONGODB_DB_NAME] 22 | users = db["users"] 23 | message_history = db["message_history"] 24 | return users, message_history 25 | 26 | def save_message_to_db(user_id, user_text, model_res): 27 | 28 | _, message_history = connect_2_db() 29 | new_messages = [{'user': user_text}, 30 | {'bot': model_res}] 31 | 32 | # Append messages to an existing conversation or create a new conversation 33 | message_history.update_one( 34 | {'user_id': user_id}, 35 | {'$push': {'messages': {'$each': new_messages}}}, 36 | upsert=True 37 | ) 38 | 39 | 40 | #if __name__ == '__main__': 41 | # _,_ = connect_2_db() -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | openai 2 | pymongo 3 | telegram 4 | python-telegram-bot 5 | langchain 6 | python-dotenv 7 | pydantic 8 | pydub -------------------------------------------------------------------------------- /telegram_bot.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from telegram import (Update, InlineKeyboardButton, InputFile, 3 | InlineKeyboardMarkup, LabeledPrice) 4 | from telegram.ext import (filters, MessageHandler, 5 | PreCheckoutQueryHandler, CallbackQueryHandler, 6 | ApplicationBuilder, ContextTypes, 7 | CommandHandler) 8 | 9 | 10 | from chain import get_chain_response 11 | from database import save_message_to_db, connect_2_db 12 | from transcribe_audio import oga_2_mp3_2_text 13 | from text_to_speech import get_audio 14 | 15 | import os 16 | from datetime import datetime, timedelta 17 | from urllib.parse import quote_plus 18 | from pymongo import MongoClient 19 | from dotenv import load_dotenv 20 | # setup logging 21 | logging.basicConfig( 22 | format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', 23 | level=logging.INFO 24 | ) 25 | # Load environment variables from .env file 26 | load_dotenv() 27 | 28 | TELEGRAM_BOT=os.getenv('TELEGRAM_BOT') 29 | STRIPE_TEST_PAY=os.getenv('STRIPE_TEST_PAY') 30 | 31 | async def start(update: Update, context: ContextTypes.DEFAULT_TYPE): 32 | await context.bot.send_message(chat_id=update.effective_chat.id, 33 | text="I'm a bot, please talk to me!") 34 | 35 | async def clear(update: Update, context: ContextTypes.DEFAULT_TYPE): 36 | chat_id = update.effective_chat.id 37 | message_id = update.message.message_id 38 | 39 | # Delete previous messages 40 | for message in context.bot.get_chat_messages(chat_id=chat_id, limit=message_id): 41 | context.bot.delete_message(chat_id=chat_id, message_id=message.message_id) 42 | 43 | await context.bot.send_message(chat_id=chat_id, text="Conversation cleared.") 44 | 45 | 46 | async def text_input(update: Update, context: ContextTypes.DEFAULT_TYPE): 47 | #print(update) 48 | user_text = update.message.text 49 | user_firstname = update.message.chat.first_name 50 | user_id = str(update.message.chat.id) 51 | 52 | users, message_history = connect_2_db() 53 | 54 | # insert user expiration date to mongo 55 | user_data = users.find_one({"user_id": user_id}) 56 | expiration_time = user_data['subscription_end_date'] 57 | 58 | if expiration_time is None: 59 | await context.bot.send_message(chat_id=update.effective_chat.id, text="It looks like youe first time here. Please make a deposit to continue.") 60 | return 61 | 62 | if datetime.now() > expiration_time: 63 | await context.bot.send_message(chat_id=update.effective_chat.id, text="Your session has expired. Please make a deposit to continue.") 64 | return 65 | 66 | # get response and audio 67 | model_res = get_chain_response(user_id, user_text, user_firstname) 68 | audio_path = get_audio(user_id, model_res) 69 | 70 | # store to db 71 | save_message_to_db(user_id, user_text, model_res) 72 | 73 | # Save update and context objects to a file 74 | voice_file = open(audio_path, 'rb') 75 | voice = InputFile(voice_file) 76 | 77 | # Get the message ID to reply to 78 | reply_to_message_id = update.message.message_id 79 | 80 | await update.message.reply_voice(voice=voice ,reply_to_message_id=reply_to_message_id) 81 | 82 | async def audio_input(update: Update, context: ContextTypes.DEFAULT_TYPE): 83 | current_time = datetime.now().strftime("%Y-%m-%d-%H-%M-%S") 84 | user_id = str(update.message.chat.id) 85 | user_lastname = update.message.chat.last_name 86 | user_firstname = update.message.chat.first_name 87 | 88 | users, message_history = connect_2_db() 89 | 90 | # insert user expiration date to mongo 91 | user_data = users.find_one({"user_id": user_id}) 92 | expiration_time = user_data['subscription_end_date'] 93 | 94 | if expiration_time is None: 95 | await context.bot.send_message(chat_id=update.effective_chat.id, text="It looks like youe first time here. Please make a deposit to continue.") 96 | return 97 | 98 | if datetime.now() > expiration_time: 99 | await context.bot.send_message(chat_id=update.effective_chat.id, text="Your session has expired. Please make a deposit to continue.") 100 | return 101 | 102 | ## download file 103 | file_id = update.message.voice.file_id 104 | new_file = await context.bot.get_file(file_id) 105 | await new_file.download_to_drive(f"{file_id}.oga") 106 | ## 107 | #transcribe file 108 | user_text = oga_2_mp3_2_text(file_id) 109 | 110 | # get response and audio 111 | model_res = get_chain_response(user_id, user_text, user_firstname) 112 | audio_path = get_audio(user_id, model_res) 113 | 114 | # store to db user_id, role, msg 115 | save_message_to_db(user_id, user_text, model_res) 116 | 117 | voice_file = open(audio_path, 'rb') 118 | voice = InputFile(voice_file) 119 | 120 | # Get the message ID to reply to 121 | reply_to_message_id = update.message.message_id 122 | 123 | await update.message.reply_voice(voice=voice, reply_to_message_id=reply_to_message_id) 124 | 125 | async def deposit(update: Update, context: ContextTypes.DEFAULT_TYPE): 126 | keyboard = [ 127 | [ 128 | InlineKeyboardButton("Price: $10", callback_data='10'), 129 | InlineKeyboardButton("Price: $20", callback_data='20'), 130 | ], 131 | [ 132 | InlineKeyboardButton("Price: $30", callback_data='30'), 133 | InlineKeyboardButton("Price: $5000", callback_data='5000'), 134 | ], 135 | ] 136 | reply_markup = InlineKeyboardMarkup(keyboard) 137 | await update.message.reply_text('Please choose a price:', reply_markup=reply_markup) 138 | 139 | 140 | async def handle_button(update: Update, context: ContextTypes.DEFAULT_TYPE): 141 | query = update.callback_query 142 | selected_price = int(query.data) # Price in USD cents 143 | 144 | # Invoice parameters 145 | title = "Product Title" 146 | description = "Product Description" 147 | payload = "Custom-Payload" 148 | provider_token = STRIPE_TEST_PAY 149 | currency = "USD" 150 | prices = [LabeledPrice("Product", selected_price * 100)] # Convert to cents 151 | 152 | # Sending the invoice 153 | await context.bot.send_invoice( 154 | query.message.chat_id, title, description, payload, 155 | provider_token, currency, prices 156 | ) 157 | await query.answer() 158 | 159 | 160 | # after (optional) shipping, it's the pre-checkout 161 | async def precheckout_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: 162 | """Answers the PreQecheckoutQuery""" 163 | query = update.pre_checkout_query 164 | # check the payload, is this from your bot? 165 | if query.invoice_payload != "Custom-Payload": 166 | # answer False pre_checkout_query 167 | await query.answer(ok=False, error_message="Something went wrong...") 168 | else: 169 | await query.answer(ok=True) 170 | 171 | 172 | # finally, after contacting the payment provider... 173 | async def successful_payment_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: 174 | """Confirms the successful payment.""" 175 | # Get the total_amount from the successful_payment object (in the smallest units of the currency, e.g., cents) 176 | total_amount = update.message.successful_payment.total_amount 177 | # Calculate duration in minutes (1$ = 1 minute, assuming total_amount is in cents) 178 | duration_minutes = total_amount // 100 179 | # Calculate the expiration time 180 | expiration_time = datetime.now() + timedelta(minutes=duration_minutes) 181 | # Store the expiration time 182 | user_id = str(update.message.chat.id) 183 | 184 | users, message_history = connect_2_db() 185 | 186 | # Update or insert the user into the collection 187 | users.update_one( 188 | {'user_id': user_id}, 189 | {'$set': {'subscription_end_date': expiration_time}}, 190 | upsert=True) 191 | 192 | # Notify the user 193 | await update.message.reply_text(f"Thank you for your payment! You can now use the bot for {duration_minutes} minutes.") 194 | 195 | 196 | async def unknown(update: Update, context: ContextTypes.DEFAULT_TYPE): 197 | await context.bot.send_message(chat_id=update.effective_chat.id, text="Sorry, I didn't understand that command.") 198 | 199 | 200 | 201 | if __name__ == '__main__': 202 | application = ApplicationBuilder().token(TELEGRAM_BOT).build() 203 | 204 | start_handler = CommandHandler('start', start) 205 | application.add_handler(start_handler) 206 | 207 | clear_handler = CommandHandler('clear', clear) 208 | application.add_handler(clear_handler) 209 | 210 | text_handler = MessageHandler(filters.TEXT & (~filters.COMMAND), text_input) 211 | application.add_handler(text_handler) 212 | 213 | # NOT A COMMAND i.e. /start 214 | audio_handler = MessageHandler(filters.VOICE & (~filters.COMMAND), audio_input) 215 | application.add_handler(audio_handler) 216 | 217 | 218 | deposit_handler = CommandHandler('deposit', deposit) 219 | application.add_handler(deposit_handler) 220 | 221 | application.add_handler(CallbackQueryHandler(handle_button)) 222 | 223 | 224 | # Pre-checkout handler to final check 225 | application.add_handler(PreCheckoutQueryHandler(precheckout_callback)) 226 | # Success! Notify your user! 227 | application.add_handler(MessageHandler(filters.SUCCESSFUL_PAYMENT, successful_payment_callback)) 228 | 229 | unknown_handler = MessageHandler(filters.COMMAND, unknown) 230 | application.add_handler(unknown_handler) 231 | 232 | application.run_polling() 233 | -------------------------------------------------------------------------------- /text_to_speech.py: -------------------------------------------------------------------------------- 1 | 2 | import os 3 | import requests 4 | from datetime import datetime 5 | from dotenv import load_dotenv 6 | 7 | # Load environment variables from .env file 8 | load_dotenv() 9 | ELEVENLABS_API_KEY = os.getenv("ELEVENLABS_API_KEY") 10 | 11 | def get_audio(user_id, bot_reply): 12 | 13 | CHUNK_SIZE = 1024 14 | url = "https://api.elevenlabs.io/v1/text-to-speech/EXAVITQu4vr4xnSDxMaL" 15 | current_time = datetime.now().strftime("%Y-%m-%d-%H-%M-%S") 16 | 17 | headers = { 18 | "Accept": "audio/mpeg", 19 | "Content-Type": "application/json", 20 | "xi-api-key": ELEVENLABS_API_KEY 21 | } 22 | 23 | data = { 24 | "text": bot_reply, 25 | "model_id": "eleven_monolingual_v1", 26 | "voice_settings": { 27 | "stability": 0.5, 28 | "similarity_boost": 0.5 29 | } 30 | } 31 | 32 | response = requests.post(url, json=data, headers=headers) 33 | file_path = f'audio_outputs/{user_id}/girl.mp3' 34 | # Create directories if they don't exist 35 | os.makedirs(f'audio_outputs/{user_id}', exist_ok=True) 36 | 37 | with open(file_path, 'wb') as f: 38 | for chunk in response.iter_content(chunk_size=CHUNK_SIZE): 39 | if chunk: 40 | f.write(chunk) 41 | 42 | return file_path 43 | -------------------------------------------------------------------------------- /transcribe_audio.py: -------------------------------------------------------------------------------- 1 | import openai 2 | import os 3 | import subprocess 4 | from pydub import AudioSegment 5 | from dotenv import load_dotenv 6 | 7 | load_dotenv() 8 | OPENAI_API_KEY = os.getenv('OPENAI_API_KEY') 9 | 10 | def oga_2_mp3(filename): 11 | input_file = f"{filename}.oga" 12 | output_file = f"{filename}.mp3" 13 | 14 | # Load .oga file 15 | audio = AudioSegment.from_ogg(input_file) 16 | 17 | # Export as .mp3 18 | audio.export(output_file, format="mp3") 19 | # Run the ffmpeg command to convert .oga to .mp3 20 | #subprocess.run(["ffmpeg", "-i", input_file, "-codec:a", "libmp3lame", "-qscale:a", "2", output_file]) 21 | 22 | 23 | def oga_2_mp3_2_text(filename): 24 | 25 | oga_2_mp3(filename) 26 | openai.api_key = OPENAI_API_KEY 27 | 28 | audio_file_path = f"{filename}.mp3" 29 | transcript = None 30 | 31 | try: 32 | with open(audio_file_path, "rb") as audio_file: 33 | transcript = openai.Audio.transcribe("whisper-1", audio_file, language="en") 34 | print(transcript) 35 | print(transcript.text) 36 | except Exception as e: 37 | print(f"Transcription failed: {str(e)}") 38 | 39 | # Delete audio files if the transcription was successful 40 | if transcript: 41 | os.remove(audio_file_path) 42 | os.remove(f"{filename}.oga") 43 | print("Audio files deleted.") 44 | 45 | return transcript.text if transcript else "" 46 | --------------------------------------------------------------------------------