Spaces:
Running
Running
Commit
·
3fe95cf
1
Parent(s):
d751b82
Play
Browse files- core/intent_detector.py +42 -14
- services/realtime_stt_service.py +22 -28
core/intent_detector.py
CHANGED
|
@@ -198,14 +198,19 @@ class IntentDetector:
|
|
| 198 |
- 位置查詢:「我在哪」「where am I」使用 reverse_geocode
|
| 199 |
- YouBike 查詢:YouBike/Ubike/微笑單車 使用 tdx_youbike
|
| 200 |
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
- neutral
|
| 204 |
-
- happy
|
| 205 |
-
- sad
|
| 206 |
-
- angry
|
| 207 |
-
- fear
|
| 208 |
-
- surprise
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 209 |
|
| 210 |
def _parse_function_calling_response(
|
| 211 |
self,
|
|
@@ -255,13 +260,36 @@ class IntentDetector:
|
|
| 255 |
return False, {"emotion": emotion}
|
| 256 |
|
| 257 |
def _extract_emotion_from_response(self, response: Dict[str, Any]) -> str:
|
| 258 |
-
"""從回應中提取情緒
|
| 259 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 260 |
content = response.get("content", "")
|
| 261 |
-
if content:
|
| 262 |
-
|
| 263 |
-
|
| 264 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 265 |
|
| 266 |
return "neutral"
|
| 267 |
|
|
|
|
| 198 |
- 位置查詢:「我在哪」「where am I」使用 reverse_geocode
|
| 199 |
- YouBike 查詢:YouBike/Ubike/微笑單車 使用 tdx_youbike
|
| 200 |
|
| 201 |
+
【情緒判斷 - 重要】
|
| 202 |
+
根據用戶消息的語氣判斷情緒,並在回應開頭以 [EMOTION:xxx] 格式輸出:
|
| 203 |
+
- [EMOTION:neutral] - 平靜、中性、一般詢問
|
| 204 |
+
- [EMOTION:happy] - 開心、興奮、正面情緒(如:我很快樂、太棒了、好開心)
|
| 205 |
+
- [EMOTION:sad] - 難過、沮喪、失落
|
| 206 |
+
- [EMOTION:angry] - 生氣、煩躁、憤怒
|
| 207 |
+
- [EMOTION:fear] - 恐懼、擔心、焦慮
|
| 208 |
+
- [EMOTION:surprise] - 驚訝、意外
|
| 209 |
+
|
| 210 |
+
範例:
|
| 211 |
+
- 用戶說「我很快樂」→ 回應開頭必須是 [EMOTION:happy]
|
| 212 |
+
- 用戶說「今天天氣如何」→ 回應開頭必須是 [EMOTION:neutral]
|
| 213 |
+
- 用戶說「我好難過」→ 回應開頭必須是 [EMOTION:sad]"""
|
| 214 |
|
| 215 |
def _parse_function_calling_response(
|
| 216 |
self,
|
|
|
|
| 260 |
return False, {"emotion": emotion}
|
| 261 |
|
| 262 |
def _extract_emotion_from_response(self, response: Dict[str, Any]) -> str:
|
| 263 |
+
"""從回應中提取情緒
|
| 264 |
+
|
| 265 |
+
優先使用 [EMOTION:xxx] 格式提取,降級使用關鍵字匹配
|
| 266 |
+
"""
|
| 267 |
+
import re
|
| 268 |
+
|
| 269 |
content = response.get("content", "")
|
| 270 |
+
if not content:
|
| 271 |
+
return "neutral"
|
| 272 |
+
|
| 273 |
+
# 優先:使用正則表達式提取 [EMOTION:xxx] 格式
|
| 274 |
+
emotion_match = re.search(r'\[EMOTION:(\w+)\]', content, re.IGNORECASE)
|
| 275 |
+
if emotion_match:
|
| 276 |
+
extracted = emotion_match.group(1).lower()
|
| 277 |
+
if extracted in self.EMOTIONS:
|
| 278 |
+
logger.info(f"🎭 從格式化標籤提取情緒: {extracted}")
|
| 279 |
+
return extracted
|
| 280 |
+
|
| 281 |
+
# 降級:使用關鍵字匹配(但需要更精確的匹配)
|
| 282 |
+
content_lower = content.lower()
|
| 283 |
+
for emotion in self.EMOTIONS:
|
| 284 |
+
# 使用單詞邊界匹配,避免誤判(如 "not angry" 被判為 angry)
|
| 285 |
+
pattern = rf'\b{emotion}\b'
|
| 286 |
+
if re.search(pattern, content_lower):
|
| 287 |
+
# 檢查是否有否定詞在前面
|
| 288 |
+
negation_pattern = rf'(not|no|isn\'t|aren\'t|wasn\'t|weren\'t|don\'t|doesn\'t|didn\'t|never|neither)\s+{emotion}'
|
| 289 |
+
if re.search(negation_pattern, content_lower):
|
| 290 |
+
continue # 跳過被否定的情緒
|
| 291 |
+
logger.info(f"🎭 從關鍵字提取情緒: {emotion}")
|
| 292 |
+
return emotion
|
| 293 |
|
| 294 |
return "neutral"
|
| 295 |
|
services/realtime_stt_service.py
CHANGED
|
@@ -45,27 +45,22 @@ class RealtimeSTTService:
|
|
| 45 |
self._receive_task: Optional[asyncio.Task] = None
|
| 46 |
self.current_language: str = "zh"
|
| 47 |
|
| 48 |
-
def _build_language_prompt(self) -> str:
|
| 49 |
"""
|
| 50 |
-
|
| 51 |
|
| 52 |
-
Whisper
|
| 53 |
-
|
| 54 |
-
|
|
|
|
|
|
|
| 55 |
|
| 56 |
Returns:
|
| 57 |
-
|
| 58 |
"""
|
| 59 |
-
#
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
"Hello", # 英文
|
| 63 |
-
"Halo", # 印尼文
|
| 64 |
-
"こんにちは", # 日文
|
| 65 |
-
"Xin chào" # 越南文
|
| 66 |
-
]
|
| 67 |
-
|
| 68 |
-
return ", ".join(prompt_samples)
|
| 69 |
|
| 70 |
def _validate_language(self, language: str) -> Optional[str]:
|
| 71 |
"""
|
|
@@ -146,19 +141,22 @@ class RealtimeSTTService:
|
|
| 146 |
self.is_connected = True
|
| 147 |
logger.info("✅ 已連接到 OpenAI Realtime API")
|
| 148 |
|
| 149 |
-
# 建立語言提示(引導 Whisper 優先識別支援的 5 種語言)
|
| 150 |
-
language_prompt = self._build_language_prompt()
|
| 151 |
-
|
| 152 |
# 發送 session 配置(正確格式:需要 session 物件包裹)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 153 |
session_config = {
|
| 154 |
"type": "transcription_session.update",
|
| 155 |
"session": {
|
| 156 |
"input_audio_format": "pcm16",
|
| 157 |
-
"input_audio_transcription":
|
| 158 |
-
"model": model,
|
| 159 |
-
"prompt": language_prompt # 使用語言提示引導識別
|
| 160 |
-
# 不指定 language,讓 Whisper 自動檢測(但透過 prompt 引導)
|
| 161 |
-
},
|
| 162 |
"turn_detection": {
|
| 163 |
"type": "server_vad",
|
| 164 |
"threshold": 0.5,
|
|
@@ -170,10 +168,6 @@ class RealtimeSTTService:
|
|
| 170 |
}
|
| 171 |
}
|
| 172 |
}
|
| 173 |
-
|
| 174 |
-
# 如果指定了語言,則加入配置
|
| 175 |
-
if validated_language:
|
| 176 |
-
session_config["session"]["input_audio_transcription"]["language"] = validated_language
|
| 177 |
|
| 178 |
await self.ws.send(json.dumps(session_config))
|
| 179 |
logger.info("📤 已發送 session 配置(含語言引導提示)")
|
|
|
|
| 45 |
self._receive_task: Optional[asyncio.Task] = None
|
| 46 |
self.current_language: str = "zh"
|
| 47 |
|
| 48 |
+
def _build_language_prompt(self, language: Optional[str] = None) -> Optional[str]:
|
| 49 |
"""
|
| 50 |
+
建立語言提示
|
| 51 |
|
| 52 |
+
注意:不使用具體詞彙(如「你好」「Hello」),避免 Whisper 在靜音或
|
| 53 |
+
低音量時產生幻覺,將 prompt 中的文字當作轉錄結果輸出。
|
| 54 |
+
|
| 55 |
+
Args:
|
| 56 |
+
language: 語言代碼(zh/en/id/ja/vi)或 None(自動檢測)
|
| 57 |
|
| 58 |
Returns:
|
| 59 |
+
語言提示字串,或 None(不使用 prompt)
|
| 60 |
"""
|
| 61 |
+
# 不使用 prompt,完全依賴 language 參數和音頻內容
|
| 62 |
+
# 這樣可以避免 Whisper 幻覺出 prompt 中的文字
|
| 63 |
+
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 64 |
|
| 65 |
def _validate_language(self, language: str) -> Optional[str]:
|
| 66 |
"""
|
|
|
|
| 141 |
self.is_connected = True
|
| 142 |
logger.info("✅ 已連接到 OpenAI Realtime API")
|
| 143 |
|
|
|
|
|
|
|
|
|
|
| 144 |
# 發送 session 配置(正確格式:需要 session 物件包裹)
|
| 145 |
+
# 不使用 prompt 參數,避免 Whisper 幻覺
|
| 146 |
+
transcription_config = {
|
| 147 |
+
"model": model,
|
| 148 |
+
}
|
| 149 |
+
|
| 150 |
+
# 如果指定了語言,加入 language 參數
|
| 151 |
+
if validated_language:
|
| 152 |
+
transcription_config["language"] = validated_language
|
| 153 |
+
logger.info(f"🌐 Whisper 語言設定: {validated_language}")
|
| 154 |
+
|
| 155 |
session_config = {
|
| 156 |
"type": "transcription_session.update",
|
| 157 |
"session": {
|
| 158 |
"input_audio_format": "pcm16",
|
| 159 |
+
"input_audio_transcription": transcription_config,
|
|
|
|
|
|
|
|
|
|
|
|
|
| 160 |
"turn_detection": {
|
| 161 |
"type": "server_vad",
|
| 162 |
"threshold": 0.5,
|
|
|
|
| 168 |
}
|
| 169 |
}
|
| 170 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 171 |
|
| 172 |
await self.ws.send(json.dumps(session_config))
|
| 173 |
logger.info("📤 已發送 session 配置(含語言引導提示)")
|