ABAO77 commited on
Commit
7f15e1c
·
1 Parent(s): f8b4e8e

feat: text to speech for AI response

Browse files
requirements.txt CHANGED
@@ -12,4 +12,5 @@ langgraph-swarm
12
  langchain-google-genai
13
  python-dotenv
14
  loguru
15
- python-multipart
 
 
12
  langchain-google-genai
13
  python-dotenv
14
  loguru
15
+ python-multipart
16
+ deepgram-sdk
src/agents/lesson_practice_2/prompt.py CHANGED
@@ -18,6 +18,7 @@ I'm **WISE**, your friendly English conversation partner! I create natural, enga
18
  - **Maximum English practice** for user
19
 
20
  ### Immediate Handoff to Teaching Agent When:
 
21
  - User speaks Vietnamese or requests Vietnamese explanation
22
  - User asks "How do I say...?" or "What does... mean?"
23
  - User makes same error 3+ times
 
18
  - **Maximum English practice** for user
19
 
20
  ### Immediate Handoff to Teaching Agent When:
21
+ - If the user initiates the conversation in a language other than English
22
  - User speaks Vietnamese or requests Vietnamese explanation
23
  - User asks "How do I say...?" or "What does... mean?"
24
  - User makes same error 3+ times
src/agents/role_play/__pycache__/prompt.cpython-311.pyc CHANGED
Binary files a/src/agents/role_play/__pycache__/prompt.cpython-311.pyc and b/src/agents/role_play/__pycache__/prompt.cpython-311.pyc differ
 
src/agents/role_play/flow.py CHANGED
@@ -9,7 +9,7 @@ class RolePlayAgent:
9
  pass
10
 
11
  @staticmethod
12
- def route_to_active_agent(state: State) -> str:
13
  if state["active_agent"] == "Roleplay Agent":
14
  return "Roleplay Agent"
15
  elif state["active_agent"] == "Guiding Agent":
 
9
  pass
10
 
11
  @staticmethod
12
+ def route_to_active_agent(state: State):
13
  if state["active_agent"] == "Roleplay Agent":
14
  return "Roleplay Agent"
15
  elif state["active_agent"] == "Guiding Agent":
src/agents/role_play/prompt.py CHANGED
@@ -11,6 +11,8 @@ You are **{your_role}** in an English learning conversation. Create authentic, e
11
 
12
  ## LANGUAGE DECISION MATRIX
13
 
 
 
14
  **CRITICAL RULE: If user uses Vietnamese at any point, immediately handoff to Guiding Agent.**
15
 
16
  ### ✅ CONTINUE ROLEPLAY:
@@ -19,6 +21,7 @@ You are **{your_role}** in an English learning conversation. Create authentic, e
19
  - Communication intent is clear in English
20
 
21
  ### ❌ HANDOFF TO GUIDING AGENT:
 
22
  - User speaks primarily Vietnamese or non-English
23
  - User speaks <30% English
24
  - User asks for language help in ANY language
 
11
 
12
  ## LANGUAGE DECISION MATRIX
13
 
14
+ ## CRITICAL LANGUAGE RULE:
15
+ **IF USER SPEAKS ANY LANGUAGE OTHER THAN ENGLISH → IMMEDIATELY HAND OFF TO GUIDING AGENT**
16
  **CRITICAL RULE: If user uses Vietnamese at any point, immediately handoff to Guiding Agent.**
17
 
18
  ### ✅ CONTINUE ROLEPLAY:
 
21
  - Communication intent is clear in English
22
 
23
  ### ❌ HANDOFF TO GUIDING AGENT:
24
+ - If the user initiates the conversation in a language other than English
25
  - User speaks primarily Vietnamese or non-English
26
  - User speaks <30% English
27
  - User asks for language help in ANY language
