Spaces:
Sleeping
Sleeping
Update backend/ai/services.py
Browse files- backend/ai/services.py +54 -32
backend/ai/services.py
CHANGED
|
@@ -2,43 +2,40 @@ import os
|
|
| 2 |
import time
|
| 3 |
import asyncio
|
| 4 |
import httpx
|
| 5 |
-
|
| 6 |
from google.genai import types
|
| 7 |
from dotenv import load_dotenv
|
| 8 |
-
from pathlib import Path
|
| 9 |
-
|
| 10 |
-
# Load ENV
|
| 11 |
-
load_dotenv(".env.local")
|
| 12 |
-
load_dotenv(Path(__file__).resolve().parents[3] / ".env")
|
| 13 |
|
|
|
|
|
|
|
| 14 |
|
| 15 |
class AIServices:
|
| 16 |
def __init__(self):
|
| 17 |
-
# Lấy base URL từ .env
|
| 18 |
self.base_url = os.getenv("VNPT_BASE_URL", "https://api.idg.vnpt.vn")
|
| 19 |
|
| 20 |
-
# Khởi tạo Gemini Client
|
| 21 |
gemini_key = os.getenv("GOOGLE_API_KEY")
|
| 22 |
-
self.model_id = os.getenv("GEMINI_MODEL_ID", "gemini-3-flash-
|
| 23 |
|
| 24 |
try:
|
| 25 |
if gemini_key:
|
| 26 |
self.client = genai.Client(api_key=gemini_key)
|
| 27 |
else:
|
| 28 |
-
print("⚠️ Cảnh báo: Chưa cấu hình GOOGLE_API_KEY
|
| 29 |
except Exception as e:
|
| 30 |
print(f"❌ Lỗi khởi tạo Gemini: {e}")
|
| 31 |
|
| 32 |
# --- 1. STT (GEMINI) ---
|
| 33 |
async def speech_to_text(self, audio_content: bytes) -> str:
|
|
|
|
| 34 |
print(f"🎤 [STT] Size: {len(audio_content)}")
|
| 35 |
try:
|
| 36 |
-
# PROMPT CHUYÊN DỤNG CHO VIỄN THÔNG VNPT
|
| 37 |
system_prompt = (
|
| 38 |
-
"
|
| 39 |
-
"
|
| 40 |
-
"
|
| 41 |
-
"Chỉ trả về
|
|
|
|
|
|
|
| 42 |
)
|
| 43 |
|
| 44 |
response = await self.client.aio.models.generate_content(
|
|
@@ -54,18 +51,18 @@ class AIServices:
|
|
| 54 |
print(f"❌ STT Error: {e}")
|
| 55 |
return ""
|
| 56 |
|
| 57 |
-
# --- 2. TTS (VNPT) ---
|
| 58 |
async def text_to_speech(self, text: str) -> bytes:
|
| 59 |
if not text: return None
|
| 60 |
-
|
|
|
|
| 61 |
|
| 62 |
-
# Lấy Key từ .env
|
| 63 |
VNPT_ID = os.getenv("VNPT_TTS_TOKEN_ID")
|
| 64 |
VNPT_KEY = os.getenv("VNPT_TTS_TOKEN_KEY")
|
| 65 |
VNPT_ACCESS = os.getenv("VNPT_TTS_ACCESS_TOKEN")
|
| 66 |
|
| 67 |
if not all([VNPT_ID, VNPT_KEY, VNPT_ACCESS]):
|
| 68 |
-
print("❌
|
| 69 |
return None
|
| 70 |
|
| 71 |
url = f"{self.base_url}/tts-service/v1/standard"
|
|
@@ -75,42 +72,52 @@ class AIServices:
|
|
| 75 |
|
| 76 |
async with httpx.AsyncClient() as client:
|
| 77 |
try:
|
| 78 |
-
|
|
|
|
| 79 |
if res.status_code != 200:
|
| 80 |
print(f"❌ VNPT Error: {res.text}")
|
| 81 |
return None
|
| 82 |
tid = res.json().get("object", {}).get("text_id")
|
| 83 |
if not tid: return None
|
| 84 |
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 88 |
if r.status_code == 200:
|
| 89 |
d = r.json()
|
|
|
|
|
|
|
|
|
|
| 90 |
if d.get("object", {}).get("code") == "success":
|
| 91 |
link = d["object"]["playlist"][0]["audio_link"]
|
| 92 |
-
dl = await client.get(link, timeout=
|
| 93 |
return dl.content
|
|
|
|
|
|
|
|
|
|
|
|
|
| 94 |
except Exception as e: print(f"❌ TTS Ex: {e}")
|
| 95 |
return None
|
| 96 |
|
| 97 |
# --- 3. SMARTBOT ---
|
| 98 |
async def chat_smartbot(self, user_text: str, session_id: str = None) -> str:
|
| 99 |
-
# Lấy Key từ .env
|
| 100 |
SB_URL = os.getenv("SMARTBOT_URL")
|
| 101 |
SB_TOK = os.getenv("SMARTBOT_ACCESS_TOKEN")
|
| 102 |
SB_ID = os.getenv("SMARTBOT_TOKEN_ID")
|
| 103 |
SB_KEY = os.getenv("SMARTBOT_TOKEN_KEY")
|
| 104 |
SB_BOT = os.getenv("SMARTBOT_BOT_ID")
|
| 105 |
|
| 106 |
-
if not all([SB_URL, SB_TOK, SB_ID, SB_KEY, SB_BOT]):
|
| 107 |
-
print("❌ Lỗi: Thiếu cấu hình SmartBot trong file .env")
|
| 108 |
-
return None
|
| 109 |
|
| 110 |
headers = { "Authorization": SB_TOK, "Content-Type": "application/json", "Token-Id": SB_ID, "Token-Key": SB_KEY }
|
| 111 |
-
# Nếu không có session_id thì tự tạo
|
| 112 |
real_sid = session_id if session_id else f"s{int(time.time())}"
|
| 113 |
-
|
| 114 |
payload = { "bot_id": SB_BOT, "text": user_text, "type": "text", "session_id": real_sid, "user_id": "guest" }
|
| 115 |
|
| 116 |
async with httpx.AsyncClient() as client:
|
|
@@ -123,11 +130,26 @@ class AIServices:
|
|
| 123 |
except: pass
|
| 124 |
return None
|
| 125 |
|
| 126 |
-
# --- 4. FALLBACK GEMINI ---
|
| 127 |
async def chat_gemini_fallback(self, prompt: str) -> str:
|
| 128 |
try:
|
| 129 |
response = await self.client.aio.models.generate_content(
|
| 130 |
model=self.model_id, contents=prompt
|
| 131 |
)
|
| 132 |
return response.text.strip() if response.text else "Dạ em nghe ạ."
|
| 133 |
-
except: return "Dạ em xin ghi nhận ạ."
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
import time
|
| 3 |
import asyncio
|
| 4 |
import httpx
|
| 5 |
+
from google import genai
|
| 6 |
from google.genai import types
|
| 7 |
from dotenv import load_dotenv
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
|
| 9 |
+
# Tải biến môi trường
|
| 10 |
+
load_dotenv()
|
| 11 |
|
| 12 |
class AIServices:
|
| 13 |
def __init__(self):
|
|
|
|
| 14 |
self.base_url = os.getenv("VNPT_BASE_URL", "https://api.idg.vnpt.vn")
|
| 15 |
|
|
|
|
| 16 |
gemini_key = os.getenv("GOOGLE_API_KEY")
|
| 17 |
+
self.model_id = os.getenv("GEMINI_MODEL_ID", "gemini-3.0-flash-exp")
|
| 18 |
|
| 19 |
try:
|
| 20 |
if gemini_key:
|
| 21 |
self.client = genai.Client(api_key=gemini_key)
|
| 22 |
else:
|
| 23 |
+
print("⚠️ Cảnh báo: Chưa cấu hình GOOGLE_API_KEY")
|
| 24 |
except Exception as e:
|
| 25 |
print(f"❌ Lỗi khởi tạo Gemini: {e}")
|
| 26 |
|
| 27 |
# --- 1. STT (GEMINI) ---
|
| 28 |
async def speech_to_text(self, audio_content: bytes) -> str:
|
| 29 |
+
if not audio_content or len(audio_content) < 1000: return ""
|
| 30 |
print(f"🎤 [STT] Size: {len(audio_content)}")
|
| 31 |
try:
|
|
|
|
| 32 |
system_prompt = (
|
| 33 |
+
"Bạn là công cụ Speech-to-Text chính xác cho viễn thông VNPT. "
|
| 34 |
+
"Nhiệm vụ: Chuyển đổi âm thanh thành văn bản tiếng Việt. "
|
| 35 |
+
"YÊU CẦU QUAN TRỌNG: "
|
| 36 |
+
"1. Chỉ trả về văn bản khách hàng nói. "
|
| 37 |
+
"2. Nếu âm thanh chỉ là tiếng ồn, tiếng thở, khoảng lặng hoặc không rõ lời: HÃY TRẢ VỀ CHUỖI RỖNG (EMPTY STRING). "
|
| 38 |
+
"3. Tuyệt đối KHÔNG tự bịa ra câu hỏi hoặc nội dung nếu không nghe thấy gì."
|
| 39 |
)
|
| 40 |
|
| 41 |
response = await self.client.aio.models.generate_content(
|
|
|
|
| 51 |
print(f"❌ STT Error: {e}")
|
| 52 |
return ""
|
| 53 |
|
| 54 |
+
# --- 2. TTS (VNPT - TỐI ƯU SMART POLLING) ---
|
| 55 |
async def text_to_speech(self, text: str) -> bytes:
|
| 56 |
if not text: return None
|
| 57 |
+
# Log ngắn gọn
|
| 58 |
+
print(f"🔊 [VNPT TTS] Request: {text[:30]}...")
|
| 59 |
|
|
|
|
| 60 |
VNPT_ID = os.getenv("VNPT_TTS_TOKEN_ID")
|
| 61 |
VNPT_KEY = os.getenv("VNPT_TTS_TOKEN_KEY")
|
| 62 |
VNPT_ACCESS = os.getenv("VNPT_TTS_ACCESS_TOKEN")
|
| 63 |
|
| 64 |
if not all([VNPT_ID, VNPT_KEY, VNPT_ACCESS]):
|
| 65 |
+
print("❌ Thiếu cấu hình VNPT TTS")
|
| 66 |
return None
|
| 67 |
|
| 68 |
url = f"{self.base_url}/tts-service/v1/standard"
|
|
|
|
| 72 |
|
| 73 |
async with httpx.AsyncClient() as client:
|
| 74 |
try:
|
| 75 |
+
# 1. Gửi request tạo file (Timeout ngắn 5s để fail fast)
|
| 76 |
+
res = await client.post(url, headers=headers, json=payload, timeout=5.0)
|
| 77 |
if res.status_code != 200:
|
| 78 |
print(f"❌ VNPT Error: {res.text}")
|
| 79 |
return None
|
| 80 |
tid = res.json().get("object", {}).get("text_id")
|
| 81 |
if not tid: return None
|
| 82 |
|
| 83 |
+
# 2. SMART POLLING (Check nhanh cho câu ngắn để giảm độ trễ)
|
| 84 |
+
# Câu ngắn (< 30 ký tự) -> check mỗi 0.1s
|
| 85 |
+
# Câu dài -> check mỗi 0.3s
|
| 86 |
+
sleep_time = 0.1 if len(text) < 30 else 0.3
|
| 87 |
+
|
| 88 |
+
# Loop tối đa 30 lần (khoảng 3-9s tùy độ dài)
|
| 89 |
+
for _ in range(30):
|
| 90 |
+
await asyncio.sleep(sleep_time)
|
| 91 |
+
|
| 92 |
+
r = await client.post(chk_url, headers=headers, json={"text_id": tid}, timeout=5.0)
|
| 93 |
if r.status_code == 200:
|
| 94 |
d = r.json()
|
| 95 |
+
status = d.get("object", {}).get("status", "")
|
| 96 |
+
|
| 97 |
+
# Thành công -> Tải file
|
| 98 |
if d.get("object", {}).get("code") == "success":
|
| 99 |
link = d["object"]["playlist"][0]["audio_link"]
|
| 100 |
+
dl = await client.get(link, timeout=15.0)
|
| 101 |
return dl.content
|
| 102 |
+
|
| 103 |
+
# Thất bại -> Dừng ngay
|
| 104 |
+
if status == "failed": break
|
| 105 |
+
|
| 106 |
except Exception as e: print(f"❌ TTS Ex: {e}")
|
| 107 |
return None
|
| 108 |
|
| 109 |
# --- 3. SMARTBOT ---
|
| 110 |
async def chat_smartbot(self, user_text: str, session_id: str = None) -> str:
|
|
|
|
| 111 |
SB_URL = os.getenv("SMARTBOT_URL")
|
| 112 |
SB_TOK = os.getenv("SMARTBOT_ACCESS_TOKEN")
|
| 113 |
SB_ID = os.getenv("SMARTBOT_TOKEN_ID")
|
| 114 |
SB_KEY = os.getenv("SMARTBOT_TOKEN_KEY")
|
| 115 |
SB_BOT = os.getenv("SMARTBOT_BOT_ID")
|
| 116 |
|
| 117 |
+
if not all([SB_URL, SB_TOK, SB_ID, SB_KEY, SB_BOT]): return None
|
|
|
|
|
|
|
| 118 |
|
| 119 |
headers = { "Authorization": SB_TOK, "Content-Type": "application/json", "Token-Id": SB_ID, "Token-Key": SB_KEY }
|
|
|
|
| 120 |
real_sid = session_id if session_id else f"s{int(time.time())}"
|
|
|
|
| 121 |
payload = { "bot_id": SB_BOT, "text": user_text, "type": "text", "session_id": real_sid, "user_id": "guest" }
|
| 122 |
|
| 123 |
async with httpx.AsyncClient() as client:
|
|
|
|
| 130 |
except: pass
|
| 131 |
return None
|
| 132 |
|
| 133 |
+
# --- 4. FALLBACK GEMINI (BLOCKING) ---
|
| 134 |
async def chat_gemini_fallback(self, prompt: str) -> str:
|
| 135 |
try:
|
| 136 |
response = await self.client.aio.models.generate_content(
|
| 137 |
model=self.model_id, contents=prompt
|
| 138 |
)
|
| 139 |
return response.text.strip() if response.text else "Dạ em nghe ạ."
|
| 140 |
+
except: return "Dạ em xin ghi nhận ạ."
|
| 141 |
+
|
| 142 |
+
# --- [QUAN TRỌNG] 5. GEMINI STREAMING (CHO PIPELINE) ---
|
| 143 |
+
# Hàm này bắt buộc phải có để Logic Flow gọi được
|
| 144 |
+
async def chat_gemini_stream(self, prompt: str):
|
| 145 |
+
try:
|
| 146 |
+
# Dùng generate_content_stream để trả về Generator
|
| 147 |
+
async for chunk in await self.client.aio.models.generate_content_stream(
|
| 148 |
+
model=self.model_id,
|
| 149 |
+
contents=prompt
|
| 150 |
+
):
|
| 151 |
+
if chunk.text:
|
| 152 |
+
yield chunk.text
|
| 153 |
+
except Exception as e:
|
| 154 |
+
print(f"❌ Gemini Stream Error: {e}")
|
| 155 |
+
yield ""
|