Merge pull request #2930 from xinnan-tech/py_test_AudioArtifacts

Py test audio artifacts
This commit is contained in:
wengzh
2026-02-02 14:23:09 +08:00
committed by GitHub
15 changed files with 214 additions and 275 deletions
@@ -213,36 +213,24 @@ class ASRProvider(ASRProviderBase):
return None return None
async def speech_to_text( async def speech_to_text(
self, opus_data: List[bytes], session_id: str, audio_format="opus" self, opus_data: List[bytes], session_id: str, audio_format="opus", artifacts=None
) -> Tuple[Optional[str], Optional[str]]: ) -> Tuple[Optional[str], Optional[str]]:
"""将语音数据转换为文本""" """将语音数据转换为文本"""
if self._is_token_expired(): if self._is_token_expired():
logger.warning("Token已过期,正在自动刷新...") logger.warning("Token已过期,正在自动刷新...")
self._refresh_token() self._refresh_token()
file_path = None
try: try:
# 解码Opus为PCM if artifacts is None:
if audio_format == "pcm": return "", None
pcm_data = opus_data
else:
pcm_data = self.decode_opus(opus_data)
combined_pcm_data = b"".join(pcm_data)
# 判断是否保存为WAV文件
if self.delete_audio_file:
pass
else:
file_path = self.save_audio_to_file(pcm_data, session_id)
# 发送请求并获取文本 # 发送请求并获取文本
text = await self._send_request(combined_pcm_data) text = await self._send_request(artifacts.pcm_bytes)
if text: if text:
return text, file_path return text, artifacts.file_path
return "", file_path return "", artifacts.file_path
except Exception as e: except Exception as e:
logger.bind(tag=TAG).error(f"语音识别失败: {e}", exc_info=True) logger.bind(tag=TAG).error(f"语音识别失败: {e}", exc_info=True)
return "", file_path return "", None
@@ -324,7 +324,7 @@ class ASRProvider(ASRProviderBase):
logger.bind(tag=TAG).debug("ASR会话清理完成") logger.bind(tag=TAG).debug("ASR会话清理完成")
async def speech_to_text(self, opus_data, session_id, audio_format): async def speech_to_text(self, opus_data, session_id, audio_format, artifacts=None):
"""获取识别结果""" """获取识别结果"""
result = self.text result = self.text
self.text = "" self.text = ""
@@ -308,7 +308,7 @@ class ASRProvider(ASRProviderBase):
logger.bind(tag=TAG).debug("ASR会话清理完成") logger.bind(tag=TAG).debug("ASR会话清理完成")
async def speech_to_text(self, opus_data, session_id, audio_format): async def speech_to_text(self, opus_data, session_id, audio_format, artifacts=None):
"""获取识别结果""" """获取识别结果"""
result = self.text result = self.text
self.text = "" self.text = ""
@@ -30,37 +30,26 @@ class ASRProvider(ASRProviderBase):
os.makedirs(self.output_dir, exist_ok=True) os.makedirs(self.output_dir, exist_ok=True)
async def speech_to_text( async def speech_to_text(
self, opus_data: List[bytes], session_id: str, audio_format="opus" self, opus_data: List[bytes], session_id: str, audio_format="opus", artifacts=None
) -> Tuple[Optional[str], Optional[str]]: ) -> Tuple[Optional[str], Optional[str]]:
"""将语音数据转换为文本""" """将语音数据转换为文本"""
if not opus_data: if not opus_data:
logger.bind(tag=TAG).warning("音频数据为空!") logger.bind(tag=TAG).warning("音频数据为空!")
return None, None return None, None
file_path = None
try: try:
# 检查配置是否已设置 # 检查配置是否已设置
if not self.app_id or not self.api_key or not self.secret_key: if not self.app_id or not self.api_key or not self.secret_key:
logger.bind(tag=TAG).error("百度语音识别配置未设置,无法进行识别") logger.bind(tag=TAG).error("百度语音识别配置未设置,无法进行识别")
return None, file_path return None, None
# 将Opus音频数据解码为PCM if artifacts is None:
if audio_format == "pcm": return "", None
pcm_data = opus_data
else:
pcm_data = self.decode_opus(opus_data)
combined_pcm_data = b"".join(pcm_data)
# 判断是否保存为WAV文件
if self.delete_audio_file:
pass
else:
self.save_audio_to_file(pcm_data, session_id)
start_time = time.time() start_time = time.time()
# 识别本地文件 # 识别本地文件
result = self.client.asr( result = self.client.asr(
combined_pcm_data, artifacts.pcm_bytes,
"pcm", "pcm",
16000, 16000,
{ {
@@ -73,13 +62,13 @@ class ASRProvider(ASRProviderBase):
f"百度语音识别耗时: {time.time() - start_time:.3f}s | 结果: {result}" f"百度语音识别耗时: {time.time() - start_time:.3f}s | 结果: {result}"
) )
result = result["result"][0] result = result["result"][0]
return result, file_path return result, artifacts.file_path
else: else:
raise Exception( raise Exception(
f"百度语音识别失败,错误码: {result['err_no']},错误信息: {result['err_msg']}" f"百度语音识别失败,错误码: {result['err_no']},错误信息: {result['err_msg']}"
) )
return None, file_path return None, artifacts.file_path
except Exception as e: except Exception as e:
logger.bind(tag=TAG).error(f"处理音频时发生错误!{e}", exc_info=True) logger.bind(tag=TAG).error(f"处理音频时发生错误!{e}", exc_info=True)
return None, file_path return None, None
+124 -14
View File
@@ -8,15 +8,18 @@ import queue
import asyncio import asyncio
import traceback import traceback
import threading import threading
import shutil
import opuslib_next import opuslib_next
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from config.logger import setup_logging from config.logger import setup_logging
from typing import Optional, Tuple, List from typing import Optional, Tuple, List, NamedTuple
from core.providers.asr.dto.dto import InterfaceType from core.providers.asr.dto.dto import InterfaceType
from core.handle.receiveAudioHandle import startToChat from core.handle.receiveAudioHandle import startToChat
from core.handle.reportHandle import enqueue_asr_report from core.handle.reportHandle import enqueue_asr_report
from core.utils.util import remove_punctuation_and_length from core.utils.util import remove_punctuation_and_length
from core.handle.receiveAudioHandle import handleAudioMessage from core.handle.receiveAudioHandle import handleAudioMessage
import tempfile
TAG = __name__ TAG = __name__
logger = setup_logging() logger = setup_logging()
@@ -93,10 +96,14 @@ class ASRProviderBase(ABC):
wav_data = self._pcm_to_wav(combined_pcm_data) wav_data = self._pcm_to_wav(combined_pcm_data)
# 定义ASR任务 # 定义ASR任务
asr_task = self.speech_to_text(asr_audio_task, conn.session_id, conn.audio_format) asr_task = self.speech_to_text_wrapper(
asr_audio_task, conn.session_id, conn.audio_format
)
if conn.voiceprint_provider and wav_data: if conn.voiceprint_provider and wav_data:
voiceprint_task = conn.voiceprint_provider.identify_speaker(wav_data, conn.session_id) voiceprint_task = conn.voiceprint_provider.identify_speaker(
wav_data, conn.session_id
)
# 并发等待两个结果 # 并发等待两个结果
asr_result, voiceprint_result = await asyncio.gather( asr_result, voiceprint_result = await asyncio.gather(
asr_task, voiceprint_task, return_exceptions=True asr_task, voiceprint_task, return_exceptions=True
@@ -161,19 +168,18 @@ class ASRProviderBase(ABC):
await startToChat(conn, enhanced_text) await startToChat(conn, enhanced_text)
audio_snapshot = asr_audio_task.copy() audio_snapshot = asr_audio_task.copy()
enqueue_asr_report(conn, enhanced_text, audio_snapshot) enqueue_asr_report(conn, enhanced_text, audio_snapshot)
except Exception as e: except Exception as e:
logger.bind(tag=TAG).error(f"处理语音停止失败: {e}") logger.bind(tag=TAG).error(f"处理语音停止失败: {e}")
import traceback import traceback
logger.bind(tag=TAG).debug(f"异常详情: {traceback.format_exc()}") logger.bind(tag=TAG).debug(f"异常详情: {traceback.format_exc()}")
def _build_enhanced_text(self, text: str, speaker_name: Optional[str]) -> str: def _build_enhanced_text(self, text: str, speaker_name: Optional[str]) -> str:
"""构建包含说话人信息的文本(仅用于纯文本ASR)""" """构建包含说话人信息的文本(仅用于纯文本ASR)"""
if speaker_name and speaker_name.strip(): if speaker_name and speaker_name.strip():
return json.dumps({ return json.dumps(
"speaker": speaker_name, {"speaker": speaker_name, "content": text}, ensure_ascii=False
"content": text )
}, ensure_ascii=False)
else: else:
return text return text
@@ -190,9 +196,9 @@ class ASRProviderBase(ABC):
# 创建WAV文件头 # 创建WAV文件头
wav_buffer = io.BytesIO() wav_buffer = io.BytesIO()
try: try:
with wave.open(wav_buffer, 'wb') as wav_file: with wave.open(wav_buffer, "wb") as wav_file:
wav_file.setnchannels(1) # 单声道 wav_file.setnchannels(1) # 单声道
wav_file.setsampwidth(2) # 16位 wav_file.setsampwidth(2) # 16位
wav_file.setframerate(16000) # 16kHz采样率 wav_file.setframerate(16000) # 16kHz采样率
wav_file.writeframes(pcm_data) wav_file.writeframes(pcm_data)
@@ -210,6 +216,41 @@ class ASRProviderBase(ABC):
async def close(self): async def close(self):
pass pass
class AudioArtifacts(NamedTuple):
pcm_frames: List[bytes]
"""PCM音频帧列表"""
pcm_bytes: bytes
"""合并后的PCM音频字节数据"""
file_path: Optional[str]
"""WAV文件路径"""
temp_path: Optional[str]
"""临时WAV文件路径"""
def get_current_artifacts(self) -> Optional["ASRProviderBase.AudioArtifacts"]:
return self._current_artifacts
def requires_file(self) -> bool:
"""是否需要文件输入"""
return False
def prefers_temp_file(self) -> bool:
"""是否优先使用临时文件"""
return False
def build_temp_file(self, pcm_bytes: bytes) -> Optional[str]:
try:
with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as temp_file:
temp_path = temp_file.name
with wave.open(temp_path, "wb") as wav_file:
wav_file.setnchannels(1)
wav_file.setsampwidth(2)
wav_file.setframerate(16000)
wav_file.writeframes(pcm_bytes)
return temp_path
except Exception as e:
logger.bind(tag=TAG).error(f"临时音频文件生成失败: {e}")
return None
def save_audio_to_file(self, pcm_data: List[bytes], session_id: str) -> str: def save_audio_to_file(self, pcm_data: List[bytes], session_id: str) -> str:
"""PCM数据保存为WAV文件""" """PCM数据保存为WAV文件"""
module_name = __name__.split(".")[-1] module_name = __name__.split(".")[-1]
@@ -224,11 +265,80 @@ class ASRProviderBase(ABC):
return file_path return file_path
@abstractmethod async def speech_to_text_wrapper(
async def speech_to_text(
self, opus_data: List[bytes], session_id: str, audio_format="opus" self, opus_data: List[bytes], session_id: str, audio_format="opus"
) -> Tuple[Optional[str], Optional[str]]: ) -> Tuple[Optional[str], Optional[str]]:
"""将语音数据转换为文本""" file_path = None
temp_path = None
try:
if audio_format == "pcm":
pcm_data = opus_data
else:
pcm_data = self.decode_opus(opus_data)
combined_pcm_data = b"".join(pcm_data)
free_space = shutil.disk_usage(self.output_dir).free
if free_space < len(combined_pcm_data) * 2:
raise OSError("磁盘空间不足")
if self.requires_file() and self.prefers_temp_file():
temp_path = self.build_temp_file(combined_pcm_data)
if (hasattr(self, "delete_audio_file") and not self.delete_audio_file) or (
self.requires_file() and not self.prefers_temp_file()
):
file_path = self.save_audio_to_file(pcm_data, session_id)
if len(combined_pcm_data) == 0:
artifacts = None
else:
artifacts = ASRProviderBase.AudioArtifacts(
pcm_frames=pcm_data,
pcm_bytes=combined_pcm_data,
file_path=file_path,
temp_path=temp_path,
)
text, _ = await self.speech_to_text(
opus_data, session_id, audio_format, artifacts
)
return text, file_path
except OSError as e:
logger.bind(tag=TAG).error(f"文件操作错误: {e}")
return None, None
except Exception as e:
logger.bind(tag=TAG).error(f"语音识别失败: {e}")
return None, None
finally:
try:
if temp_path and os.path.exists(temp_path):
os.unlink(temp_path)
if (
hasattr(self, "delete_audio_file")
and self.delete_audio_file
and file_path
and os.path.exists(file_path)
):
os.remove(file_path)
except Exception as e:
logger.bind(tag=TAG).error(f"文件清理失败: {e}")
@abstractmethod
async def speech_to_text(
self,
opus_data: List[bytes],
session_id: str,
audio_format="opus",
artifacts: Optional[AudioArtifacts] = None,
) -> Tuple[Optional[str], Optional[str]]:
"""将语音数据转换为文本
:param opus_data: 输入的Opus音频数据
:param session_id: 会话ID
:param audio_format: 音频格式,默认"opus"
:param artifacts: 音频工件,包含PCM数据、文件路径等
:return: 识别结果文本和文件路径(如果有)
"""
pass pass
@staticmethod @staticmethod
@@ -232,24 +232,13 @@ class ASRProvider(ASRProviderBase):
yield data[offset:data_len], True yield data[offset:data_len], True
async def speech_to_text( async def speech_to_text(
self, opus_data: List[bytes], session_id: str, audio_format="opus" self, opus_data: List[bytes], session_id: str, audio_format="opus", artifacts=None
) -> Tuple[Optional[str], Optional[str]]: ) -> Tuple[Optional[str], Optional[str]]:
"""将语音数据转换为文本""" """将语音数据转换为文本"""
file_path = None
try: try:
# 合并所有opus数据包 if artifacts is None:
if audio_format == "pcm": return "", None
pcm_data = opus_data
else:
pcm_data = self.decode_opus(opus_data)
combined_pcm_data = b"".join(pcm_data)
# 判断是否保存为WAV文件
if self.delete_audio_file:
pass
else:
file_path = self.save_audio_to_file(pcm_data, session_id)
# 直接使用PCM数据 # 直接使用PCM数据
# 计算分段大小 (单声道, 16bit, 16kHz采样率) # 计算分段大小 (单声道, 16bit, 16kHz采样率)
@@ -258,14 +247,14 @@ class ASRProvider(ASRProviderBase):
# 语音识别 # 语音识别
start_time = time.time() start_time = time.time()
text = await self._send_request(combined_pcm_data, segment_size) text = await self._send_request(artifacts.pcm_bytes, segment_size)
if text: if text:
logger.bind(tag=TAG).debug( logger.bind(tag=TAG).debug(
f"语音识别耗时: {time.time() - start_time:.3f}s | 结果: {text}" f"语音识别耗时: {time.time() - start_time:.3f}s | 结果: {text}"
) )
return text, file_path return text, artifacts.file_path
return "", file_path return "", artifacts.file_path
except Exception as e: except Exception as e:
logger.bind(tag=TAG).error(f"语音识别失败: {e}", exc_info=True) logger.bind(tag=TAG).error(f"语音识别失败: {e}", exc_info=True)
return "", file_path return "", None
@@ -388,7 +388,7 @@ class ASRProvider(ASRProviderBase):
logger.bind(tag=TAG).error(f"原始响应数据: {res.hex()}") logger.bind(tag=TAG).error(f"原始响应数据: {res.hex()}")
raise raise
async def speech_to_text(self, opus_data, session_id, audio_format): async def speech_to_text(self, opus_data, session_id, audio_format, artifacts=None):
result = self.text result = self.text
self.text = "" # 清空text self.text = "" # 清空text
return result, None return result, None
@@ -64,39 +64,21 @@ class ASRProvider(ASRProviderBase):
) )
async def speech_to_text( async def speech_to_text(
self, opus_data: List[bytes], session_id: str, audio_format="opus" self, opus_data: List[bytes], session_id: str, audio_format="opus", artifacts=None
) -> Tuple[Optional[str], Optional[str]]: ) -> Tuple[Optional[str], Optional[str]]:
"""语音转文本主处理逻辑""" """语音转文本主处理逻辑"""
file_path = None
retry_count = 0 retry_count = 0
while retry_count < MAX_RETRIES: while retry_count < MAX_RETRIES:
try: try:
# 合并所有opus数据包 if artifacts is None:
if audio_format == "pcm": return "", None
pcm_data = opus_data
else:
pcm_data = self.decode_opus(opus_data)
combined_pcm_data = b"".join(pcm_data)
# 检查磁盘空间
if not self.delete_audio_file:
free_space = shutil.disk_usage(self.output_dir).free
if free_space < len(combined_pcm_data) * 2: # 预留2倍空间
raise OSError("磁盘空间不足")
# 判断是否保存为WAV文件
if self.delete_audio_file:
pass
else:
file_path = self.save_audio_to_file(pcm_data, session_id)
# 语音识别 - 使用线程池避免阻塞事件循环 # 语音识别 - 使用线程池避免阻塞事件循环
start_time = time.time() start_time = time.time()
result = await asyncio.to_thread( result = await asyncio.to_thread(
self.model.generate, self.model.generate,
input=combined_pcm_data, input=artifacts.pcm_bytes,
cache={}, cache={},
language="auto", language="auto",
use_itn=True, use_itn=True,
@@ -107,7 +89,7 @@ class ASRProvider(ASRProviderBase):
f"语音识别耗时: {time.time() - start_time:.3f}s | 结果: {text['content']}" f"语音识别耗时: {time.time() - start_time:.3f}s | 结果: {text['content']}"
) )
return text, file_path return text, artifacts.file_path
except OSError as e: except OSError as e:
retry_count += 1 retry_count += 1
@@ -115,7 +97,7 @@ class ASRProvider(ASRProviderBase):
logger.bind(tag=TAG).error( logger.bind(tag=TAG).error(
f"语音识别失败(已重试{retry_count}次): {e}", exc_info=True f"语音识别失败(已重试{retry_count}次): {e}", exc_info=True
) )
return "", file_path return "", None
logger.bind(tag=TAG).warning( logger.bind(tag=TAG).warning(
f"语音识别失败,正在重试({retry_count}/{MAX_RETRIES}: {e}" f"语音识别失败,正在重试({retry_count}/{MAX_RETRIES}: {e}"
) )
@@ -123,15 +105,4 @@ class ASRProvider(ASRProviderBase):
except Exception as e: except Exception as e:
logger.bind(tag=TAG).error(f"语音识别失败: {e}", exc_info=True) logger.bind(tag=TAG).error(f"语音识别失败: {e}", exc_info=True)
return "", file_path return "", None
finally:
# 文件清理逻辑
if self.delete_audio_file and file_path and os.path.exists(file_path):
try:
os.remove(file_path)
logger.bind(tag=TAG).debug(f"已删除临时音频文件: {file_path}")
except Exception as e:
logger.bind(tag=TAG).error(
f"文件删除失败: {file_path} | 错误: {e}"
)
@@ -101,7 +101,7 @@ class ASRProvider(ASRProviderBase):
logger.bind(tag=TAG).debug(f"Sent end message: {end_message}") logger.bind(tag=TAG).debug(f"Sent end message: {end_message}")
async def speech_to_text( async def speech_to_text(
self, opus_data: List[bytes], session_id: str, audio_format="opus" self, opus_data: List[bytes], session_id: str, audio_format="opus", artifacts=None
) -> Tuple[Optional[str], Optional[str]]: ) -> Tuple[Optional[str], Optional[str]]:
""" """
Convert speech data to text using FunASR. Convert speech data to text using FunASR.
@@ -109,18 +109,9 @@ class ASRProvider(ASRProviderBase):
:param session_id: Unique session identifier. :param session_id: Unique session identifier.
:return: Tuple containing recognized text and optional timestamp. :return: Tuple containing recognized text and optional timestamp.
""" """
file_path = None
if audio_format == "pcm":
pcm_data = opus_data
else:
pcm_data = self.decode_opus(opus_data)
combined_pcm_data = b"".join(pcm_data)
# 判断是否保存为WAV文件 if artifacts is None:
if self.delete_audio_file: return "", None
pass
else:
file_path = self.save_audio_to_file(pcm_data, session_id)
auth_header = {"Authorization": "Bearer; {}".format(self.api_key)} auth_header = {"Authorization": "Bearer; {}".format(self.api_key)}
async with websockets.connect( async with websockets.connect(
self.uri, self.uri,
@@ -132,7 +123,7 @@ class ASRProvider(ASRProviderBase):
try: try:
# Use asyncio to handle WebSocket communication # Use asyncio to handle WebSocket communication
send_task = asyncio.create_task( send_task = asyncio.create_task(
self._send_data(ws, combined_pcm_data, session_id) self._send_data(ws, artifacts.pcm_bytes, session_id)
) )
receive_task = asyncio.create_task(self._receive_responses(ws)) receive_task = asyncio.create_task(self._receive_responses(ws))
@@ -161,14 +152,14 @@ class ASRProvider(ASRProviderBase):
result = lang_tag_filter(result) result = lang_tag_filter(result)
return ( return (
result, result,
file_path, artifacts.file_path,
) # Return the recognized text and timestamp (if any) ) # Return the recognized text and timestamp (if any)
except websockets.exceptions.ConnectionClosed as e: except websockets.exceptions.ConnectionClosed as e:
logger.bind(tag=TAG).error(f"WebSocket connection closed: {e}") logger.bind(tag=TAG).error(f"WebSocket connection closed: {e}")
return "", file_path return "", artifacts.file_path
except Exception as e: except Exception as e:
logger.bind(tag=TAG).error( logger.bind(tag=TAG).error(
f"Error during speech-to-text conversion: {e}", exc_info=True f"Error during speech-to-text conversion: {e}", exc_info=True
) )
return "", file_path return "", artifacts.file_path
@@ -21,19 +21,15 @@ class ASRProvider(ASRProviderBase):
os.makedirs(self.output_dir, exist_ok=True) os.makedirs(self.output_dir, exist_ok=True)
async def speech_to_text(self, opus_data: List[bytes], session_id: str, audio_format="opus") -> Tuple[Optional[str], Optional[str]]: def requires_file(self) -> bool:
return True
async def speech_to_text(self, opus_data: List[bytes], session_id: str, audio_format="opus", artifacts=None) -> Tuple[Optional[str], Optional[str]]:
file_path = None file_path = None
try: try:
start_time = time.time() if artifacts is None:
if audio_format == "pcm": return "", None
pcm_data = opus_data file_path = artifacts.file_path
else:
pcm_data = self.decode_opus(opus_data)
file_path = self.save_audio_to_file(pcm_data, session_id)
logger.bind(tag=TAG).debug(
f"音频文件保存耗时: {time.time() - start_time:.3f}s | 路径: {file_path}"
)
logger.bind(tag=TAG).info(f"file path: {file_path}") logger.bind(tag=TAG).info(f"file path: {file_path}")
headers = { headers = {
@@ -71,12 +67,4 @@ class ASRProvider(ASRProviderBase):
except Exception as e: except Exception as e:
logger.bind(tag=TAG).error(f"语音识别失败: {e}") logger.bind(tag=TAG).error(f"语音识别失败: {e}")
return "", None return "", None
finally:
# 文件清理逻辑
if self.delete_audio_file and file_path and os.path.exists(file_path):
try:
os.remove(file_path)
logger.bind(tag=TAG).debug(f"已删除临时音频文件: {file_path}")
except Exception as e:
logger.bind(tag=TAG).error(f"文件删除失败: {file_path} | 错误: {e}")
@@ -1,5 +1,4 @@
import os import os
import tempfile
from typing import Optional, Tuple, List from typing import Optional, Tuple, List
import dashscope import dashscope
from config.logger import setup_logging from config.logger import setup_logging
@@ -35,56 +34,25 @@ class ASRProvider(ASRProviderBase):
# 确保输出目录存在 # 确保输出目录存在
os.makedirs(self.output_dir, exist_ok=True) os.makedirs(self.output_dir, exist_ok=True)
def _prepare_audio_file(self, pcm_data: bytes) -> str: def prefers_temp_file(self) -> bool:
"""将PCM数据转换为WAV文件并返回文件路径""" return True
try:
import wave
# 创建临时WAV文件 def requires_file(self) -> bool:
with tempfile.NamedTemporaryFile(suffix='.wav', delete=False) as temp_file: return True
temp_path = temp_file.name
# 写入WAV格式
with wave.open(temp_path, 'wb') as wav_file:
wav_file.setnchannels(1) # 单声道
wav_file.setsampwidth(2) # 16位
wav_file.setframerate(16000) # 16kHz采样率
wav_file.writeframes(pcm_data)
return temp_path
except Exception as e:
logger.bind(tag=tag).error(f"音频文件准备失败: {e}")
return None
async def speech_to_text( async def speech_to_text(
self, opus_data: List[bytes], session_id: str, audio_format="opus" self, opus_data: List[bytes], session_id: str, audio_format="opus", artifacts=None
) -> Tuple[Optional[str], Optional[str]]: ) -> Tuple[Optional[str], Optional[str]]:
"""将语音数据转换为文本""" """将语音数据转换为文本"""
temp_file_path = None temp_file_path = None
file_path = None file_path = None
try: try:
# 解码音频数据 if artifacts is None:
if audio_format == "pcm":
pcm_data = opus_data
else:
pcm_data = self.decode_opus(opus_data)
combined_pcm_data = b"".join(pcm_data)
if len(combined_pcm_data) == 0:
logger.bind(tag=tag).warning("音频数据为空")
return "", None return "", None
temp_file_path = artifacts.temp_path
# 准备音频文件 file_path = artifacts.file_path
temp_file_path = self._prepare_audio_file(combined_pcm_data)
if not temp_file_path: if not temp_file_path:
return "", None return "", file_path
# 保存音频文件(如果需要)
if not self.delete_audio_file:
file_path = self.save_audio_to_file(pcm_data, session_id)
# 构造请求消息 # 构造请求消息
messages = [ messages = [
{ {
@@ -141,11 +109,3 @@ class ASRProvider(ASRProviderBase):
except Exception as e: except Exception as e:
logger.bind(tag=tag).error(f"语音识别失败: {e}") logger.bind(tag=tag).error(f"语音识别失败: {e}")
return "", file_path return "", file_path
finally:
# 清理临时文件
if temp_file_path and os.path.exists(temp_file_path):
try:
os.unlink(temp_file_path)
except Exception as e:
logger.bind(tag=tag).warning(f"清理临时文件失败: {e}")
@@ -120,24 +120,19 @@ class ASRProvider(ASRProviderBase):
samples_float32 = samples_float32 / 32768 samples_float32 = samples_float32 / 32768
return samples_float32, f.getframerate() return samples_float32, f.getframerate()
def requires_file(self) -> bool:
return True
async def speech_to_text( async def speech_to_text(
self, opus_data: List[bytes], session_id: str, audio_format="opus" self, opus_data: List[bytes], session_id: str, audio_format="opus", artifacts=None
) -> Tuple[Optional[str], Optional[str]]: ) -> Tuple[Optional[str], Optional[str]]:
"""语音转文本主处理逻辑""" """语音转文本主处理逻辑"""
file_path = None file_path = None
try: try:
# 保存音频文件 if artifacts is None:
start_time = time.time() return "", None
if audio_format == "pcm": file_path = artifacts.file_path
pcm_data = opus_data
else:
pcm_data = self.decode_opus(opus_data)
file_path = self.save_audio_to_file(pcm_data, session_id)
logger.bind(tag=TAG).debug(
f"音频文件保存耗时: {time.time() - start_time:.3f}s | 路径: {file_path}"
)
# 语音识别
start_time = time.time() start_time = time.time()
s = self.model.create_stream() s = self.model.create_stream()
samples, sample_rate = self.read_wave(file_path) samples, sample_rate = self.read_wave(file_path)
@@ -153,11 +148,3 @@ class ASRProvider(ASRProviderBase):
except Exception as e: except Exception as e:
logger.bind(tag=TAG).error(f"语音识别失败: {e}", exc_info=True) logger.bind(tag=TAG).error(f"语音识别失败: {e}", exc_info=True)
return "", file_path return "", file_path
finally:
# 文件清理逻辑
if self.delete_audio_file and file_path and os.path.exists(file_path):
try:
os.remove(file_path)
logger.bind(tag=TAG).debug(f"已删除临时音频文件: {file_path}")
except Exception as e:
logger.bind(tag=TAG).error(f"文件删除失败: {file_path} | 错误: {e}")
@@ -32,35 +32,24 @@ class ASRProvider(ASRProviderBase):
os.makedirs(self.output_dir, exist_ok=True) os.makedirs(self.output_dir, exist_ok=True)
async def speech_to_text( async def speech_to_text(
self, opus_data: List[bytes], session_id: str, audio_format="opus" self, opus_data: List[bytes], session_id: str, audio_format="opus", artifacts=None
) -> Tuple[Optional[str], Optional[str]]: ) -> Tuple[Optional[str], Optional[str]]:
"""将语音数据转换为文本""" """将语音数据转换为文本"""
if not opus_data: if not opus_data:
logger.bind(tag=TAG).warning("音频数据为空!") logger.bind(tag=TAG).warning("音频数据为空!")
return None, None return None, None
file_path = None
try: try:
# 检查配置是否已设置 # 检查配置是否已设置
if not self.secret_id or not self.secret_key: if not self.secret_id or not self.secret_key:
logger.bind(tag=TAG).error("腾讯云语音识别配置未设置,无法进行识别") logger.bind(tag=TAG).error("腾讯云语音识别配置未设置,无法进行识别")
return None, file_path return None, None
# 将Opus音频数据解码为PCM if artifacts is None:
if audio_format == "pcm": return "", None
pcm_data = opus_data
else:
pcm_data = self.decode_opus(opus_data)
combined_pcm_data = b"".join(pcm_data)
# 判断是否保存为WAV文件
if self.delete_audio_file:
pass
else:
self.save_audio_to_file(pcm_data, session_id)
# 将音频数据转换为Base64编码 # 将音频数据转换为Base64编码
base64_audio = base64.b64encode(combined_pcm_data).decode("utf-8") base64_audio = base64.b64encode(artifacts.pcm_bytes).decode("utf-8")
# 构建请求体 # 构建请求体
request_body = self._build_request_body(base64_audio) request_body = self._build_request_body(base64_audio)
@@ -77,11 +66,11 @@ class ASRProvider(ASRProviderBase):
f"腾讯云语音识别耗时: {time.time() - start_time:.3f}s | 结果: {result}" f"腾讯云语音识别耗时: {time.time() - start_time:.3f}s | 结果: {result}"
) )
return result, file_path return result, artifacts.file_path
except Exception as e: except Exception as e:
logger.bind(tag=TAG).error(f"处理音频时发生错误!{e}", exc_info=True) logger.bind(tag=TAG).error(f"处理音频时发生错误!{e}", exc_info=True)
return None, file_path return None, None
def _build_request_body(self, base64_audio: str) -> str: def _build_request_body(self, base64_audio: str) -> str:
"""构建请求体""" """构建请求体"""
+6 -29
View File
@@ -44,36 +44,21 @@ class ASRProvider(ASRProviderBase):
raise raise
async def speech_to_text( async def speech_to_text(
self, audio_data: List[bytes], session_id: str, audio_format: str = "opus" self, opus_data: List[bytes], session_id: str, audio_format="opus", artifacts=None
) -> Tuple[Optional[str], Optional[str]]: ) -> Tuple[Optional[str], Optional[str]]:
"""将语音数据转换为文本""" """将语音数据转换为文本"""
file_path = None
try: try:
# 检查模型是否加载成功 # 检查模型是否加载成功
if not self.model: if not self.model:
logger.bind(tag=TAG).error("VOSK模型未加载,无法进行识别") logger.bind(tag=TAG).error("VOSK模型未加载,无法进行识别")
return "", None return "", None
# 解码音频(如果原始格式是Opus if artifacts is None:
if audio_format == "pcm":
pcm_data = audio_data
else:
pcm_data = self.decode_opus(audio_data)
if not pcm_data:
logger.bind(tag=TAG).warning("解码后的PCM数据为空,无法进行识别")
return "", None return "", None
if not artifacts.pcm_bytes:
# 合并PCM数据
combined_pcm_data = b"".join(pcm_data)
if len(combined_pcm_data) == 0:
logger.bind(tag=TAG).warning("合并后的PCM数据为空") logger.bind(tag=TAG).warning("合并后的PCM数据为空")
return "", None return "", None
# 判断是否保存为WAV文件
if not self.delete_audio_file:
file_path = self.save_audio_to_file(pcm_data, session_id)
start_time = time.time() start_time = time.time()
@@ -81,8 +66,8 @@ class ASRProvider(ASRProviderBase):
chunk_size = 2000 chunk_size = 2000
text_result = "" text_result = ""
for i in range(0, len(combined_pcm_data), chunk_size): for i in range(0, len(artifacts.pcm_bytes), chunk_size):
chunk = combined_pcm_data[i:i+chunk_size] chunk = artifacts.pcm_bytes[i:i+chunk_size]
if self.recognizer.AcceptWaveform(chunk): if self.recognizer.AcceptWaveform(chunk):
result = json.loads(self.recognizer.Result()) result = json.loads(self.recognizer.Result())
text = result.get('text', '') text = result.get('text', '')
@@ -99,16 +84,8 @@ class ASRProvider(ASRProviderBase):
f"VOSK语音识别耗时: {time.time() - start_time:.3f}s | 结果: {text_result.strip()}" f"VOSK语音识别耗时: {time.time() - start_time:.3f}s | 结果: {text_result.strip()}"
) )
return text_result.strip(), file_path return text_result.strip(), artifacts.file_path
except Exception as e: except Exception as e:
logger.bind(tag=TAG).error(f"VOSK语音识别失败: {e}") logger.bind(tag=TAG).error(f"VOSK语音识别失败: {e}")
return "", None return "", None
finally:
# 文件清理逻辑
if self.delete_audio_file and file_path and os.path.exists(file_path):
try:
os.remove(file_path)
logger.bind(tag=TAG).debug(f"已删除临时音频文件: {file_path}")
except Exception as e:
logger.bind(tag=TAG).error(f"文件删除失败: {file_path} | 错误: {e}")
@@ -318,7 +318,7 @@ class ASRProvider(ASRProviderBase):
logger.bind(tag=TAG).debug("ASR会话清理完成") logger.bind(tag=TAG).debug("ASR会话清理完成")
async def speech_to_text(self, opus_data, session_id, audio_format): async def speech_to_text(self, opus_data, session_id, audio_format, artifacts=None):
"""获取识别结果""" """获取识别结果"""
result = self.text result = self.text
self.text = "" self.text = ""