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
|