Spaces:
Build error
Build error
File size: 19,353 Bytes
e691b50 acff9d2 ec49eaa db4417d ec49eaa 24e0e48 345defd b26ed25 a915c7a 608dbbe c2e3894 784eedc ad23d1e 24e0e48 fa3524d 0f3b800 622c927 ad23d1e 4b7bf52 1ffbb97 1db6059 0f3b800 1ffbb97 8275a0d 1ffbb97 ad23d1e 622c927 3708793 622c927 0f3b800 ad23d1e 275010b ad23d1e 275010b 43e16dd 275010b b5f9809 275010b b5f9809 43e16dd 275010b 86a24e9 b06d4e9 86a24e9 ec49eaa 3ff485a ec49eaa 345defd 8275a0d ec49eaa 345defd fcf0190 345defd df010cc 345defd 608dbbe 784eedc 1b11747 ad23d1e 784eedc ec49eaa 24e0e48 acff9d2 ec49eaa 24e0e48 ec49eaa 73fbfdc ec49eaa 24e0e48 ec49eaa e691b50 df010cc 345defd 24e0e48 ec49eaa 24e0e48 b386c3c ec49eaa e691b50 a915c7a e691b50 c5e7135 60af8b3 86a24e9 3ddb9c1 60af8b3 e691b50 86a24e9 c5e7135 abb1ace c5e7135 37441bf 86a24e9 949938b 60af8b3 a30e721 1b11747 1ffbb97 ad58fc4 a30e721 1b11747 7ee3fed 578f055 1ffbb97 ad58fc4 a3ff15c c5e7135 86a24e9 c5e7135 60af8b3 a30e721 1b11747 1ffbb97 ad58fc4 a30e721 1b11747 60af8b3 578f055 1ffbb97 ad58fc4 a3ff15c 8fede7d 60af8b3 d0a5bac 1ffbb97 784eedc c1b3810 1b11747 1ffbb97 c1b3810 ad58fc4 1b11747 8fede7d 60af8b3 ef74f9e 60d9d69 60af8b3 c5e7135 a915c7a e691b50 33e4b09 ec49eaa 24e0e48 5eef028 33e4b09 ec49eaa 345defd 86a24e9 345defd 86a24e9 345defd d02369a 345defd 44fa9f5 a83e4c6 53320ef f2ca9d0 5337a3f d0a5bac a915c7a fe42433 a915c7a 73a832c 44fa9f5 fe42433 c2e3894 44fa9f5 a915c7a b386c3c 44fa9f5 b386c3c 44fa9f5 b386c3c 1b11747 b386c3c ec49eaa b386c3c 44fa9f5 a915c7a ec49eaa 24e0e48 ec49eaa 60af8b3 2ca6b40 60af8b3 abb1ace 60af8b3 83b6f15 60af8b3 fa3524d 524fbfe 275010b 9997552 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 | from fastapi import FastAPI, Request, HTTPException, BackgroundTasks
from fastapi.responses import JSONResponse, Response
import requests
import uvicorn
import os
import logging
import torch
from transformers import pipeline
import subprocess
import re
from piper.voice import PiperVoice
import wave
from pydub import AudioSegment
from gradio_client import Client
import time
import gc
import threading
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
)
logger = logging.getLogger(__name__)
app = FastAPI()
HF_READ_TKN = os.getenv("HF_READ_TKN")
hope_client = None
Lock = threading.Lock()
TIMESTAMP_LAST_CALL_TO_HOPE = None
#Ongoing llama chat; Dict will tell if we are in chat with this person and what llama input earlier was
ONGOING_CONVO = {}
def is_space_sleeping():
# Stage can be SLEEPING, PAUSED, RUNNING, or APP_STARTING
headers = {"Authorization": f"Bearer {HF_READ_TKN}"}
response = requests.get("https://huggingface.co/api/spaces/HopeLLM/HopeV1.0", headers=headers)
if response.status_code == 200:
space_info = response.json()
status = space_info.get("runtime", {}).get("stage", "")
if status == "SLEEPING":
return True
elif status == "APP_STARTING":
return True
elif status == "RESTARTING":
return True
elif status == "BUILDING":
return True
elif status == "RUNNING":
return False
elif status == "PAUSED":
return "unknown"
else:
return False
return "unknown" # If API call fails, assume the space is sleeping
def call_hope_api(input_txt, sender_id):
global hope_client
global TIMESTAMP_LAST_CALL_TO_HOPE
TIMESTAMP_LAST_CALL_TO_HOPE = time.time()
with Lock:
if hope_client == None:
if(is_space_sleeping()==True):
logger.info("Hope is sleeping, trying to wake it up")
send_whatsapp_message(sender_id, None, "Hope is sleeping, waking it up ;)\nKindly wait for some time", text=True)
try:
Client("HopeLLM/HopeV1.0", hf_token=HF_READ_TKN)
except:
while(is_space_sleeping()):
#Sleep for 30sec
time.sleep(30)
elif (is_space_sleeping() == "unknown"):
logger.error("Something wrong with Hope Check")
hope_client = Client("HopeLLM/HopeV1.0", hf_token=HF_READ_TKN)
return hope_client.predict(inp = input_txt, api_name = "/answer_question")
def reset_hope_client(exit_fn=None):
#The HOPE GPU Space may sleep; Hence we need to remove this hope_client variable before that
#If we dont do that, we get runtime error in our WhatsAppServer Space
global hope_client
global TIMESTAMP_LAST_CALL_TO_HOPE
#If More than an hour to last access to HOPE
try:
while True:
time.sleep(3600)
with Lock:
if hope_client and TIMESTAMP_LAST_CALL_TO_HOPE and time.time() - TIMESTAMP_LAST_CALL_TO_HOPE > 3600:
logger.info("Closing Hope Client Connection. An hour since the last iteraction.")
hope_client = None
gc.collect()
##Added cleanup as well here after 40 min sleep time
logger.debug("Cleaning Ongoing convo")
for sender_id in ONGOING_CONVO.keys():
logger.debug(f"Trying to clean up sender {sender_id}")
if( ONGOING_CONVO[sender_id] and (time.time() - ONGOING_CONVO[sender_id].get("lst_msg_time", 0) > 2400)):
send_whatsapp_message(sender_id, None, "Session Time-Out", text=True)
clean_up_audio_files(sender_id)
ONGOING_CONVO[sender_id] = None
finally:
if exit_fn:
exit_fn()
def notify_parent_of_exit():
logger.error("The cleaner thread has exited. Check!!")
def clean_up_audio_files(sender_id):
command = f"rm -f /tmp/*{sender_id}*"
subprocess.run(command, text = True, shell = True)
# Your WhatsApp verification token
VERIFY_TOKEN = os.getenv("WHATSAPP_VERIFY_TOKEN", "your_verification_token")
# WhatsApp API credentials
WHATSAPP_BASE_URL = "https://graph.facebook.com/v22.0"
WHATSAPP_API_URL = f"{WHATSAPP_BASE_URL}/599521673246967/messages"
WHATSAPP_MEDIA_URL = f"{WHATSAPP_BASE_URL}/599521673246967/media"
WHATSAPP_ACCESS_TOKEN = os.getenv("WHATSAPP_ACCESS_TOKEN", "your_access_token")
# Initialize the Whisper model pipeline
device = "cuda" if torch.cuda.is_available() else "cpu"
transcriber = pipeline(
task="automatic-speech-recognition",
model="openai/whisper-small",
device=device,
generate_kwargs={"task": "translate", "language":"hi"}
)
transcriber.model.config.forced_decoder_ids = transcriber.tokenizer.get_decoder_prompt_ids(language="hi", task="translate")
amy_voice = PiperVoice.load("./piper/en_US-amy-medium.onnx")
FIXED_PROMPT = "You are a helpful English Teaching Bot. You need to be friendly. Since you teach kids less than 7 years old, be motivating and don't point out any mistakes. Dont have long responses, keep your responses short. Assume I am your students and refer me as your student when replying"
llama_input_template = "<s> [INST] <<SYS>> {fixed_prompt} <</SYS>> Understand the story below and ask me one basic question regarding the story. Then wait for me to reply, when I reply, appreciate my participation and same way keep asking more questions one by one. Keep the questions one-liners and do not give much context. Keep everything brief \n {story_to_use} [/INST]"
llama_input_gen_random_story = f"<s> [INST] <<SYS>> You are a helpful English Teacher. You teach kids less than 7 years old.<</SYS>> Give me an english story for today's english learning session. Don't make it very big. Just output the story nothing else, no chatting with me [/INST]"
@app.get("/webhook")
async def verify_webhook(request: Request):
"""
Endpoint for WhatsApp to verify the webhook during setup.
"""
params = request.query_params
if (
params.get("hub.mode") == "subscribe" and
params.get("hub.verify_token") == VERIFY_TOKEN
):
logger.info("Webhook verified successfully.")
return Response(content=params.get("hub.challenge"), media_type="text/plain")
else:
logger.warning("Webhook verification failed.")
raise HTTPException(status_code=403, detail="Verification failed")
@app.post("/webhook")
async def receive_message(request: Request, background_tasks: BackgroundTasks):
"""
Endpoint to receive incoming WhatsApp messages.
"""
data = await request.json()
# Extract the message and sender information
try:
logger.info(f"Incoming message data: {data}")
entry = data["entry"][0]
changes = entry["changes"][0]
value = changes["value"]
message = value["messages"][0]
background_tasks.add_task(backgnd_process_message, message)
return JSONResponse(status_code=200, content={"message": "Webhook received successfully"})
except (KeyError, IndexError) as e:
# If the expected fields are not present, return a 400 response
logger.error(f"Error parsing incoming message: {e}")
return JSONResponse(status_code=200, content={"message": "Webhook received successfully"})
return {"status": "success"}
def backgnd_process_message(message):
sender_id = message["from"]
message_type = message["type"]
message_id = message["id"]
if (ONGOING_CONVO.get(sender_id, None) == None):
#Send the button to start story
welcome_prompt = "Hi there!! I am Hope, your english teaching bot :)\nGive me a story or news article and we can chat around it.\n\n_Note: To end this teaching session, send the message_ END"
ONGOING_CONVO[sender_id] = {'Story': None, 'Llama_conv' : None, 'lst_msg_time': time.time()}
send_whatsapp_message(sender_id, message_id, welcome_prompt, text=True)
send_story_option(sender_id)
else:
ONGOING_CONVO[sender_id]['lst_msg_time'] = time.time()
if message_type == "text":
message_text = message["text"]["body"]
if(message_text.lower() == "end"):
ONGOING_CONVO[sender_id] = None
response_text = "I hope you had a wonderful learning session!! Keep up the good work and best of luck"
clean_up_audio_files(sender_id)
elif (ONGOING_CONVO[sender_id]["Story"] == "PENDING"):
ONGOING_CONVO[sender_id]["Story"] = message_text
response_text = message_text #The input story. We will repeat the story
## Generate question from llama
llama_input = llama_input_template.format(story_to_use = message_text, fixed_prompt=FIXED_PROMPT)
llama_output = call_hope_api(llama_input,sender_id)
logger.info(f"LLama Ouput: {llama_output}")
response_text = response_text + "\n \n" + llama_output
ONGOING_CONVO[sender_id]['Llama_conv'] = llama_input + llama_output + "</s> <s> [INST] "
else:
llama_input = ONGOING_CONVO[sender_id]['Llama_conv'] + message_text + "[/INST]"
llama_output = call_hope_api(llama_input,sender_id)
logger.info(f"LLama Ouput: {llama_output}")
response_text = llama_output
ONGOING_CONVO[sender_id]['Llama_conv'] = llama_input + llama_output + "</s> <s> [INST] "
elif message_type == "audio":
media_id = message["audio"]["id"]
audio_url = fetch_media_url(media_id)
audio_path = download_audio(audio_url, sender_id)
transcription = transcribe_audio(audio_path)
if (ONGOING_CONVO[sender_id]["Story"] == "PENDING"):
ONGOING_CONVO[sender_id]["Story"] = transcription
response_text = transcription
## Generate question from llama
llama_input = llama_input_template.format(story_to_use = transcription, fixed_prompt=FIXED_PROMPT)
llama_output = call_hope_api(llama_input,sender_id)
logger.info(f"LLama Ouput: {llama_output}")
response_text = response_text + "\n \n" + llama_output
ONGOING_CONVO[sender_id]['Llama_conv'] = llama_input + llama_output + "</s> <s> [INST] "
else:
llama_input = ONGOING_CONVO[sender_id]['Llama_conv'] + transcription + "[/INST]"
llama_output = call_hope_api(llama_input,sender_id)
logger.info(f"LLama Ouput: {llama_output}")
response_text = llama_output
ONGOING_CONVO[sender_id]['Llama_conv'] = llama_input + llama_output + "</s> <s> [INST] "
elif message_type == "interactive":
if message["interactive"]["button_reply"]["id"] == "random_story":
ONGOING_CONVO[sender_id]['Story'] = "random_story"
#send_whatsapp_message(sender_id, message_id, "Perfect, let me find a nice story for today.")
story_for_chat = call_hope_api(llama_input_gen_random_story,sender_id)
ONGOING_CONVO[sender_id]['Story'] = story_for_chat
logger.info(f"Llama gave story {story_for_chat}")
## Generate question from llama
llama_input = llama_input_template.format(story_to_use = story_for_chat, fixed_prompt=FIXED_PROMPT)
llama_output = call_hope_api(llama_input,sender_id)
logger.info(f"LLama Ouput: {llama_output}")
response_text = story_for_chat + "\n \n" + llama_output
ONGOING_CONVO[sender_id]['Llama_conv'] = llama_input + llama_output + "</s> <s> [INST] "
elif message["interactive"]["button_reply"]["id"] == "own_story":
response_text = "Great!! Send me your story as the next message, it can be text or audio!!"
ONGOING_CONVO[sender_id]['Story'] = "PENDING"
else:
button_type = message["interactive"]["button_reply"]["id"]
logger.warning(f"Unsupported button reply: {button_type}")
response_text = "Sorry, invalid story type, send a valid type"
else:
logger.warning(f"Unsupported message type: {message_type}")
response_text = "Sorry, I can only process text and audio messages at the moment."
send_whatsapp_message(sender_id, message_id, response_text) #TO-DO remove audio files every hr or something
def process_text_message(message_text: str, sender_id) -> str:
"""
Process the incoming message and generate a response.
Customize this function based on your requirements.
"""
# For demonstration, echoing back the received message
logger.info(f"Processing message: {message_text}")
if ONGOING_CONVO[sender_id]["Story"] == None:
ONGOING_CONVO[sender_id]["Story"] = message_text
return f"Received your message: {message_text}"
def fetch_media_url(media_id: str) -> str:
"""
Fetch the media URL using the media ID.
"""
url = f"{WHATSAPP_BASE_URL}/{media_id}"
headers = {
"Authorization": f"Bearer {WHATSAPP_ACCESS_TOKEN}"
}
response = requests.get(url, headers=headers)
if response.status_code == 200:
media_url = response.json().get("url")
logger.info(f"Fetched media URL: {media_url}")
return media_url
else:
logger.error(f"Failed to fetch media URL: {response.status_code} - {response.text}")
raise HTTPException(status_code=500, detail="Failed to fetch media URL")
def download_audio(audio_url: str, sender_id) -> str:
"""
Download the audio file from the provided URL.
"""
headers = {
"Authorization": f"Bearer {WHATSAPP_ACCESS_TOKEN}"
}
response = requests.get(audio_url, headers=headers)
if response.status_code == 200:
audio_path = f"/tmp/voice_message{sender_id}.ogg"
with open(audio_path, "wb") as audio_file:
audio_file.write(response.content)
logger.info(f"Audio file downloaded to: {audio_path}")
return audio_path
else:
logger.error(f"Failed to download audio file: {response.status_code} - {response.text}")
raise HTTPException(status_code=500, detail="Failed to download audio file")
def transcribe_audio(audio_path: str) -> str:
"""
Transcribe the audio file to text.
"""
try:
logger.info(f"Transcribing audio file: {audio_path}")
transcription = transcriber(audio_path, return_timestamps=True)["text"]
logger.info(f"Transcription: {transcription}")
return transcription
except Exception as e:
logger.error(f"Failed to transcribe audio: {e}")
return "I'm sorry, I couldn't understand the audio message."
def convert_text_to_mp3(text, mp3_name = '/tmp/amy_said.mp3'):
text = re.sub(r':', ". ", text) #To add pause need to add period and not space
text = re.sub(r'!', ". ", text) #To add pause need to add period and not space
text = re.sub(r'\n', ". ", text) #To add pause need to add period and not space
text = re.sub(r"\*[^*]+\*", " ", text)
text = re.sub(r'[^A-Za-z0-9.,$ \'?]+', " ", text)
text = text.encode('cp1252', 'ignore').decode('cp1252')
logger.info(f"Input to amy: {text}")
if text.isspace() or text == "":
return None
with wave.open(mp3_name+'.wav', 'w') as wav_file:
amy_voice.synthesize(text, wav_file, length_scale=1.3)
audio = AudioSegment.from_wav(mp3_name+'.wav')
audio.export(mp3_name, format='mp3')
def get_meta_id(audio_file_path, file_name):
# Open the audio file in binary mode
with open(audio_file_path, 'rb') as audio_file:
# Prepare the data for the request
files = {
'file': (file_name, audio_file, 'audio/mpeg')
}
payload = {
'messaging_product': 'whatsapp',
'type': 'audio/mpeg'
}
headers = {
'Authorization': f'Bearer {WHATSAPP_ACCESS_TOKEN}'
}
# Send the POST request to upload media
response = requests.post(WHATSAPP_MEDIA_URL, headers=headers, data=payload, files=files)
# Check for successful upload
if response.status_code == 200:
media_id = response.json().get('id')
logger.info(f"Media uploaded successfully. Media ID: {media_id}")
else:
media_id = -1
logger.info(f"Failed to upload media: {response.status_code} {response.text}")
return media_id
def send_whatsapp_message(recipient_id: str,message_id:str, message_text: str, text=False):
"""
Send a message back to the WhatsApp user.
"""
headers = {
"Authorization": f"Bearer {WHATSAPP_ACCESS_TOKEN}",
"Content-Type": "application/json"
}
if text:
payload = {
"messaging_product": "whatsapp",
"to": recipient_id,
"type": "text",
"text": {"body": message_text}
}
else:
payload = {
'messaging_product': 'whatsapp',
'to': recipient_id,
'type': 'audio',
'audio': {
'id': None
}
}
audio_file = convert_text_to_mp3(message_text, "/tmp/" + recipient_id + message_id +".mp3")
payload['audio']['id'] = get_meta_id("/tmp/" + recipient_id + message_id +".mp3", recipient_id + message_id +".mp3")
response = requests.post(WHATSAPP_API_URL, json=payload, headers=headers)
if response.status_code == 200:
logger.info(f"Message sent successfully to {recipient_id}.")
else:
logger.error(f"Failed to send message: {response.status_code} - {response.text}")
def send_story_option(recipient_id):
headers = {
"Authorization": f"Bearer {WHATSAPP_ACCESS_TOKEN}",
"Content-Type": "application/json"
}
payload = {
"messaging_product": "whatsapp",
"to": recipient_id,
"type": "interactive",
"interactive": {
"type": "button",
"body": {
"text": "Story option:"
},
"action": {
"buttons": [
{
"type": "reply",
"reply": {
"id": "random_story",
"title": "Pick a story"
}
},
{
"type": "reply",
"reply": {
"id": "own_story",
"title": "I have my own story"
}
}
]
}
}
}
response = requests.post(WHATSAPP_API_URL, json=payload, headers=headers)
if response.status_code == 200:
logger.info(f"Message sent successfully to {recipient_id}.")
else:
logger.error(f"Failed to send message: {response.status_code} - {response.text}")
if __name__ == "__main__":
TIMESTAMP_LAST_CALL_TO_HOPE = time.time()
threading.Thread(target=reset_hope_client, kwargs={'exit_fn': notify_parent_of_exit},daemon=True).start()
uvicorn.run(app, host="0.0.0.0", port=7860) |