import time
|
import wave
|
import os
|
import sys
|
import io
|
from config.logger import setup_logging
|
from typing import Optional, Tuple, List
|
from core.providers.asr.dto.dto import InterfaceType
|
from core.providers.asr.base import ASRProviderBase
|
|
import numpy as np
|
import sherpa_onnx
|
|
from modelscope.hub.file_download import model_file_download
|
|
TAG = __name__
|
logger = setup_logging()
|
|
|
# 捕获标准输出
|
class CaptureOutput:
|
def __enter__(self):
|
self._output = io.StringIO()
|
self._original_stdout = sys.stdout
|
sys.stdout = self._output
|
|
def __exit__(self, exc_type, exc_value, traceback):
|
sys.stdout = self._original_stdout
|
self.output = self._output.getvalue()
|
self._output.close()
|
|
# 将捕获到的内容通过 logger 输出
|
if self.output:
|
logger.bind(tag=TAG).info(self.output.strip())
|
|
|
class ASRProvider(ASRProviderBase):
|
def __init__(self, config: dict, delete_audio_file: bool):
|
super().__init__()
|
self.interface_type = InterfaceType.LOCAL
|
self.model_dir = config.get("model_dir")
|
self.output_dir = config.get("output_dir")
|
self.model_type = config.get("model_type", "sense_voice") # 支持 paraformer
|
self.delete_audio_file = delete_audio_file
|
|
# 确保输出目录存在
|
os.makedirs(self.output_dir, exist_ok=True)
|
|
# 初始化模型文件路径
|
model_files = {
|
"model.int8.onnx": os.path.join(self.model_dir, "model.int8.onnx"),
|
"tokens.txt": os.path.join(self.model_dir, "tokens.txt"),
|
}
|
|
# 下载并检查模型文件
|
try:
|
for file_name, file_path in model_files.items():
|
if not os.path.isfile(file_path):
|
logger.bind(tag=TAG).info(f"正在下载模型文件: {file_name}")
|
model_file_download(
|
model_id="pengzhendong/sherpa-onnx-sense-voice-zh-en-ja-ko-yue",
|
file_path=file_name,
|
local_dir=self.model_dir,
|
)
|
|
if not os.path.isfile(file_path):
|
raise FileNotFoundError(f"模型文件下载失败: {file_path}")
|
|
self.model_path = model_files["model.int8.onnx"]
|
self.tokens_path = model_files["tokens.txt"]
|
|
except Exception as e:
|
logger.bind(tag=TAG).error(f"模型文件处理失败: {str(e)}")
|
raise
|
|
with CaptureOutput():
|
if self.model_type == "paraformer":
|
self.model = sherpa_onnx.OfflineRecognizer.from_paraformer(
|
paraformer=self.model_path,
|
tokens=self.tokens_path,
|
num_threads=2,
|
sample_rate=16000,
|
feature_dim=80,
|
decoding_method="greedy_search",
|
debug=False,
|
)
|
else: # sense_voice
|
self.model = sherpa_onnx.OfflineRecognizer.from_sense_voice(
|
model=self.model_path,
|
tokens=self.tokens_path,
|
num_threads=2,
|
sample_rate=16000,
|
feature_dim=80,
|
decoding_method="greedy_search",
|
debug=False,
|
use_itn=True,
|
)
|
|
def read_wave(self, wave_filename: str) -> Tuple[np.ndarray, int]:
|
"""
|
Args:
|
wave_filename:
|
Path to a wave file. It should be single channel and each sample should
|
be 16-bit. Its sample rate does not need to be 16kHz.
|
Returns:
|
Return a tuple containing:
|
- A 1-D array of dtype np.float32 containing the samples, which are
|
normalized to the range [-1, 1].
|
- sample rate of the wave file
|
"""
|
|
with wave.open(wave_filename) as f:
|
assert f.getnchannels() == 1, f.getnchannels()
|
assert f.getsampwidth() == 2, f.getsampwidth() # it is in bytes
|
num_samples = f.getnframes()
|
samples = f.readframes(num_samples)
|
samples_int16 = np.frombuffer(samples, dtype=np.int16)
|
samples_float32 = samples_int16.astype(np.float32)
|
|
samples_float32 = samples_float32 / 32768
|
return samples_float32, f.getframerate()
|
|
def requires_file(self) -> bool:
|
return True
|
|
async def speech_to_text(
|
self, opus_data: List[bytes], session_id: str, artifacts=None
|
) -> Tuple[Optional[str], Optional[str]]:
|
"""语音转文本主处理逻辑"""
|
file_path = None
|
try:
|
if artifacts is None:
|
return "", None
|
file_path = artifacts.file_path
|
|
start_time = time.time()
|
s = self.model.create_stream()
|
samples, sample_rate = self.read_wave(file_path)
|
s.accept_waveform(sample_rate, samples)
|
self.model.decode_stream(s)
|
text = s.result.text
|
logger.bind(tag=TAG).debug(
|
f"语音识别耗时: {time.time() - start_time:.3f}s | 结果: {text}"
|
)
|
|
return text, file_path
|
|
except Exception as e:
|
logger.bind(tag=TAG).error(f"语音识别失败: {e}", exc_info=True)
|
return "", file_path
|