src/agents/role_play/tools.py CHANGED
@@ -11,6 +11,6 @@ def function_name(
11
  """
12
  logger.info(f"Received input: {input}")
13
  # Thực hiện các thao tác cần thiết với input
14
- result = f"Processed: {input}"
15
  logger.info(f"Returning result: {result}")
16
  return result
 
11
  """
12
  logger.info(f"Received input: {input}")
13
  # Thực hiện các thao tác cần thiết với input
14
+ result: str = f"Processed: {input}"
15
  logger.info(f"Returning result: {result}")
16
  return result
src/apis/routes/__pycache__/chat_route.cpython-311.pyc CHANGED
Binary files a/src/apis/routes/__pycache__/chat_route.cpython-311.pyc and b/src/apis/routes/__pycache__/chat_route.cpython-311.pyc differ
 
src/apis/routes/chat_route.py CHANGED
@@ -9,6 +9,7 @@ from fastapi import (
9
  from fastapi.responses import JSONResponse, StreamingResponse
10
  from src.utils.logger import logger
11
  from src.agents.role_play.flow import role_play_agent
 
12
  from pydantic import BaseModel, Field
13
  from typing import Dict, Any, Optional
14
  from src.agents.role_play.scenarios import get_scenarios
@@ -144,6 +145,7 @@ async def roleplay_stream(
144
  ),
145
  text_message: Optional[str] = Form(None, description="Text message from user"),
146
  audio_file: Optional[UploadFile] = File(None, description="Audio file from user"),
 
147
  ):
148
  """Send a message (text or audio) to the roleplay agent with streaming response"""
149
  logger.info(f"Received streaming roleplay request: {session_id}")
@@ -214,6 +216,7 @@ async def roleplay_stream(
214
 
215
  async def generate_stream():
216
  """Generator function for streaming responses"""
 
217
  try:
218
  input_graph = {
219
  "messages": [message],
@@ -242,6 +245,9 @@ async def roleplay_stream(
242
  content = getattr(message_chunk, 'content', '')
243
 
244
  if content:
 
 
 
245
  # Create SSE-formatted response
246
  response_data = {
247
  "type": "message_chunk",
@@ -257,8 +263,30 @@ async def roleplay_stream(
257
  # Small delay to prevent overwhelming the client
258
  await asyncio.sleep(0.01)
259
 
260
- # Send completion signal
261
- completion_data = {"type": "completion", "content": ""}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
262
  yield f"data: {json.dumps(completion_data)}\n\n"
263
 
264
  except Exception as e:
 
9
  from fastapi.responses import JSONResponse, StreamingResponse
10
  from src.utils.logger import logger
11
  from src.agents.role_play.flow import role_play_agent
12
+ from src.services.tts_service import tts_service
13
  from pydantic import BaseModel, Field
14
  from typing import Dict, Any, Optional
15
  from src.agents.role_play.scenarios import get_scenarios
 
145
  ),
146
  text_message: Optional[str] = Form(None, description="Text message from user"),
147
  audio_file: Optional[UploadFile] = File(None, description="Audio file from user"),
148
+ audio: bool = Form(False, description="Whether to return TTS audio response"),
149
  ):
150
  """Send a message (text or audio) to the roleplay agent with streaming response"""
151
  logger.info(f"Received streaming roleplay request: {session_id}")
 
216
 
217
  async def generate_stream():
218
  """Generator function for streaming responses"""
219
+ accumulated_content = ""
220
  try:
221
  input_graph = {
222
  "messages": [message],
 
245
  content = getattr(message_chunk, 'content', '')
246
 
247
  if content:
248
+ # Accumulate content for TTS
249
+ accumulated_content += content
250
+
251
  # Create SSE-formatted response
252
  response_data = {
253
  "type": "message_chunk",
 
263
  # Small delay to prevent overwhelming the client
264
  await asyncio.sleep(0.01)
265
 
266
+ # Generate TTS audio if requested
267
+ audio_data = None
268
+ if audio and accumulated_content.strip():
269
+ try:
270
+ logger.info(f"Generating TTS for accumulated content: {len(accumulated_content)} chars")
271
+ audio_result = await tts_service.text_to_speech(accumulated_content)
272
+ if audio_result:
273
+ audio_data = {
274
+ "audio_data": audio_result["audio_data"],
275
+ "mime_type": audio_result["mime_type"],
276
+ "format": audio_result["format"]
277
+ }
278
+ logger.info("TTS audio generated successfully")
279
+ else:
280
+ logger.warning("TTS generation failed")
281
+ except Exception as tts_error:
282
+ logger.error(f"TTS generation error: {str(tts_error)}")
283
+
284
+ # Send completion signal with optional audio
285
+ completion_data = {
286
+ "type": "completion",
287
+ "content": "",
288
+ "audio": audio_data
289
+ }
290
  yield f"data: {json.dumps(completion_data)}\n\n"
291
 
292
  except Exception as e:
src/apis/routes/lesson_route.py CHANGED
@@ -10,6 +10,7 @@ from fastapi import (
10
  )
11
  from fastapi.responses import JSONResponse, StreamingResponse
12
  from src.utils.logger import logger
 
13
  from pydantic import BaseModel, Field
14
  from typing import List, Dict, Any, Optional
15
  from src.agents.lesson_practice.flow import lesson_practice_agent
@@ -367,6 +368,7 @@ async def chat_v2_stream(
367
  ),
368
  text_message: Optional[str] = Form(None, description="Text message from user"),
369
  audio_file: Optional[UploadFile] = File(None, description="Audio file from user"),
 
370
  ):
371
  """Send a message (text or audio) to the lesson practice v2 agent with streaming response"""
372
  logger.info(f"Received streaming lesson practice v2 request: {session_id}")
@@ -437,6 +439,7 @@ async def chat_v2_stream(
437
 
438
  async def generate_stream():
439
  """Generator function for streaming responses"""
 
440
  try:
441
  input_graph = {
442
  "messages": [message],
@@ -465,6 +468,9 @@ async def chat_v2_stream(
465
  content = getattr(message_chunk, 'content', '')
466
 
467
  if content:
 
 
 
468
  # Create SSE-formatted response
469
  response_data = {
470
  "type": "message_chunk",
@@ -480,8 +486,30 @@ async def chat_v2_stream(
480
  # Small delay to prevent overwhelming the client
481
  await asyncio.sleep(0.01)
482
 
483
- # Send completion signal
484
- completion_data = {"type": "completion", "content": ""}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
485
  yield f"data: {json.dumps(completion_data)}\n\n"
486
 
487
  except Exception as e:
 
10
  )
11
  from fastapi.responses import JSONResponse, StreamingResponse
12
  from src.utils.logger import logger
13
+ from src.services.tts_service import tts_service
14
  from pydantic import BaseModel, Field
15
  from typing import List, Dict, Any, Optional
16
  from src.agents.lesson_practice.flow import lesson_practice_agent
 
368
  ),
369
  text_message: Optional[str] = Form(None, description="Text message from user"),
370
  audio_file: Optional[UploadFile] = File(None, description="Audio file from user"),
371
+ audio: bool = Form(False, description="Whether to return TTS audio response"),
372
  ):
373
  """Send a message (text or audio) to the lesson practice v2 agent with streaming response"""
374
  logger.info(f"Received streaming lesson practice v2 request: {session_id}")
 
439
 
440
  async def generate_stream():
441
  """Generator function for streaming responses"""
442
+ accumulated_content = ""
443
  try:
444
  input_graph = {
445
  "messages": [message],
 
468
  content = getattr(message_chunk, 'content', '')
469
 
470
  if content:
471
+ # Accumulate content for TTS
472
+ accumulated_content += content
473
+
474
  # Create SSE-formatted response
475
  response_data = {
476
  "type": "message_chunk",
 
486
  # Small delay to prevent overwhelming the client
487
  await asyncio.sleep(0.01)
488
 
489
+ # Generate TTS audio if requested
490
+ audio_data = None
491
+ if audio and accumulated_content.strip():
492
+ try:
493
+ logger.info(f"Generating TTS for lesson v2 content: {len(accumulated_content)} chars")
494
+ audio_result = await tts_service.text_to_speech(accumulated_content)
495
+ if audio_result:
496
+ audio_data = {
497
+ "audio_data": audio_result["audio_data"],
498
+ "mime_type": audio_result["mime_type"],
499
+ "format": audio_result["format"]
500
+ }
501
+ logger.info("Lesson v2 TTS audio generated successfully")
502
+ else:
503
+ logger.warning("Lesson v2 TTS generation failed")
504
+ except Exception as tts_error:
505
+ logger.error(f"Lesson v2 TTS generation error: {str(tts_error)}")
506
+
507
+ # Send completion signal with optional audio
508
+ completion_data = {
509
+ "type": "completion",
510
+ "content": "",
511
+ "audio": audio_data
512
+ }
513
  yield f"data: {json.dumps(completion_data)}\n\n"
514
 
515
  except Exception as e:
src/config/__pycache__/llm.cpython-311.pyc CHANGED
Binary files a/src/config/__pycache__/llm.cpython-311.pyc and b/src/config/__pycache__/llm.cpython-311.pyc differ
 
src/services/tts_service.py ADDED
@@ -0,0 +1,114 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Text-to-Speech (TTS) Service using Deepgram API
3
+ """
4
+
5
+ import requests
6
+ import os
7
+ import base64
8
+ from src.utils.logger import logger
9
+ from typing import Optional
10
+
11
+ class TTSService:
12
+ """Service for handling text-to-speech conversion using Deepgram API"""
13
+
14
+ def __init__(self):
15
+ self.api_key = os.getenv("YOUR_DEEPGRAM_API_KEY")
16
+ self.base_url = "https://api.deepgram.com/v1/speak"
17
+ self.default_model = "aura-2-thalia-en"
18
+
19
+ if not self.api_key:
20
+ logger.error("Deepgram API key not found in environment variables")
21
+ raise ValueError("Deepgram API key is required")
22
+
23
+ async def text_to_speech(
24
+ self,
25
+ text: str,
26
+ model: Optional[str] = None,
27
+ format: str = "mp3"
28
+ ) -> Optional[dict]:
29
+ """
30
+ Convert text to speech using Deepgram API
31
+
32
+ Args:
33
+ text (str): The text to convert to speech
34
+ model (str): The TTS model to use (default: aura-2-thalia-en)
35
+ format (str): Audio format (default: mp3)
36
+
37
+ Returns:
38
+ dict: Contains audio data and metadata, or None if failed
39
+ """
40
+ try:
41
+ if not text or not text.strip():
42
+ logger.warning("Empty text provided for TTS conversion")
43
+ return None
44
+
45
+ # Clean and prepare text
46
+ cleaned_text = text.strip()
47
+ if len(cleaned_text) > 2000: # Limit text length for TTS
48
+ cleaned_text = cleaned_text[:2000] + "..."
49
+ logger.warning(f"Text truncated to 2000 characters for TTS")
50
+
51
+ # Prepare request
52
+ url = self.base_url
53
+ querystring = {"model": model or self.default_model}
54
+ payload = {"text": cleaned_text}
55
+ headers = {
56
+ "Authorization": f"Token {self.api_key}",
57
+ "Content-Type": "application/json"
58
+ }
59
+
60
+ logger.info(f"Converting text to speech: {cleaned_text[:100]}...")
61
+
62
+ # Make request to Deepgram API
63
+ response = requests.post(
64
+ url,
65
+ json=payload,
66
+ headers=headers,
67
+ params=querystring,
68
+ timeout=30
69
+ )
70
+
71
+ if response.status_code == 200:
72
+ # Encode audio data as base64
73
+ audio_data = response.content
74
+ audio_base64 = base64.b64encode(audio_data).decode('utf-8')
75
+
76
+ # Determine MIME type based on format
77
+ mime_type = f"audio/{format}"
78
+ if format == "mp3":
79
+ mime_type = "audio/mpeg"
80
+ elif format == "wav":
81
+ mime_type = "audio/wav"
82
+
83
+ result = {
84
+ "audio_data": audio_base64,
85
+ "mime_type": mime_type,
86
+ "format": format,
87
+ "text": cleaned_text,
88
+ "model": model or self.default_model,
89
+ "size_bytes": len(audio_data)
90
+ }
91
+
92
+ logger.info(f"TTS conversion successful: {len(audio_data)} bytes")
93
+ return result
94
+
95
+ else:
96
+ logger.error(f"Deepgram TTS API error: {response.status_code} - {response.text}")
97
+ return None
98
+
99
+ except requests.exceptions.Timeout:
100
+ logger.error("TTS request timed out")
101
+ return None
102
+ except requests.exceptions.RequestException as e:
103
+ logger.error(f"TTS request failed: {str(e)}")
104
+ return None
105
+ except Exception as e:
106
+ logger.error(f"Unexpected error in TTS conversion: {str(e)}")
107
+ return None
108
+
109
+ def is_available(self) -> bool:
110
+ """Check if TTS service is available"""
111
+ return bool(self.api_key)
112
+
113
+ # Global TTS service instance
114
+ tts_service = TTSService()