wangzhibo
2026-08-30 c8515170fd399b4d2dfaebb59128d4674e58915e
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
import ssl
import json
import asyncio
import websockets
 
from config.logger import setup_logging
from typing import Optional, Tuple, List
from core.providers.asr.base import ASRProviderBase
from core.providers.asr.utils import lang_tag_filter
from core.providers.asr.dto.dto import InterfaceType
 
TAG = __name__
logger = setup_logging()
 
 
class ASRProvider(ASRProviderBase):
    def __init__(self, config: dict, delete_audio_file: bool):
        """
        Initialize the ASRProvider with server configuration.
        :param config: Dictionary containing 'host', 'port', and 'is_ssl'.
        :param delete_audio_file: Boolean to indicate whether to delete audio files after processing.
        """
        super().__init__()
        self.interface_type = InterfaceType.NON_STREAM
        self.host = config.get("host", "localhost")
        self.port = config.get("port", 10095)
        self.api_key = config.get("api_key", "none")
        self.is_ssl = str(config.get("is_ssl", True)).lower() in (
            "true",
            "1",
            "yes",
        )
        self.output_dir = config.get("output_dir")
        self.delete_audio_file = delete_audio_file
        self.uri = (
            f"wss://{self.host}:{self.port}"
            if self.is_ssl
            else f"ws://{self.host}:{self.port}"
        )
        self.ssl_context = ssl.SSLContext() if self.is_ssl else None
        if self.ssl_context:
            self.ssl_context.check_hostname = False
            self.ssl_context.verify_mode = ssl.CERT_NONE
 
    async def _receive_responses(self, ws) -> None:
        """
        Asynchronous generator to receive messages from the WebSocket.
        Yields each message as it is received.
        """
        text = ""
        while True:
            try:
                response = await asyncio.wait_for(ws.recv(), timeout=5)
                response_data = json.loads(response)
                logger.bind(tag=TAG).debug(f"Received response: {response_data}")
                if response_data.get("is_final", True):
                    text += response_data.get("text", "")
                    break
                else:
                    text += response_data.get("text", "")
            except asyncio.TimeoutError:
                logger.bind(tag=TAG).error(
                    "Timeout while waiting for response from WebSocket."
                )
                break
            except websockets.exceptions.ConnectionClosed as e:
                logger.bind(tag=TAG).error(f"WebSocket connection closed: {e}")
                break
        return text
 
    async def _send_data(self, ws, pcm_data: bytes, session_id: str) -> tuple:
        """
        Internal method to handle WebSocket communication.
        Reuses the persistent WebSocket connection if available.
        :param pcm_data: PCM audio data to send.
        :param session_id: Unique session identifier.
        :return: Tuple containing recognized text and optional timestamp.
        """
 
        # Send initial configuration message
        config_message = json.dumps(
            {
                "mode": "offline",
                "chunk_size": [5, 10, 5],
                "chunk_interval": 10,
                "wav_name": session_id,
                "is_speaking": True,
                "itn": False,
            }
        )
        await ws.send(config_message)
        logger.bind(tag=TAG).debug(f"Sent configuration message: {config_message}")
 
        # Send PCM data
        await ws.send(pcm_data)
        logger.bind(tag=TAG).debug(f"Sent PCM data of length: {len(pcm_data)} bytes")
 
        # Indicate end of speech
        end_message = json.dumps({"is_speaking": False})
        await ws.send(end_message)
        logger.bind(tag=TAG).debug(f"Sent end message: {end_message}")
 
    async def speech_to_text(
        self, opus_data: List[bytes], session_id: str, artifacts=None
    ) -> Tuple[Optional[str], Optional[str]]:
        """
        Convert speech data to text using FunASR.
        :param opus_data: List of Opus-encoded audio data chunks.
        :param session_id: Unique session identifier.
        :return: Tuple containing recognized text and optional timestamp.
        """
        
        if artifacts is None:
            return "", None
        auth_header = {"Authorization": "Bearer; {}".format(self.api_key)}
        async with websockets.connect(
            self.uri,
            additional_headers=auth_header,
            subprotocols=["binary"],
            ping_interval=None,
            ssl=self.ssl_context,
        ) as ws:
            try:
                # Use asyncio to handle WebSocket communication
                send_task = asyncio.create_task(
                    self._send_data(ws, artifacts.pcm_bytes, session_id)
                )
                receive_task = asyncio.create_task(self._receive_responses(ws))
 
                # Gather tasks with error handling
                done, pending = await asyncio.wait(
                    [send_task, receive_task], return_when=asyncio.FIRST_EXCEPTION
                )
 
                # Cancel any pending tasks
                for task in pending:
                    task.cancel()
 
                # Check for exceptions in completed tasks
                for task in done:
                    if task.exception():
                        raise task.exception()
 
                # Get the result from the receive task
                result = receive_task.result()
                
                # match = re.match(r"<\|(.*?)\|><\|(.*?)\|><\|(.*?)\|>(.*)", result)
                # if match:
                #     result = match.group(4).strip()
 
                # Handle tags
                result = lang_tag_filter(result)
                return (
                    result,
                    artifacts.file_path,
                )  # Return the recognized text and timestamp (if any)
 
            except websockets.exceptions.ConnectionClosed as e:
                logger.bind(tag=TAG).error(f"WebSocket connection closed: {e}")
                return "", artifacts.file_path
            except Exception as e:
                logger.bind(tag=TAG).error(
                    f"Error during speech-to-text conversion: {e}", exc_info=True
                )
                return "", artifacts.file_path