| import base64 |
| import logging |
| import os |
| import tempfile |
| from pathlib import Path |
|
|
| logger = logging.getLogger(__name__) |
|
|
| |
| SUPPORTED_AUDIO_SUFFIXES = {".wav", ".mp3", ".ogg", ".flac", ".m4a", ".aac", ".wma", ".opus", ".webm"} |
|
|
|
|
| class AudioTranscriptionService: |
| """基于 transformers ASR pipeline 的音频转录服务。""" |
|
|
| def __init__(self): |
| self._pipeline = None |
| self.model_name = os.getenv("ASR_MODEL", "openai/whisper-tiny") |
| self._load_error = None |
|
|
| def get_pipeline(self): |
| if self._pipeline is None: |
| if self._load_error: |
| raise RuntimeError(f"模型加载失败(已缓存错误): {self._load_error}") |
| logger.info(f"正在加载音频转录模型: {self.model_name}(首次加载约 10-30 秒)") |
| from transformers import pipeline |
|
|
| try: |
| self._pipeline = pipeline( |
| "automatic-speech-recognition", |
| model=self.model_name, |
| ) |
| logger.info("音频转录模型加载完成") |
| except Exception as e: |
| self._load_error = str(e) |
| logger.error(f"音频转录模型加载失败: {e}") |
| raise |
| return self._pipeline |
|
|
| def transcribe_file(self, audio_path: str, language: str | None = None) -> dict: |
| """转录本地音频文件。""" |
| try: |
| path = Path(audio_path) |
| if not path.exists(): |
| return { |
| "success": False, |
| "text": "", |
| "segments": [], |
| "error": f"音频文件不存在: {audio_path}", |
| } |
|
|
| |
| suffix = path.suffix.lower() |
| if suffix not in SUPPORTED_AUDIO_SUFFIXES: |
| logger.warning(f"音频格式 {suffix} 可能不被支持,继续尝试转录") |
|
|
| |
| file_size_mb = path.stat().st_size / (1024 * 1024) |
| if file_size_mb > 50: |
| logger.warning(f"音频文件较大 ({file_size_mb:.1f}MB),转录可能需要较长时间") |
|
|
| logger.info(f"开始转录: {path.name} ({file_size_mb:.1f}MB)") |
|
|
| options = {"return_timestamps": True} |
| if language: |
| options["generate_kwargs"] = {"language": language} |
|
|
| result = self.get_pipeline()(str(path), **options) |
| text = result.get("text", "") if isinstance(result, dict) else str(result) |
|
|
| logger.info(f"转录完成: {len(text)} 字符") |
|
|
| return { |
| "success": True, |
| "text": text.strip(), |
| "segments": result.get("chunks", []) if isinstance(result, dict) else [], |
| "error": None, |
| } |
| except Exception as e: |
| logger.error(f"音频转录失败: {e}", exc_info=True) |
| return { |
| "success": False, |
| "text": "", |
| "segments": [], |
| "error": f"转录失败: {str(e)}", |
| } |
|
|
| def transcribe_base64( |
| self, |
| audio_base64: str, |
| suffix: str = ".wav", |
| language: str | None = None, |
| ) -> dict: |
| """转录 Base64 音频。""" |
| try: |
| audio_bytes = base64.b64decode(audio_base64) |
| except Exception as e: |
| return { |
| "success": False, |
| "text": "", |
| "segments": [], |
| "error": f"Base64 解码失败: {e}", |
| } |
|
|
| |
| if suffix and not suffix.startswith("."): |
| suffix = f".{suffix}" |
|
|
| with tempfile.NamedTemporaryFile(delete=False, suffix=suffix) as tmp_file: |
| tmp_file.write(audio_bytes) |
| tmp_path = tmp_file.name |
|
|
| try: |
| return self.transcribe_file(tmp_path, language) |
| finally: |
| Path(tmp_path).unlink(missing_ok=True) |
|
|
|
|
| _transcription_service: AudioTranscriptionService | None = None |
|
|
|
|
| def get_transcription_service() -> AudioTranscriptionService: |
| global _transcription_service |
| if _transcription_service is None: |
| _transcription_service = AudioTranscriptionService() |
| return _transcription_service |
|
|