| import re |
| import os |
| import json |
| import logging |
| import subprocess |
| import tempfile |
| import librosa |
| import numpy as np |
| import requests |
| from typing import Optional |
| from dataclasses import dataclass |
| from dotenv import load_dotenv |
|
|
| load_dotenv() |
| logger = logging.getLogger(__name__) |
|
|
|
|
| @dataclass |
| class ASRResult: |
| """ASR 识别结果""" |
| text: str |
| lang: str = "" |
| emotion: str = "" |
| event: str = "" |
| timestamps: list = None |
|
|
|
|
| class ASRModel: |
| """语音识别模型封装,调用 asr_server 的 /asr 接口""" |
|
|
| def __init__(self, base_url: str = None, language: str = "auto", sample_rate: int = 16000): |
| """ |
| 初始化 ASR 模型 |
| |
| Args: |
| base_url: ASR 服务地址,如 "http://localhost:8000" |
| language: 默认语言代码 (auto, zh, en, ja, ko, yue) |
| sample_rate: 音频采样率,默认 16000 Hz |
| """ |
| self.base_url = (base_url or os.getenv("ASR_API_BASE_URL", "http://localhost:8000")).rstrip('/') |
| self.language = language or os.getenv("ASR_LANGUAGE", "auto") |
| self.sample_rate = sample_rate |
| self.asr_endpoint = f"{self.base_url}/asr" |
|
|
| def _load_audio(self, audio_path: str) -> np.ndarray: |
| """ |
| 加载音频文件并重采样到目标采样率 |
| |
| Args: |
| audio_path: 音频文件路径 |
| |
| Returns: |
| 音频数组 |
| """ |
| try: |
| waveform, sr = librosa.load(audio_path, sr=None, mono=True) |
| if sr != self.sample_rate: |
| waveform = librosa.resample(waveform, orig_sr=sr, target_sr=self.sample_rate) |
| return waveform |
| except Exception as e: |
| logger.error(f"Failed to load audio file {audio_path}: {e}") |
| raise |
|
|
| def transcribe(self, audio_file_path: str, language: Optional[str] = None) -> ASRResult: |
| """ |
| 转录音频文件 |
| |
| Args: |
| audio_file_path: 音频文件路径 |
| language: 语言代码,不提供则使用默认值 |
| |
| Returns: |
| ASRResult 对象,包含 text 字段 |
| """ |
| lang = language or self.language |
|
|
| try: |
| |
| if not os.path.exists(audio_file_path): |
| raise FileNotFoundError(f"Audio file not found: {audio_file_path}") |
|
|
| |
|
|
| |
| waveform = self._load_audio(audio_file_path) |
|
|
| |
| audio_data = waveform.tolist() |
|
|
| |
| |
|
|
| |
| response = requests.post( |
| self.asr_endpoint, |
| json={ |
| "audio_data": audio_data, |
| "sample_rate": self.sample_rate, |
| "language": lang |
| }, |
| timeout=300 |
| ) |
|
|
| if response.status_code != 200: |
| error_detail = response.json().get("detail", response.text) if response.headers.get("content-type") == "application/json" else response.text |
| logger.error(f"ASR API error: {response.status_code} - {error_detail}") |
| raise RuntimeError(f"ASR API error: {response.status_code} - {error_detail}") |
|
|
| result_data = response.json() |
| text = result_data.get("text", "") |
|
|
| |
| text = self._clean_text(text) |
|
|
| return ASRResult(text=text) |
|
|
| except FileNotFoundError as e: |
| logger.error(f"File error: {e}") |
| raise |
| except requests.RequestException as e: |
| logger.error(f"Failed to connect to ASR API: {e}") |
| raise RuntimeError(f"Failed to connect to ASR API at {self.asr_endpoint}: {e}") |
| except Exception as e: |
| logger.error(f"ASR transcription error: {e}") |
| raise |
|
|
| @staticmethod |
| def _clean_text(text: str) -> str: |
| """清理 SenseVoice 返回的文本中的特殊标记""" |
| |
| text = re.sub(r'<\|[^|]*\|>', '', text) |
| |
| text = re.sub(r'\s+', ' ', text).strip() |
| return text |
|
|
|
|
|
|
| class SherpaOnnxASRClient: |
| """调用 Sherpa-ONNX FastAPI 服务的客户端""" |
|
|
| def __init__(self, base_url: str = None): |
| self.base_url = (base_url or os.getenv("SHERPA_ASR_URL", "http://10.126.102.211:8016")).rstrip("/") |
|
|
| def _parse_response(self, response) -> ASRResult: |
| if response.status_code != 200: |
| raise RuntimeError(f"Sherpa ASR error: {response.status_code} - {response.text}") |
| data = response.json() |
| |
| return ASRResult( |
| text=data.get("text", ""), |
| lang=data.get("lang", ""), |
| emotion=data.get("emotion", ""), |
| event=data.get("event", ""), |
| timestamps=data.get("timestamps", []), |
| ) |
|
|
| def transcribe(self, audio_path: str) -> ASRResult: |
| """通过 /asr/file 接口上传音频文件""" |
| if not os.path.exists(audio_path): |
| raise FileNotFoundError(f"Audio file not found: {audio_path}") |
|
|
| with open(audio_path, "rb") as f: |
| response = requests.post( |
| f"{self.base_url}/asr/file", |
| files={"file": f}, |
| timeout=120, |
| ) |
| return self._parse_response(response) |
|
|
| def transcribe_audio_data(self, audio_data: list, sample_rate: int = 16000) -> ASRResult: |
| """通过 /asr 接口发送音频数组数据""" |
| response = requests.post( |
| f"{self.base_url}/asr", |
| json={"audio_data": audio_data, "sample_rate": sample_rate}, |
| timeout=120, |
| ) |
| return self._parse_response(response) |
|
|