mirror of
https://github.com/xinnan-tech/xiaozhi-esp32-server.git
synced 2026-07-27 09:33:55 +08:00
Merge pull request #2887 from shengzhou1216/refactor/asr-delete_audio
refactor(asr): 统一音频预处理逻辑并引入AudioArtifacts
This commit is contained in:
@@ -220,29 +220,18 @@ class ASRProvider(ASRProviderBase):
|
|||||||
logger.warning("Token已过期,正在自动刷新...")
|
logger.warning("Token已过期,正在自动刷新...")
|
||||||
self._refresh_token()
|
self._refresh_token()
|
||||||
|
|
||||||
file_path = None
|
|
||||||
try:
|
try:
|
||||||
# 解码Opus为PCM
|
artifacts = self.get_current_artifacts()
|
||||||
if audio_format == "pcm":
|
if artifacts is None:
|
||||||
pcm_data = opus_data
|
return "", None
|
||||||
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
|
||||||
|
|||||||
@@ -37,30 +37,20 @@ class ASRProvider(ASRProviderBase):
|
|||||||
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
|
artifacts = self.get_current_artifacts()
|
||||||
if audio_format == "pcm":
|
if artifacts is None:
|
||||||
pcm_data = opus_data
|
return "", None
|
||||||
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 +63,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
|
||||||
|
|||||||
@@ -8,14 +8,16 @@ 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.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()
|
||||||
@@ -23,7 +25,8 @@ logger = setup_logging()
|
|||||||
|
|
||||||
class ASRProviderBase(ABC):
|
class ASRProviderBase(ABC):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
pass
|
self._current_artifacts: Optional[ASRProviderBase.AudioArtifacts] = None
|
||||||
|
"""当前正在处理的音频 artifact"""
|
||||||
|
|
||||||
# 打开音频通道
|
# 打开音频通道
|
||||||
async def open_audio_channels(self, conn):
|
async def open_audio_channels(self, conn):
|
||||||
@@ -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
|
||||||
@@ -160,19 +167,19 @@ class ASRProviderBase(ABC):
|
|||||||
# 使用自定义模块进行上报
|
# 使用自定义模块进行上报
|
||||||
await startToChat(conn, enhanced_text)
|
await startToChat(conn, enhanced_text)
|
||||||
enqueue_asr_report(conn, enhanced_text, asr_audio_task)
|
enqueue_asr_report(conn, enhanced_text, asr_audio_task)
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
@@ -181,23 +188,23 @@ class ASRProviderBase(ABC):
|
|||||||
if len(pcm_data) == 0:
|
if len(pcm_data) == 0:
|
||||||
logger.bind(tag=TAG).warning("PCM数据为空,无法转换WAV")
|
logger.bind(tag=TAG).warning("PCM数据为空,无法转换WAV")
|
||||||
return b""
|
return b""
|
||||||
|
|
||||||
# 确保数据长度是偶数(16位音频)
|
# 确保数据长度是偶数(16位音频)
|
||||||
if len(pcm_data) % 2 != 0:
|
if len(pcm_data) % 2 != 0:
|
||||||
pcm_data = pcm_data[:-1]
|
pcm_data = pcm_data[:-1]
|
||||||
|
|
||||||
# 创建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)
|
||||||
|
|
||||||
wav_buffer.seek(0)
|
wav_buffer.seek(0)
|
||||||
wav_data = wav_buffer.read()
|
wav_data = wav_buffer.read()
|
||||||
|
|
||||||
return wav_data
|
return wav_data
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.bind(tag=TAG).error(f"WAV转换失败: {e}")
|
logger.bind(tag=TAG).error(f"WAV转换失败: {e}")
|
||||||
@@ -206,6 +213,37 @@ class ASRProviderBase(ABC):
|
|||||||
def stop_ws_connection(self):
|
def stop_ws_connection(self):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
class AudioArtifacts(NamedTuple):
|
||||||
|
pcm_frames: List[bytes]
|
||||||
|
pcm_bytes: bytes
|
||||||
|
file_path: Optional[str]
|
||||||
|
temp_path: Optional[str]
|
||||||
|
|
||||||
|
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]
|
||||||
@@ -220,6 +258,59 @@ class ASRProviderBase(ABC):
|
|||||||
|
|
||||||
return file_path
|
return file_path
|
||||||
|
|
||||||
|
async def speech_to_text_wrapper(
|
||||||
|
self, opus_data: List[bytes], session_id: str, audio_format="opus"
|
||||||
|
) -> 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)
|
||||||
|
|
||||||
|
self._current_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)
|
||||||
|
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
|
@abstractmethod
|
||||||
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"
|
||||||
@@ -235,23 +326,23 @@ class ASRProviderBase(ABC):
|
|||||||
decoder = opuslib_next.Decoder(16000, 1)
|
decoder = opuslib_next.Decoder(16000, 1)
|
||||||
pcm_data = []
|
pcm_data = []
|
||||||
buffer_size = 960 # 每次处理960个采样点 (60ms at 16kHz)
|
buffer_size = 960 # 每次处理960个采样点 (60ms at 16kHz)
|
||||||
|
|
||||||
for i, opus_packet in enumerate(opus_data):
|
for i, opus_packet in enumerate(opus_data):
|
||||||
try:
|
try:
|
||||||
if not opus_packet or len(opus_packet) == 0:
|
if not opus_packet or len(opus_packet) == 0:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
pcm_frame = decoder.decode(opus_packet, buffer_size)
|
pcm_frame = decoder.decode(opus_packet, buffer_size)
|
||||||
if pcm_frame and len(pcm_frame) > 0:
|
if pcm_frame and len(pcm_frame) > 0:
|
||||||
pcm_data.append(pcm_frame)
|
pcm_data.append(pcm_frame)
|
||||||
|
|
||||||
except opuslib_next.OpusError as e:
|
except opuslib_next.OpusError as e:
|
||||||
logger.bind(tag=TAG).warning(f"Opus解码错误,跳过数据包 {i}: {e}")
|
logger.bind(tag=TAG).warning(f"Opus解码错误,跳过数据包 {i}: {e}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.bind(tag=TAG).error(f"音频处理错误,数据包 {i}: {e}")
|
logger.bind(tag=TAG).error(f"音频处理错误,数据包 {i}: {e}")
|
||||||
|
|
||||||
return pcm_data
|
return pcm_data
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.bind(tag=TAG).error(f"音频解码过程发生错误: {e}")
|
logger.bind(tag=TAG).error(f"音频解码过程发生错误: {e}")
|
||||||
return []
|
return []
|
||||||
|
|||||||
@@ -236,20 +236,10 @@ class ASRProvider(ASRProviderBase):
|
|||||||
) -> Tuple[Optional[str], Optional[str]]:
|
) -> Tuple[Optional[str], Optional[str]]:
|
||||||
"""将语音数据转换为文本"""
|
"""将语音数据转换为文本"""
|
||||||
|
|
||||||
file_path = None
|
|
||||||
try:
|
try:
|
||||||
# 合并所有opus数据包
|
artifacts = self.get_current_artifacts()
|
||||||
if audio_format == "pcm":
|
if artifacts is None:
|
||||||
pcm_data = opus_data
|
return "", None
|
||||||
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 +248,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
|
||||||
|
|||||||
@@ -67,36 +67,19 @@ class ASRProvider(ASRProviderBase):
|
|||||||
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
|
|
||||||
retry_count = 0
|
retry_count = 0
|
||||||
|
|
||||||
while retry_count < MAX_RETRIES:
|
while retry_count < MAX_RETRIES:
|
||||||
try:
|
try:
|
||||||
# 合并所有opus数据包
|
artifacts = self.get_current_artifacts()
|
||||||
if audio_format == "pcm":
|
if artifacts is None:
|
||||||
pcm_data = opus_data
|
return "", None
|
||||||
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 +90,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 +98,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 +106,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}"
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -109,18 +109,10 @@ 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
|
artifacts = self.get_current_artifacts()
|
||||||
if audio_format == "pcm":
|
|
||||||
pcm_data = opus_data
|
if artifacts is None:
|
||||||
else:
|
return "", None
|
||||||
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)
|
|
||||||
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 +124,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 +153,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,20 +21,17 @@ class ASRProvider(ASRProviderBase):
|
|||||||
|
|
||||||
os.makedirs(self.output_dir, exist_ok=True)
|
os.makedirs(self.output_dir, exist_ok=True)
|
||||||
|
|
||||||
|
def requires_file(self) -> bool:
|
||||||
|
return True
|
||||||
|
|
||||||
async def speech_to_text(self, opus_data: List[bytes], session_id: str, audio_format="opus") -> Tuple[Optional[str], Optional[str]]:
|
async def speech_to_text(self, opus_data: List[bytes], session_id: str, audio_format="opus") -> Tuple[Optional[str], Optional[str]]:
|
||||||
file_path = None
|
file_path = None
|
||||||
try:
|
try:
|
||||||
start_time = time.time()
|
artifacts = self.get_current_artifacts()
|
||||||
if audio_format == "pcm":
|
if artifacts is None:
|
||||||
pcm_data = opus_data
|
return "", None
|
||||||
else:
|
file_path = artifacts.file_path
|
||||||
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 = {
|
||||||
"Authorization": f"Bearer {self.api_key}",
|
"Authorization": f"Bearer {self.api_key}",
|
||||||
@@ -71,12 +68,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,27 +34,11 @@ 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
|
def requires_file(self) -> bool:
|
||||||
|
return True
|
||||||
# 创建临时WAV文件
|
|
||||||
with tempfile.NamedTemporaryFile(suffix='.wav', delete=False) as temp_file:
|
|
||||||
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"
|
||||||
@@ -63,28 +46,14 @@ class ASRProvider(ASRProviderBase):
|
|||||||
"""将语音数据转换为文本"""
|
"""将语音数据转换为文本"""
|
||||||
temp_file_path = None
|
temp_file_path = None
|
||||||
file_path = None
|
file_path = None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# 解码音频数据
|
artifacts = self.get_current_artifacts()
|
||||||
if audio_format == "pcm":
|
if artifacts is None:
|
||||||
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 +110,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,20 @@ 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"
|
||||||
) -> Tuple[Optional[str], Optional[str]]:
|
) -> Tuple[Optional[str], Optional[str]]:
|
||||||
"""语音转文本主处理逻辑"""
|
"""语音转文本主处理逻辑"""
|
||||||
file_path = None
|
file_path = None
|
||||||
try:
|
try:
|
||||||
# 保存音频文件
|
artifacts = self.get_current_artifacts()
|
||||||
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}"
|
|
||||||
)
|
|
||||||
|
|
||||||
# 语音识别
|
|
||||||
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 +149,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}")
|
|
||||||
|
|||||||
@@ -39,28 +39,18 @@ class ASRProvider(ASRProviderBase):
|
|||||||
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
|
artifacts = self.get_current_artifacts()
|
||||||
if audio_format == "pcm":
|
if artifacts is None:
|
||||||
pcm_data = opus_data
|
return "", None
|
||||||
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 +67,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:
|
||||||
"""构建请求体"""
|
"""构建请求体"""
|
||||||
|
|||||||
@@ -44,36 +44,22 @@ 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"
|
||||||
) -> 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)
|
artifacts = self.get_current_artifacts()
|
||||||
if audio_format == "pcm":
|
if artifacts is None:
|
||||||
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 +67,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 +85,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}")
|
|
||||||
|
|||||||
Reference in New Issue
Block a user