File size: 4,384 Bytes
ae8b600
 
 
 
 
 
 
 
e5e756a
 
 
ae8b600
 
 
 
 
 
 
e5e756a
ae8b600
 
 
e5e756a
 
 
ae8b600
 
e5e756a
 
 
 
 
 
 
 
 
 
ae8b600
 
 
 
 
 
 
 
 
 
 
 
 
 
e5e756a
 
 
 
 
 
 
 
 
 
 
 
3899a42
ae8b600
 
 
 
 
 
e5e756a
 
ae8b600
 
 
 
 
 
 
 
 
 
 
 
e5e756a
ae8b600
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e5e756a
 
 
 
ae8b600
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
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}",
            }

        # 确保 suffix 以点开头
        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