mirror of
https://github.com/xinnan-tech/xiaozhi-esp32-server.git
synced 2026-07-27 17:43:55 +08:00
add:asr赋值audio_format
This commit is contained in:
@@ -28,7 +28,8 @@ async def handleHelloMessage(conn, msg_json):
|
|||||||
format = audio_params.get("format")
|
format = audio_params.get("format")
|
||||||
logger.bind(tag=TAG).info(f"客户端音频格式: {format}")
|
logger.bind(tag=TAG).info(f"客户端音频格式: {format}")
|
||||||
conn.audio_format = format
|
conn.audio_format = format
|
||||||
conn.welcome_msg['audio_params'] = audio_params
|
conn.asr.set_audio_format(format)
|
||||||
|
conn.welcome_msg["audio_params"] = audio_params
|
||||||
|
|
||||||
await conn.websocket.send(json.dumps(conn.welcome_msg))
|
await conn.websocket.send(json.dumps(conn.welcome_msg))
|
||||||
|
|
||||||
|
|||||||
@@ -20,62 +20,75 @@ from core.providers.asr.base import ASRProviderBase
|
|||||||
TAG = __name__
|
TAG = __name__
|
||||||
logger = setup_logging()
|
logger = setup_logging()
|
||||||
|
|
||||||
|
|
||||||
class AccessToken:
|
class AccessToken:
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _encode_text(text):
|
def _encode_text(text):
|
||||||
encoded_text = parse.quote_plus(text)
|
encoded_text = parse.quote_plus(text)
|
||||||
return encoded_text.replace('+', '%20').replace('*', '%2A').replace('%7E', '~')
|
return encoded_text.replace("+", "%20").replace("*", "%2A").replace("%7E", "~")
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _encode_dict(dic):
|
def _encode_dict(dic):
|
||||||
keys = dic.keys()
|
keys = dic.keys()
|
||||||
dic_sorted = [(key, dic[key]) for key in sorted(keys)]
|
dic_sorted = [(key, dic[key]) for key in sorted(keys)]
|
||||||
encoded_text = parse.urlencode(dic_sorted)
|
encoded_text = parse.urlencode(dic_sorted)
|
||||||
return encoded_text.replace('+', '%20').replace('*', '%2A').replace('%7E', '~')
|
return encoded_text.replace("+", "%20").replace("*", "%2A").replace("%7E", "~")
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def create_token(access_key_id, access_key_secret):
|
def create_token(access_key_id, access_key_secret):
|
||||||
parameters = {'AccessKeyId': access_key_id,
|
parameters = {
|
||||||
'Action': 'CreateToken',
|
"AccessKeyId": access_key_id,
|
||||||
'Format': 'JSON',
|
"Action": "CreateToken",
|
||||||
'RegionId': 'cn-shanghai',
|
"Format": "JSON",
|
||||||
'SignatureMethod': 'HMAC-SHA1',
|
"RegionId": "cn-shanghai",
|
||||||
'SignatureNonce': str(uuid.uuid1()),
|
"SignatureMethod": "HMAC-SHA1",
|
||||||
'SignatureVersion': '1.0',
|
"SignatureNonce": str(uuid.uuid1()),
|
||||||
'Timestamp': time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
|
"SignatureVersion": "1.0",
|
||||||
'Version': '2019-02-28'}
|
"Timestamp": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
|
||||||
|
"Version": "2019-02-28",
|
||||||
|
}
|
||||||
# 构造规范化的请求字符串
|
# 构造规范化的请求字符串
|
||||||
query_string = AccessToken._encode_dict(parameters)
|
query_string = AccessToken._encode_dict(parameters)
|
||||||
# print('规范化的请求字符串: %s' % query_string)
|
# print('规范化的请求字符串: %s' % query_string)
|
||||||
# 构造待签名字符串
|
# 构造待签名字符串
|
||||||
string_to_sign = 'GET' + '&' + AccessToken._encode_text('/') + '&' + AccessToken._encode_text(query_string)
|
string_to_sign = (
|
||||||
|
"GET"
|
||||||
|
+ "&"
|
||||||
|
+ AccessToken._encode_text("/")
|
||||||
|
+ "&"
|
||||||
|
+ AccessToken._encode_text(query_string)
|
||||||
|
)
|
||||||
# print('待签名的字符串: %s' % string_to_sign)
|
# print('待签名的字符串: %s' % string_to_sign)
|
||||||
# 计算签名
|
# 计算签名
|
||||||
secreted_string = hmac.new(bytes(access_key_secret + '&', encoding='utf-8'),
|
secreted_string = hmac.new(
|
||||||
bytes(string_to_sign, encoding='utf-8'),
|
bytes(access_key_secret + "&", encoding="utf-8"),
|
||||||
hashlib.sha1).digest()
|
bytes(string_to_sign, encoding="utf-8"),
|
||||||
|
hashlib.sha1,
|
||||||
|
).digest()
|
||||||
signature = base64.b64encode(secreted_string)
|
signature = base64.b64encode(secreted_string)
|
||||||
# print('签名: %s' % signature)
|
# print('签名: %s' % signature)
|
||||||
# 进行URL编码
|
# 进行URL编码
|
||||||
signature = AccessToken._encode_text(signature)
|
signature = AccessToken._encode_text(signature)
|
||||||
# print('URL编码后的签名: %s' % signature)
|
# print('URL编码后的签名: %s' % signature)
|
||||||
# 调用服务
|
# 调用服务
|
||||||
full_url = 'http://nls-meta.cn-shanghai.aliyuncs.com/?Signature=%s&%s' % (signature, query_string)
|
full_url = "http://nls-meta.cn-shanghai.aliyuncs.com/?Signature=%s&%s" % (
|
||||||
|
signature,
|
||||||
|
query_string,
|
||||||
|
)
|
||||||
# print('url: %s' % full_url)
|
# print('url: %s' % full_url)
|
||||||
# 提交HTTP GET请求
|
# 提交HTTP GET请求
|
||||||
response = requests.get(full_url)
|
response = requests.get(full_url)
|
||||||
if response.ok:
|
if response.ok:
|
||||||
root_obj = response.json()
|
root_obj = response.json()
|
||||||
key = 'Token'
|
key = "Token"
|
||||||
if key in root_obj:
|
if key in root_obj:
|
||||||
token = root_obj[key]['Id']
|
token = root_obj[key]["Id"]
|
||||||
expire_time = root_obj[key]['ExpireTime']
|
expire_time = root_obj[key]["ExpireTime"]
|
||||||
return token, expire_time
|
return token, expire_time
|
||||||
# print(response.text)
|
# print(response.text)
|
||||||
return None, None
|
return None, None
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class ASRProvider(ASRProviderBase):
|
class ASRProvider(ASRProviderBase):
|
||||||
def __init__(self, config: dict, delete_audio_file: bool):
|
def __init__(self, config: dict, delete_audio_file: bool):
|
||||||
"""阿里云ASR初始化"""
|
"""阿里云ASR初始化"""
|
||||||
@@ -102,28 +115,23 @@ class ASRProvider(ASRProviderBase):
|
|||||||
# 确保输出目录存在
|
# 确保输出目录存在
|
||||||
os.makedirs(self.output_dir, exist_ok=True)
|
os.makedirs(self.output_dir, exist_ok=True)
|
||||||
|
|
||||||
|
|
||||||
def _refresh_token(self):
|
def _refresh_token(self):
|
||||||
"""刷新Token并记录过期时间"""
|
"""刷新Token并记录过期时间"""
|
||||||
if self.access_key_id and self.access_key_secret:
|
if self.access_key_id and self.access_key_secret:
|
||||||
self.token, expire_time_str = AccessToken.create_token(
|
self.token, expire_time_str = AccessToken.create_token(
|
||||||
self.access_key_id,
|
self.access_key_id, self.access_key_secret
|
||||||
self.access_key_secret
|
|
||||||
)
|
)
|
||||||
if not expire_time_str:
|
if not expire_time_str:
|
||||||
raise ValueError("无法获取有效的Token过期时间")
|
raise ValueError("无法获取有效的Token过期时间")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
#统一转换为字符串处理
|
# 统一转换为字符串处理
|
||||||
expire_str = str(expire_time_str).strip()
|
expire_str = str(expire_time_str).strip()
|
||||||
|
|
||||||
if expire_str.isdigit():
|
if expire_str.isdigit():
|
||||||
expire_time = datetime.fromtimestamp(int(expire_str))
|
expire_time = datetime.fromtimestamp(int(expire_str))
|
||||||
else:
|
else:
|
||||||
expire_time = datetime.strptime(
|
expire_time = datetime.strptime(expire_str, "%Y-%m-%dT%H:%M:%SZ")
|
||||||
expire_str,
|
|
||||||
"%Y-%m-%dT%H:%M:%SZ"
|
|
||||||
)
|
|
||||||
self.expire_time = expire_time.timestamp() - 60
|
self.expire_time = expire_time.timestamp() - 60
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise ValueError(f"无效的过期时间格式: {expire_str}") from e
|
raise ValueError(f"无效的过期时间格式: {expire_str}") from e
|
||||||
@@ -145,9 +153,12 @@ class ASRProvider(ASRProviderBase):
|
|||||||
# f"过期时间 {datetime.fromtimestamp(self.expire_time)} | "
|
# f"过期时间 {datetime.fromtimestamp(self.expire_time)} | "
|
||||||
# f"剩余 {remaining:.2f}秒")
|
# f"剩余 {remaining:.2f}秒")
|
||||||
return time.time() > self.expire_time
|
return time.time() > self.expire_time
|
||||||
def generate_filename(self, extension=".wav"):
|
|
||||||
return os.path.join(self.output_file, f"tts-{__name__}{datetime.now().date()}@{uuid.uuid4().hex}{extension}")
|
|
||||||
|
|
||||||
|
def generate_filename(self, extension=".wav"):
|
||||||
|
return os.path.join(
|
||||||
|
self.output_file,
|
||||||
|
f"tts-{__name__}{datetime.now().date()}@{uuid.uuid4().hex}{extension}",
|
||||||
|
)
|
||||||
|
|
||||||
def _construct_request_url(self) -> str:
|
def _construct_request_url(self) -> str:
|
||||||
"""构造请求URL,包含参数"""
|
"""构造请求URL,包含参数"""
|
||||||
@@ -159,7 +170,6 @@ class ASRProvider(ASRProviderBase):
|
|||||||
request += "&enable_voice_detection=false"
|
request += "&enable_voice_detection=false"
|
||||||
return request
|
return request
|
||||||
|
|
||||||
|
|
||||||
def save_audio_to_file(self, pcm_data: List[bytes], session_id: str) -> str:
|
def save_audio_to_file(self, pcm_data: List[bytes], session_id: str) -> str:
|
||||||
"""PCM数据保存为WAV文件"""
|
"""PCM数据保存为WAV文件"""
|
||||||
module_name = __name__.split(".")[-1]
|
module_name = __name__.split(".")[-1]
|
||||||
@@ -170,7 +180,7 @@ class ASRProvider(ASRProviderBase):
|
|||||||
wf.setnchannels(1) # 单声道
|
wf.setnchannels(1) # 单声道
|
||||||
wf.setsampwidth(2) # 16-bit
|
wf.setsampwidth(2) # 16-bit
|
||||||
wf.setframerate(self.sample_rate)
|
wf.setframerate(self.sample_rate)
|
||||||
wf.writeframes(b''.join(pcm_data))
|
wf.writeframes(b"".join(pcm_data))
|
||||||
|
|
||||||
logger.bind(tag=TAG).debug(f"音频文件已保存至: {file_path}")
|
logger.bind(tag=TAG).debug(f"音频文件已保存至: {file_path}")
|
||||||
return file_path
|
return file_path
|
||||||
@@ -180,9 +190,9 @@ class ASRProvider(ASRProviderBase):
|
|||||||
try:
|
try:
|
||||||
# 设置HTTP头
|
# 设置HTTP头
|
||||||
headers = {
|
headers = {
|
||||||
'X-NLS-Token': self.token,
|
"X-NLS-Token": self.token,
|
||||||
'Content-type': 'application/octet-stream',
|
"Content-type": "application/octet-stream",
|
||||||
'Content-Length': str(len(pcm_data))
|
"Content-Length": str(len(pcm_data)),
|
||||||
}
|
}
|
||||||
|
|
||||||
# 创建连接并发送请求
|
# 创建连接并发送请求
|
||||||
@@ -190,12 +200,12 @@ class ASRProvider(ASRProviderBase):
|
|||||||
request_url = self._construct_request_url()
|
request_url = self._construct_request_url()
|
||||||
|
|
||||||
loop = asyncio.get_event_loop()
|
loop = asyncio.get_event_loop()
|
||||||
await loop.run_in_executor(None, lambda: conn.request(
|
await loop.run_in_executor(
|
||||||
method='POST',
|
None,
|
||||||
url=request_url,
|
lambda: conn.request(
|
||||||
body=pcm_data,
|
method="POST", url=request_url, body=pcm_data, headers=headers
|
||||||
headers=headers
|
),
|
||||||
))
|
)
|
||||||
|
|
||||||
# 获取响应
|
# 获取响应
|
||||||
response = await loop.run_in_executor(None, conn.getresponse)
|
response = await loop.run_in_executor(None, conn.getresponse)
|
||||||
@@ -205,10 +215,10 @@ class ASRProvider(ASRProviderBase):
|
|||||||
# 解析响应
|
# 解析响应
|
||||||
try:
|
try:
|
||||||
body_json = json.loads(body)
|
body_json = json.loads(body)
|
||||||
status = body_json.get('status')
|
status = body_json.get("status")
|
||||||
|
|
||||||
if status == 20000000:
|
if status == 20000000:
|
||||||
result = body_json.get('result', '')
|
result = body_json.get("result", "")
|
||||||
logger.bind(tag=TAG).debug(f"ASR结果: {result}")
|
logger.bind(tag=TAG).debug(f"ASR结果: {result}")
|
||||||
return result
|
return result
|
||||||
else:
|
else:
|
||||||
@@ -223,7 +233,9 @@ class ASRProvider(ASRProviderBase):
|
|||||||
logger.bind(tag=TAG).error(f"ASR请求失败: {e}", exc_info=True)
|
logger.bind(tag=TAG).error(f"ASR请求失败: {e}", exc_info=True)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
async def speech_to_text(self, opus_data: List[bytes], session_id: str) -> Tuple[Optional[str], Optional[str]]:
|
async def speech_to_text(
|
||||||
|
self, opus_data: List[bytes], session_id: str
|
||||||
|
) -> Tuple[Optional[str], Optional[str]]:
|
||||||
"""将语音数据转换为文本"""
|
"""将语音数据转换为文本"""
|
||||||
if self._is_token_expired():
|
if self._is_token_expired():
|
||||||
logger.warning("Token已过期,正在自动刷新...")
|
logger.warning("Token已过期,正在自动刷新...")
|
||||||
@@ -235,8 +247,8 @@ class ASRProvider(ASRProviderBase):
|
|||||||
if self.audio_format == "pcm":
|
if self.audio_format == "pcm":
|
||||||
pcm_data = opus_data
|
pcm_data = opus_data
|
||||||
else:
|
else:
|
||||||
pcm_data = self.decode_opus(opus_data, session_id)
|
pcm_data = self.decode_opus(opus_data)
|
||||||
combined_pcm_data = b''.join(pcm_data)
|
combined_pcm_data = b"".join(pcm_data)
|
||||||
|
|
||||||
# 判断是否保存为WAV文件
|
# 判断是否保存为WAV文件
|
||||||
if self.delete_audio_file:
|
if self.delete_audio_file:
|
||||||
|
|||||||
@@ -253,7 +253,7 @@ class ASRProvider(ASRProviderBase):
|
|||||||
if self.audio_format == "pcm":
|
if self.audio_format == "pcm":
|
||||||
pcm_data = opus_data
|
pcm_data = opus_data
|
||||||
else:
|
else:
|
||||||
pcm_data = self.decode_opus(opus_data, session_id)
|
pcm_data = self.decode_opus(opus_data)
|
||||||
combined_pcm_data = b"".join(pcm_data)
|
combined_pcm_data = b"".join(pcm_data)
|
||||||
|
|
||||||
# 判断是否保存为WAV文件
|
# 判断是否保存为WAV文件
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ class ASRProvider(ASRProviderBase):
|
|||||||
model=self.model_dir,
|
model=self.model_dir,
|
||||||
vad_kwargs={"max_single_segment_time": 30000},
|
vad_kwargs={"max_single_segment_time": 30000},
|
||||||
disable_update=True,
|
disable_update=True,
|
||||||
hub="hf"
|
hub="hf",
|
||||||
# device="cuda:0", # 启用GPU加速
|
# device="cuda:0", # 启用GPU加速
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -62,7 +62,9 @@ class ASRProvider(ASRProviderBase):
|
|||||||
|
|
||||||
return file_path
|
return file_path
|
||||||
|
|
||||||
async def speech_to_text(self, opus_data: List[bytes], session_id: str) -> Tuple[Optional[str], Optional[str]]:
|
async def speech_to_text(
|
||||||
|
self, opus_data: List[bytes], session_id: str
|
||||||
|
) -> Tuple[Optional[str], Optional[str]]:
|
||||||
"""语音转文本主处理逻辑"""
|
"""语音转文本主处理逻辑"""
|
||||||
file_path = None
|
file_path = None
|
||||||
try:
|
try:
|
||||||
@@ -70,7 +72,7 @@ class ASRProvider(ASRProviderBase):
|
|||||||
if self.audio_format == "pcm":
|
if self.audio_format == "pcm":
|
||||||
pcm_data = opus_data
|
pcm_data = opus_data
|
||||||
else:
|
else:
|
||||||
pcm_data = self.decode_opus(opus_data, session_id)
|
pcm_data = self.decode_opus(opus_data)
|
||||||
|
|
||||||
combined_pcm_data = b"".join(pcm_data)
|
combined_pcm_data = b"".join(pcm_data)
|
||||||
|
|
||||||
@@ -90,7 +92,9 @@ class ASRProvider(ASRProviderBase):
|
|||||||
batch_size_s=60,
|
batch_size_s=60,
|
||||||
)
|
)
|
||||||
text = rich_transcription_postprocess(result[0]["text"])
|
text = rich_transcription_postprocess(result[0]["text"])
|
||||||
logger.bind(tag=TAG).debug(f"语音识别耗时: {time.time() - start_time:.3f}s | 结果: {text}")
|
logger.bind(tag=TAG).debug(
|
||||||
|
f"语音识别耗时: {time.time() - start_time:.3f}s | 结果: {text}"
|
||||||
|
)
|
||||||
|
|
||||||
return text, file_path
|
return text, file_path
|
||||||
|
|
||||||
|
|||||||
@@ -132,7 +132,7 @@ class ASRProvider(ASRProviderBase):
|
|||||||
if self.audio_format == "pcm":
|
if self.audio_format == "pcm":
|
||||||
pcm_data = opus_data
|
pcm_data = opus_data
|
||||||
else:
|
else:
|
||||||
pcm_data = self.decode_opus(opus_data, session_id)
|
pcm_data = self.decode_opus(opus_data)
|
||||||
file_path = self.save_audio_to_file(pcm_data, session_id)
|
file_path = self.save_audio_to_file(pcm_data, session_id)
|
||||||
logger.bind(tag=TAG).debug(
|
logger.bind(tag=TAG).debug(
|
||||||
f"音频文件保存耗时: {time.time() - start_time:.3f}s | 路径: {file_path}"
|
f"音频文件保存耗时: {time.time() - start_time:.3f}s | 路径: {file_path}"
|
||||||
|
|||||||
Reference in New Issue
Block a user