File size: 7,043 Bytes
59bd45e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
"""ASR (Automatic Speech Recognition) service for Voice Text Processor.

This module implements the ASRService class for transcribing audio files
to text using the Zhipu AI GLM-ASR-2512 API.

Requirements: 2.1, 2.2, 2.3, 2.4, 9.2, 9.5
"""

import logging
from typing import Optional
import httpx


logger = logging.getLogger(__name__)


class ASRServiceError(Exception):
    """Exception raised when ASR service operations fail.
    
    This exception is raised when the Zhipu ASR API call fails,
    such as due to network issues, API errors, or invalid responses.
    
    Requirements: 2.3
    """
    
    def __init__(self, message: str = "语音识别服务不可用"):
        """Initialize ASRServiceError.
        
        Args:
            message: Error message describing the failure
        """
        super().__init__(message)
        self.message = message


class ASRService:
    """Service for transcribing audio files using Zhipu AI ASR API.
    
    This service handles audio file transcription by calling the Zhipu AI
    GLM-ASR-2512 API. It manages API authentication, request formatting,
    response parsing, and error handling.
    
    Attributes:
        api_key: Zhipu AI API key for authentication
        client: Async HTTP client for making API requests
        api_url: Zhipu AI ASR API endpoint URL
        model: ASR model identifier
    
    Requirements: 2.1, 2.2, 2.3, 2.4, 9.2, 9.5
    """
    
    def __init__(self, api_key: str):
        """Initialize the ASR service.
        
        Args:
            api_key: Zhipu AI API key for authentication
        """
        self.api_key = api_key
        self.client = httpx.AsyncClient(timeout=30.0)
        self.api_url = "https://api.z.ai/api/paas/v4/audio/transcriptions"
        self.model = "glm-asr-2512"
    
    async def close(self):
        """Close the HTTP client.
        
        This should be called when the service is no longer needed
        to properly clean up resources.
        """
        await self.client.aclose()
    
    async def transcribe(self, audio_file: bytes, filename: str = "audio.mp3") -> str:
        """Transcribe audio file to text using Zhipu ASR API.
        
        This method sends the audio file to the Zhipu AI ASR API and returns
        the transcribed text. It handles API errors, empty recognition results,
        and logs all errors with timestamps and stack traces.
        
        Args:
            audio_file: Audio file content as bytes
            filename: Name of the audio file (for API request)
        
        Returns:
            Transcribed text content. Returns empty string if audio cannot
            be recognized (empty recognition result).
        
        Raises:
            ASRServiceError: If API call fails or returns invalid response
        
        Requirements: 2.1, 2.2, 2.3, 2.4, 9.2, 9.5
        """
        try:
            # Prepare request headers
            headers = {
                "Authorization": f"Bearer {self.api_key}"
            }
            
            # Prepare multipart form data
            files = {
                "file": (filename, audio_file, "audio/mpeg")
            }
            
            data = {
                "model": self.model,
                "stream": "false"
            }
            
            logger.info(f"Calling Zhipu ASR API for file: {filename}")
            
            # Make API request
            response = await self.client.post(
                self.api_url,
                headers=headers,
                files=files,
                data=data
            )
            
            # Check response status
            if response.status_code != 200:
                error_msg = f"ASR API returned status {response.status_code}"
                try:
                    error_detail = response.json()
                    error_msg += f": {error_detail}"
                except Exception:
                    error_msg += f": {response.text}"
                
                logger.error(
                    f"ASR API call failed: {error_msg}",
                    exc_info=True,
                    extra={"timestamp": logger.makeRecord(
                        logger.name, logging.ERROR, "", 0, error_msg, (), None
                    ).created}
                )
                raise ASRServiceError(f"语音识别服务不可用: {error_msg}")
            
            # Parse response
            try:
                result = response.json()
            except Exception as e:
                error_msg = f"Failed to parse ASR API response: {str(e)}"
                logger.error(
                    error_msg,
                    exc_info=True,
                    extra={"timestamp": logger.makeRecord(
                        logger.name, logging.ERROR, "", 0, error_msg, (), None
                    ).created}
                )
                raise ASRServiceError(f"语音识别服务不可用: 响应格式无效")
            
            # Extract transcribed text
            text = result.get("text", "")
            
            # Handle empty recognition result
            if not text or text.strip() == "":
                logger.warning(
                    f"ASR returned empty text for file: {filename}. "
                    "Audio content may be unrecognizable."
                )
                return ""
            
            logger.info(
                f"ASR transcription successful for {filename}. "
                f"Text length: {len(text)} characters"
            )
            
            return text
            
        except ASRServiceError:
            # Re-raise ASRServiceError as-is
            raise
            
        except httpx.TimeoutException as e:
            error_msg = f"ASR API request timeout: {str(e)}"
            logger.error(
                error_msg,
                exc_info=True,
                extra={"timestamp": logger.makeRecord(
                    logger.name, logging.ERROR, "", 0, error_msg, (), None
                ).created}
            )
            raise ASRServiceError("语音识别服务不可用: 请求超时")
            
        except httpx.RequestError as e:
            error_msg = f"ASR API request failed: {str(e)}"
            logger.error(
                error_msg,
                exc_info=True,
                extra={"timestamp": logger.makeRecord(
                    logger.name, logging.ERROR, "", 0, error_msg, (), None
                ).created}
            )
            raise ASRServiceError(f"语音识别服务不可用: 网络错误")
            
        except Exception as e:
            error_msg = f"Unexpected error in ASR service: {str(e)}"
            logger.error(
                error_msg,
                exc_info=True,
                extra={"timestamp": logger.makeRecord(
                    logger.name, logging.ERROR, "", 0, error_msg, (), None
                ).created}
            )
            raise ASRServiceError(f"语音识别服务不可用: {str(e)}")