stt_service.py 34 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859
  1. """
  2. STT服务模块 - 基于 DashScope OmniRealtimeConversation API
  3. 参考官方demo实现,提供流式语音识别功能
  4. """
  5. import asyncio
  6. import time
  7. import logging
  8. import traceback
  9. from typing import Optional, Callable, Awaitable
  10. import dashscope
  11. from dashscope.audio.qwen_omni import OmniRealtimeConversation, OmniRealtimeCallback
  12. from dashscope.audio.qwen_omni.omni_realtime import MultiModality, TranscriptionParams
  13. from config import settings, get_dashscope_ws_url
  14. logger = logging.getLogger(__name__)
  15. # 🔥 配置 DashScope SDK 的日志级别,避免空错误日志
  16. dashscope_logger = logging.getLogger('dashscope')
  17. dashscope_logger.setLevel(logging.WARNING) # 只记录 WARNING 及以上级别
  18. class STTCallback(OmniRealtimeCallback):
  19. """STT识别回调处理器(参考官方demo实现)"""
  20. def __init__(
  21. self,
  22. request_id: str,
  23. on_text_update: Optional[Callable[[str, bool], Awaitable[None]]] = None,
  24. on_error: Optional[Callable[[str], Awaitable[None]]] = None,
  25. event_loop: Optional[asyncio.AbstractEventLoop] = None
  26. ):
  27. """
  28. 初始化STT回调处理器
  29. Args:
  30. request_id: 请求ID(用于日志标识)
  31. on_text_update: 文本更新回调 (text, is_final) -> None
  32. on_error: 错误回调 (error_msg) -> None
  33. event_loop: 事件循环(用于线程安全的异步调用)
  34. """
  35. self.request_id = request_id
  36. self.on_text_update = on_text_update
  37. self.on_error = on_error
  38. self.event_loop = event_loop
  39. # 识别结果存储
  40. self.current_text = ""
  41. self.is_final = False
  42. self.final_event = asyncio.Event() if event_loop else None
  43. # 时间统计
  44. self.last_update_time = 0.0
  45. self.first_text_time = None # 首个文本生成时间
  46. self.session_id: Optional[str] = None
  47. # 语音检测统计
  48. self.speech_detected = False # 是否检测到语音
  49. self.speech_start_count = 0 # 语音开始次数
  50. self.speech_stop_count = 0 # 语音停止次数
  51. # 事件接收统计(用于诊断)
  52. self.event_count = 0 # 收到的事件总数
  53. self.received_events = [] # 收到的事件类型列表(最多保存20个)
  54. # 事件处理器映射(参考官方demo)
  55. self.handlers = {
  56. 'session.created': self._handle_session_created,
  57. 'conversation.item.input_audio_transcription.completed': self._handle_final_text,
  58. 'conversation.item.input_audio_transcription.text': self._handle_stash_text,
  59. 'input_audio_buffer.speech_started': self._handle_speech_start,
  60. 'input_audio_buffer.speech_stopped': self._handle_speech_stop,
  61. 'error': self._handle_error,
  62. }
  63. def on_open(self):
  64. """连接打开回调"""
  65. logger.debug(
  66. f"[STT调试] request_id={self.request_id}, "
  67. f"WebSocket连接已打开"
  68. )
  69. def on_close(self, code: int, msg: str):
  70. """连接关闭回调"""
  71. logger.debug(
  72. f"[STT调试] request_id={self.request_id}, "
  73. f"WebSocket连接已关闭, "
  74. f"关闭码={code}, "
  75. f"关闭消息={msg}"
  76. )
  77. # 非正常关闭(1000是正常关闭码,None是主动关闭)
  78. if code is not None and code != 1000 and self.on_error:
  79. error_msg = f"STT连接异常关闭: {msg} (code: {code})"
  80. logger.error(
  81. f"[STT异常] request_id={self.request_id}, "
  82. f"错误类型=连接异常关闭, "
  83. f"关闭码={code}, "
  84. f"错误消息={msg}"
  85. )
  86. self._async_call(self.on_error(error_msg))
  87. def on_event(self, response: dict):
  88. """事件分发(参考官方demo)"""
  89. try:
  90. event_type = response.get('type')
  91. handler = self.handlers.get(event_type)
  92. # 🔥 更新事件接收统计
  93. self.event_count += 1
  94. if len(self.received_events) < 20:
  95. self.received_events.append(event_type)
  96. # 🔥 打印STT接口的原始返回日志(完整JSON)
  97. import json
  98. try:
  99. response_json = json.dumps(response, ensure_ascii=False, indent=2)
  100. logger.info(
  101. f"[STT原始返回] request_id={self.request_id}, "
  102. f"事件#{self.event_count}, "
  103. f"事件类型={event_type}\n"
  104. f"完整响应数据:\n{response_json}"
  105. )
  106. except Exception as json_error:
  107. logger.warning(
  108. f"[STT原始返回] request_id={self.request_id}, "
  109. f"事件#{self.event_count}, "
  110. f"事件类型={event_type}, "
  111. f"JSON序列化失败: {json_error}, "
  112. f"原始响应={response}"
  113. )
  114. # 调试日志:记录所有事件(高频事件简化输出)
  115. if event_type in ['input_audio_buffer.speech_started', 'input_audio_buffer.speech_stopped']:
  116. logger.debug(
  117. f"[STT调试] request_id={self.request_id}, "
  118. f"事件类型={event_type}"
  119. )
  120. else:
  121. logger.debug(
  122. f"[STT调试] request_id={self.request_id}, "
  123. f"事件类型={event_type}, "
  124. f"事件数据={response}"
  125. )
  126. if handler:
  127. handler(response)
  128. else:
  129. # 记录未处理的事件(排除常见的已知事件)
  130. known_events = [
  131. 'session.created',
  132. 'session.updated',
  133. 'input_audio_buffer.committed',
  134. 'conversation.item.created' # 会话项创建事件
  135. ]
  136. if event_type not in known_events:
  137. pass
  138. except Exception as e:
  139. logger.error(
  140. f"[STT异常] request_id={self.request_id}, "
  141. f"错误类型=事件处理异常, "
  142. f"异常类型={type(e).__name__}, "
  143. f"错误消息={str(e)}, "
  144. f"事件类型={response.get('type', 'unknown')}, "
  145. f"堆栈跟踪:\n{traceback.format_exc()}"
  146. )
  147. # ========== 事件处理器(参考官方demo) ==========
  148. def _handle_session_created(self, response: dict):
  149. """处理会话创建事件"""
  150. session_id = response.get('session', {}).get('id', '')
  151. self.session_id = session_id
  152. logger.debug(
  153. f"[STT调试] request_id={self.request_id}, "
  154. f"会话已创建, session_id={session_id}"
  155. )
  156. def _handle_final_text(self, response: dict):
  157. """处理最终识别结果(参考官方demo)"""
  158. text = response.get('transcript', '')
  159. logger.debug(
  160. f"[STT调试] request_id={self.request_id}, "
  161. f"收到最终识别结果, 文本长度={len(text)}, "
  162. f"文本内容={text[:50]}{'...' if len(text) > 50 else ''}"
  163. )
  164. # 更新状态
  165. self.current_text = text
  166. self.is_final = True
  167. self.last_update_time = time.time()
  168. # 触发最终结果事件
  169. if self.final_event and self.event_loop:
  170. try:
  171. self.event_loop.call_soon_threadsafe(self.final_event.set)
  172. logger.debug(
  173. f"[STT调试] request_id={self.request_id}, "
  174. f"已触发最终结果事件"
  175. )
  176. except Exception as e:
  177. logger.error(
  178. f"[STT异常] request_id={self.request_id}, "
  179. f"错误类型=事件触发异常, "
  180. f"异常类型={type(e).__name__}, "
  181. f"错误消息={str(e)}, "
  182. f"堆栈跟踪:\n{traceback.format_exc()}"
  183. )
  184. # 回调通知
  185. if self.on_text_update:
  186. self._async_call(self.on_text_update(text, True))
  187. def _handle_stash_text(self, response: dict):
  188. """处理中间识别结果(参考官方demo)"""
  189. # 合并 text(已确认部分)和 stash(正在识别部分)
  190. confirmed_text = response.get('text', '')
  191. stash_text = response.get('stash', '')
  192. full_text = confirmed_text + stash_text
  193. # 更新状态
  194. if full_text:
  195. self.current_text = full_text
  196. self.is_final = False
  197. current_time = time.time()
  198. self.last_update_time = current_time
  199. # 记录首个文本生成时间
  200. if self.first_text_time is None and full_text:
  201. self.first_text_time = current_time
  202. logger.info(
  203. f"[STT] request_id={self.request_id}, "
  204. f"首个文本生成, 文本={full_text[:30]}{'...' if len(full_text) > 30 else ''}"
  205. )
  206. # 调试日志:每5次中间结果记录一次(避免日志过多)
  207. if hasattr(self, '_stash_count'):
  208. self._stash_count += 1
  209. else:
  210. self._stash_count = 1
  211. if self._stash_count % 5 == 0:
  212. logger.debug(
  213. f"[STT调试] request_id={self.request_id}, "
  214. f"中间识别结果 #{self._stash_count}, "
  215. f"文本长度={len(full_text)}, "
  216. f"文本内容={full_text[:30]}{'...' if len(full_text) > 30 else ''}"
  217. )
  218. # 回调通知
  219. if self.on_text_update:
  220. self._async_call(self.on_text_update(full_text, False))
  221. def _handle_speech_start(self, response: dict):
  222. """处理语音开始事件"""
  223. self.speech_detected = True
  224. self.speech_start_count += 1
  225. logger.debug(
  226. f"[STT调试] request_id={self.request_id}, "
  227. f"检测到语音开始, speech_start_count={self.speech_start_count}"
  228. )
  229. def _handle_speech_stop(self, response: dict):
  230. """处理语音停止事件"""
  231. self.speech_stop_count += 1
  232. logger.debug(
  233. f"[STT调试] request_id={self.request_id}, "
  234. f"检测到语音停止, speech_stop_count={self.speech_stop_count}"
  235. )
  236. def _handle_error(self, response: dict):
  237. """处理错误事件"""
  238. error = response.get('error', {})
  239. error_msg = error.get('message', 'Unknown error')
  240. error_code = error.get('code', '')
  241. # 详细记录STT API错误
  242. logger.error(
  243. f"[STT异常] request_id={self.request_id}, "
  244. f"错误类型=STT API错误, "
  245. f"错误码={error_code}, "
  246. f"错误消息={error_msg}, "
  247. f"完整响应={response}"
  248. )
  249. # 回调通知
  250. if self.on_error:
  251. self._async_call(self.on_error(f"{error_msg} (code: {error_code})"))
  252. # ========== 辅助方法 ==========
  253. def _async_call(self, coro):
  254. """线程安全的异步调用"""
  255. if self.event_loop:
  256. try:
  257. asyncio.run_coroutine_threadsafe(coro, self.event_loop)
  258. except Exception as e:
  259. logger.error(
  260. f"[STT异常] request_id={self.request_id}, "
  261. f"错误类型=异步调用异常, "
  262. f"异常类型={type(e).__name__}, "
  263. f"错误消息={str(e)}, "
  264. f"堆栈跟踪:\n{traceback.format_exc()}"
  265. )
  266. def get_text(self) -> str:
  267. """获取当前识别文本"""
  268. return self.current_text
  269. def is_final_result(self) -> bool:
  270. """是否为最终结果"""
  271. return self.is_final
  272. class STTSession:
  273. """STT识别会话管理器(参考官方demo实现)"""
  274. def __init__(
  275. self,
  276. request_id: str,
  277. on_text_update: Optional[Callable[[str, bool], Awaitable[None]]] = None,
  278. on_error: Optional[Callable[[str], Awaitable[None]]] = None,
  279. event_loop: Optional[asyncio.AbstractEventLoop] = None
  280. ):
  281. """
  282. 初始化STT会话
  283. Args:
  284. request_id: 请求ID
  285. on_text_update: 文本更新回调
  286. on_error: 错误回调
  287. event_loop: 事件循环
  288. """
  289. self.request_id = request_id
  290. self.callback = STTCallback(
  291. request_id=request_id,
  292. on_text_update=on_text_update,
  293. on_error=on_error,
  294. event_loop=event_loop
  295. )
  296. self.conversation: Optional[OmniRealtimeConversation] = None
  297. self.is_connected = False
  298. self.is_closed = False
  299. async def connect(self, max_retries: int = 2) -> bool:
  300. """
  301. 连接STT服务并初始化会话(带重试机制)
  302. 🔥 每次请求都创建新的 WebSocket 会话,确保可以随时建立请求
  303. 不会因为连接复用而阻塞,每个 request_id 都有独立的会话
  304. Args:
  305. max_retries: 最大重试次数(默认2次,总共尝试3次)
  306. Returns:
  307. 是否连接成功
  308. """
  309. last_error = None
  310. for attempt in range(max_retries + 1):
  311. try:
  312. start_time = time.time()
  313. # 设置API Key
  314. dashscope.api_key = settings.active_dashscope_api_key
  315. logger.debug(
  316. f"[STT调试] request_id={self.request_id}, "
  317. f"连接尝试 {attempt + 1}/{max_retries + 1}, "
  318. f"API Key已设置, 使用国际版={settings.use_dashscope_intl}"
  319. )
  320. # 🔥 创建新的会话对象(每次请求独立创建,参考官方demo)
  321. def create_conversation():
  322. ws_url = get_dashscope_ws_url()
  323. logger.debug(
  324. f"[STT调试] request_id={self.request_id}, "
  325. f"创建会话对象, URL={ws_url}"
  326. )
  327. conversation = OmniRealtimeConversation(
  328. model=settings.STT_MODEL,
  329. url=ws_url,
  330. callback=self.callback
  331. )
  332. # 注入自身引用(回调中可能需要)
  333. self.callback.conversation = conversation
  334. logger.debug(
  335. f"[STT调试] request_id={self.request_id}, "
  336. f"会话对象已创建"
  337. )
  338. return conversation
  339. # 异步创建会话(每次都是新连接)
  340. create_start = time.time()
  341. self.conversation = await asyncio.to_thread(create_conversation)
  342. create_duration = (time.time() - create_start) * 1000
  343. logger.debug(
  344. f"[STT调试] request_id={self.request_id}, "
  345. f"会话对象创建完成, 耗时={create_duration:.1f}ms"
  346. )
  347. # 连接WebSocket(添加超时保护)
  348. logger.debug(
  349. f"[STT调试] request_id={self.request_id}, "
  350. f"开始连接WebSocket..."
  351. )
  352. connect_start = time.time()
  353. try:
  354. await asyncio.wait_for(
  355. asyncio.to_thread(self.conversation.connect),
  356. timeout=10.0 # 10秒连接超时
  357. )
  358. connect_duration = (time.time() - connect_start) * 1000
  359. logger.debug(
  360. f"[STT调试] request_id={self.request_id}, "
  361. f"WebSocket连接成功, 耗时={connect_duration:.1f}ms"
  362. )
  363. except asyncio.TimeoutError:
  364. raise Exception("STT WebSocket连接超时(10秒)")
  365. # 配置转录参数(参考官方demo)
  366. # ⚠️ 重要:language参数是必需的!
  367. # 官方支持: 'zh'(中文), 'en'(英文), 'ja'(日文), 'ko'(韩文)
  368. # 尝试支持: 'auto'(自动检测) - 需要测试是否有效
  369. stt_language = getattr(settings, 'STT_LANGUAGE', 'zh')
  370. transcription_params = TranscriptionParams(
  371. language=stt_language,
  372. sample_rate=16000,
  373. input_audio_format="pcm"
  374. # corpus_text="" # 可选:辅助识别的语料
  375. )
  376. logger.debug(
  377. f"[STT调试] request_id={self.request_id}, "
  378. f"配置转录参数: language={stt_language}, "
  379. f"sample_rate=16000, format=pcm"
  380. )
  381. # 更新会话配置(禁用VAD,用户通过按钮控制录音)
  382. logger.debug(
  383. f"[STT调试] request_id={self.request_id}, "
  384. f"开始更新会话配置..."
  385. )
  386. config_start = time.time()
  387. try:
  388. await asyncio.wait_for(
  389. asyncio.to_thread(
  390. self.conversation.update_session,
  391. output_modalities=[MultiModality.TEXT],
  392. enable_input_audio_transcription=True,
  393. transcription_params=transcription_params,
  394. enable_turn_detection=False
  395. ),
  396. timeout=5.0 # 5秒配置超时
  397. )
  398. config_duration = (time.time() - config_start) * 1000
  399. logger.debug(
  400. f"[STT调试] request_id={self.request_id}, "
  401. f"会话配置完成, 耗时={config_duration:.1f}ms"
  402. )
  403. except asyncio.TimeoutError:
  404. raise Exception("STT会话配置超时(5秒)")
  405. self.is_connected = True
  406. connect_duration = (time.time() - start_time) * 1000
  407. logger.info(
  408. f"[STT连接成功] request_id={self.request_id}, "
  409. f"尝试次数={attempt + 1}, "
  410. f"耗时={connect_duration:.1f}ms"
  411. )
  412. return True
  413. except Exception as e:
  414. last_error = e
  415. connect_duration = (time.time() - start_time) * 1000 if 'start_time' in locals() else 0
  416. # 详细记录错误
  417. error_details = {
  418. "request_id": self.request_id,
  419. "错误类型": "STT连接失败",
  420. "异常类型": type(e).__name__,
  421. "错误消息": str(e),
  422. "尝试次数": f"{attempt + 1}/{max_retries + 1}",
  423. "耗时": f"{connect_duration:.1f}ms",
  424. "API Key": f"{settings.active_dashscope_api_key[:10]}... (已脱敏)",
  425. "使用国际版": settings.use_dashscope_intl,
  426. }
  427. # 如果是最后一次尝试,记录完整堆栈
  428. if attempt == max_retries:
  429. error_details["堆栈跟踪"] = traceback.format_exc()
  430. logger.error(f"[STT异常] {error_details}")
  431. else:
  432. logger.warning(f"[STT连接重试] {error_details}")
  433. # 如果不是最后一次尝试,等待后重试
  434. if attempt < max_retries:
  435. wait_time = (attempt + 1) * 0.5 # 递增等待:0.5s, 1s
  436. logger.info(f"[STT连接重试] request_id={self.request_id}, {wait_time:.1f}秒后重试...")
  437. await asyncio.sleep(wait_time)
  438. else:
  439. # 最后一次尝试失败,通知错误回调
  440. if self.callback.on_error:
  441. await self.callback.on_error(f"连接失败(已重试{max_retries}次): {str(e)}")
  442. return False
  443. async def send_audio(self, audio_data: bytes) -> bool:
  444. """
  445. 发送音频数据(参考官方demo,带超时保护)
  446. Args:
  447. audio_data: PCM音频数据(原始bytes)
  448. Returns:
  449. 是否发送成功
  450. """
  451. if not self.is_connected or not self.conversation:
  452. logger.warning(
  453. f"[STT警告] request_id={self.request_id}, "
  454. f"连接状态异常: is_connected={self.is_connected}, "
  455. f"conversation={'存在' if self.conversation else '不存在'}"
  456. )
  457. return False
  458. try:
  459. # 🔥 音频数据验证和统计(用于诊断问题)
  460. import struct
  461. try:
  462. # 将PCM字节数据转换为Int16数组
  463. sample_count = len(audio_data) // 2
  464. if sample_count > 0:
  465. samples = struct.unpack(f'<{sample_count}h', audio_data)
  466. max_amplitude = max(abs(s) for s in samples) if samples else 0
  467. avg_amplitude = sum(abs(s) for s in samples) // len(samples) if samples else 0
  468. zero_count = sum(1 for s in samples if s == 0)
  469. zero_ratio = zero_count / len(samples) if samples else 0.0
  470. # 记录音频统计信息(仅第一个chunk和异常情况)
  471. if not hasattr(self, '_audio_chunk_count'):
  472. self._audio_chunk_count = 0
  473. self._audio_chunk_count += 1
  474. # 第一个chunk或异常情况(全零或振幅过低)时记录详细信息
  475. if self._audio_chunk_count == 1 or max_amplitude < 100 or zero_ratio > 0.9:
  476. logger.info(
  477. f"[STT音频统计] request_id={self.request_id}, "
  478. f"chunk={self._audio_chunk_count}, "
  479. f"长度={len(audio_data)} bytes, "
  480. f"采样数={sample_count}, "
  481. f"最大振幅={max_amplitude}, "
  482. f"平均振幅={avg_amplitude}, "
  483. f"零值比例={zero_ratio:.2%}, "
  484. f"是否静音={'是' if max_amplitude < 100 or zero_ratio > 0.9 else '否'}"
  485. )
  486. except Exception as stats_error:
  487. logger.warning(
  488. f"[STT音频统计] request_id={self.request_id}, "
  489. f"统计计算失败: {stats_error}"
  490. )
  491. # Base64编码(API要求)
  492. import base64
  493. audio_b64 = base64.b64encode(audio_data).decode('ascii')
  494. # 使用INFO级别记录音频准备发送(便于排查问题)
  495. logger.info(
  496. f"[STT] request_id={self.request_id}, "
  497. f"准备发送音频: 原始长度={len(audio_data)} bytes, "
  498. f"Base64长度={len(audio_b64)} chars"
  499. )
  500. # 发送音频帧(参考官方demo,添加超时保护)
  501. send_start = time.time()
  502. try:
  503. await asyncio.wait_for(
  504. asyncio.to_thread(self.conversation.append_audio, audio_b64),
  505. timeout=5.0 # 5秒发送超时
  506. )
  507. send_duration = (time.time() - send_start) * 1000
  508. # 使用INFO级别记录音频发送成功(便于排查问题)
  509. logger.info(
  510. f"[STT] request_id={self.request_id}, "
  511. f"音频发送成功, 原始长度={len(audio_data)} bytes, "
  512. f"耗时={send_duration:.1f}ms"
  513. )
  514. except asyncio.TimeoutError:
  515. raise Exception("发送音频数据超时(5秒)")
  516. return True
  517. except Exception as e:
  518. # 标记连接可能已断开
  519. error_str = str(e).lower()
  520. if 'timeout' in error_str or 'closed' in error_str or 'disconnect' in error_str:
  521. self.is_connected = False
  522. logger.error(
  523. f"[STT异常] request_id={self.request_id}, "
  524. f"错误类型=连接可能已断开, "
  525. f"异常类型={type(e).__name__}, "
  526. f"错误消息={str(e)}, "
  527. f"音频数据长度={len(audio_data)} bytes"
  528. )
  529. else:
  530. logger.error(
  531. f"[STT异常] request_id={self.request_id}, "
  532. f"错误类型=发送音频失败, "
  533. f"异常类型={type(e).__name__}, "
  534. f"错误消息={str(e)}, "
  535. f"音频数据长度={len(audio_data)} bytes, "
  536. f"连接状态=is_connected={self.is_connected}, "
  537. f"堆栈跟踪:\n{traceback.format_exc()}"
  538. )
  539. if self.callback.on_error:
  540. await self.callback.on_error(f"发送音频失败: {e}")
  541. return False
  542. async def commit_audio(self) -> bool:
  543. """
  544. 提交音频以触发识别(禁用VAD时需要手动调用,带超时保护)
  545. Returns:
  546. 是否提交成功
  547. """
  548. if not self.is_connected or not self.conversation:
  549. logger.warning(
  550. f"[STT警告] request_id={self.request_id}, "
  551. f"连接状态异常,无法提交音频: "
  552. f"is_connected={self.is_connected}, "
  553. f"conversation={'存在' if self.conversation else '不存在'}"
  554. )
  555. return False
  556. try:
  557. logger.debug(
  558. f"[STT调试] request_id={self.request_id}, "
  559. f"开始提交音频..."
  560. )
  561. # 添加超时保护
  562. commit_start = time.time()
  563. try:
  564. await asyncio.wait_for(
  565. asyncio.to_thread(self.conversation.commit),
  566. timeout=5.0 # 5秒提交超时
  567. )
  568. commit_duration = (time.time() - commit_start) * 1000
  569. logger.info(
  570. f"[STT] request_id={self.request_id}, "
  571. f"音频提交成功, 耗时={commit_duration:.1f}ms"
  572. )
  573. except asyncio.TimeoutError:
  574. raise Exception("提交音频超时(5秒)")
  575. return True
  576. except Exception as e:
  577. # 标记连接可能已断开
  578. error_str = str(e).lower()
  579. if 'timeout' in error_str or 'closed' in error_str or 'disconnect' in error_str:
  580. self.is_connected = False
  581. logger.error(
  582. f"[STT异常] request_id={self.request_id}, "
  583. f"错误类型=提交音频失败, "
  584. f"异常类型={type(e).__name__}, "
  585. f"错误消息={str(e)}, "
  586. f"连接状态=is_connected={self.is_connected}, "
  587. f"堆栈跟踪:\n{traceback.format_exc()}"
  588. )
  589. return False
  590. async def wait_for_final_result(self, timeout: float = 3.0) -> Optional[str]:
  591. """
  592. 等待最终识别结果
  593. Args:
  594. timeout: 超时时间(秒)
  595. Returns:
  596. 识别文本,超时或失败返回None
  597. """
  598. logger.debug(
  599. f"[STT调试] request_id={self.request_id}, "
  600. f"开始等待最终识别结果, 超时时间={timeout}秒"
  601. )
  602. if not self.callback.final_event:
  603. logger.debug(
  604. f"[STT调试] request_id={self.request_id}, "
  605. f"未设置事件循环,直接返回当前文本"
  606. )
  607. return self.callback.get_text()
  608. wait_start = time.time()
  609. try:
  610. await asyncio.wait_for(self.callback.final_event.wait(), timeout=timeout)
  611. wait_duration = (time.time() - wait_start) * 1000
  612. result_text = self.callback.get_text()
  613. logger.info(
  614. f"[STT] request_id={self.request_id}, "
  615. f"收到最终识别结果, 等待耗时={wait_duration:.1f}ms, "
  616. f"文本长度={len(result_text)}, "
  617. f"文本内容={result_text[:50]}{'...' if len(result_text) > 50 else ''}"
  618. )
  619. return result_text
  620. except asyncio.TimeoutError:
  621. # 超时后,检查是否有中间结果和语音检测状态
  622. wait_duration = (time.time() - wait_start) * 1000
  623. current_text = self.callback.get_text()
  624. speech_detected = self.callback.speech_detected
  625. speech_start_count = self.callback.speech_start_count
  626. speech_stop_count = self.callback.speech_stop_count
  627. # 🔥 输出事件接收统计(用于诊断)
  628. event_count = self.callback.event_count
  629. received_events = self.callback.received_events
  630. logger.warning(
  631. f"[STT警告] request_id={self.request_id}, "
  632. f"错误类型=等待识别结果超时, "
  633. f"超时时间={timeout}秒, "
  634. f"等待耗时={wait_duration:.1f}ms, "
  635. f"当前识别文本={current_text}, "
  636. f"语音检测状态=speech_detected={speech_detected}, "
  637. f"speech_start_count={speech_start_count}, "
  638. f"speech_stop_count={speech_stop_count}, "
  639. f"收到事件总数={event_count}, "
  640. f"收到的事件类型={received_events}"
  641. )
  642. return current_text
  643. except Exception as e:
  644. logger.error(
  645. f"[STT异常] request_id={self.request_id}, "
  646. f"错误类型=等待识别结果异常, "
  647. f"异常类型={type(e).__name__}, "
  648. f"错误消息={str(e)}, "
  649. f"超时时间={timeout}秒, "
  650. f"堆栈跟踪:\n{traceback.format_exc()}"
  651. )
  652. return None
  653. async def close(self):
  654. """关闭STT会话(参考官方demo)"""
  655. if self.is_closed:
  656. logger.debug(
  657. f"[STT调试] request_id={self.request_id}, "
  658. f"会话已关闭,跳过"
  659. )
  660. return
  661. logger.debug(
  662. f"[STT调试] request_id={self.request_id}, "
  663. f"开始关闭STT会话, "
  664. f"连接状态=is_connected={self.is_connected}, "
  665. f"conversation={'存在' if self.conversation else '不存在'}"
  666. )
  667. try:
  668. if self.conversation:
  669. # 异步关闭连接
  670. close_start = time.time()
  671. await asyncio.to_thread(self.conversation.close)
  672. close_duration = (time.time() - close_start) * 1000
  673. logger.debug(
  674. f"[STT调试] request_id={self.request_id}, "
  675. f"会话关闭完成, 耗时={close_duration:.1f}ms"
  676. )
  677. self.is_connected = False
  678. self.is_closed = True
  679. except Exception as e:
  680. logger.error(
  681. f"[STT异常] request_id={self.request_id}, "
  682. f"错误类型=关闭会话异常, "
  683. f"异常类型={type(e).__name__}, "
  684. f"错误消息={str(e)}, "
  685. f"连接状态=is_connected={self.is_connected}, is_closed={self.is_closed}, "
  686. f"堆栈跟踪:\n{traceback.format_exc()}"
  687. )
  688. def get_current_text(self) -> str:
  689. """获取当前识别文本"""
  690. return self.callback.get_text()
  691. def get_last_update_time(self) -> float:
  692. """获取最后更新时间"""
  693. return self.callback.last_update_time
  694. def get_first_text_time(self) -> Optional[float]:
  695. """获取首个文本生成时间"""
  696. return self.callback.first_text_time
  697. class STTService:
  698. """STT服务管理器(单例)"""
  699. _instance = None
  700. def __new__(cls):
  701. if cls._instance is None:
  702. cls._instance = super().__new__(cls)
  703. return cls._instance
  704. def __init__(self):
  705. """初始化STT服务"""
  706. if not hasattr(self, '_initialized'):
  707. self._initialized = True
  708. # 设置API Key
  709. dashscope.api_key = settings.active_dashscope_api_key
  710. # 国际版endpoint配置
  711. if settings.use_dashscope_intl:
  712. from config import get_dashscope_domain
  713. dashscope_domain = get_dashscope_domain()
  714. dashscope.base_http_api_url = f"https://{dashscope_domain}/api/v1"
  715. dashscope.base_websocket_api_url = f"wss://{dashscope_domain}/api-ws/v1"
  716. async def create_session(
  717. self,
  718. request_id: str,
  719. on_text_update: Optional[Callable[[str, bool], Awaitable[None]]] = None,
  720. on_error: Optional[Callable[[str], Awaitable[None]]] = None,
  721. event_loop: Optional[asyncio.AbstractEventLoop] = None
  722. ) -> STTSession:
  723. """
  724. 创建STT识别会话
  725. Args:
  726. request_id: 请求ID
  727. on_text_update: 文本更新回调
  728. on_error: 错误回调
  729. event_loop: 事件循环
  730. Returns:
  731. STT会话对象
  732. """
  733. session = STTSession(
  734. request_id=request_id,
  735. on_text_update=on_text_update,
  736. on_error=on_error,
  737. event_loop=event_loop
  738. )
  739. # 自动连接
  740. success = await session.connect()
  741. return session
  742. # 全局STT服务实例
  743. stt_service = STTService()