From ce13a7e3e6e4e25e24480c859f79cbbbf2e78ea4 Mon Sep 17 00:00:00 2001 From: Sakura-RanChen <1908198662@qq.com> Date: Wed, 22 Apr 2026 10:28:47 +0800 Subject: [PATCH 1/2] =?UTF-8?q?=E4=BD=BF=E7=94=A8=E5=8F=8C=E6=B5=81?= =?UTF-8?q?=E4=BC=98=E5=8C=96=E7=89=88=E6=8E=A5=E5=8F=A3=EF=BC=88=E9=80=9F?= =?UTF-8?q?=E5=BA=A6=E6=8F=90=E5=8D=87=EF=BC=89=EF=BC=8C=E5=A2=9E=E5=8A=A0?= =?UTF-8?q?=E8=B1=86=E5=8C=85=E6=B5=81=E5=BC=8F=E8=AF=AD=E9=9F=B3=E8=AF=86?= =?UTF-8?q?=E5=88=AB=E6=A8=A1=E5=9E=8B2.0=E6=A8=A1=E5=9E=8B=E9=80=89?= =?UTF-8?q?=E6=8B=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../resources/db/changelog/202604211700.sql | 45 +++++++++++++++++++ .../db/changelog/db.changelog-master.yaml | 7 +++ main/xiaozhi-server/config.yaml | 23 +++++++++- .../core/providers/asr/doubao_stream.py | 21 ++++++--- 4 files changed, 89 insertions(+), 7 deletions(-) create mode 100644 main/manager-api/src/main/resources/db/changelog/202604211700.sql diff --git a/main/manager-api/src/main/resources/db/changelog/202604211700.sql b/main/manager-api/src/main/resources/db/changelog/202604211700.sql new file mode 100644 index 00000000..1f5f0d37 --- /dev/null +++ b/main/manager-api/src/main/resources/db/changelog/202604211700.sql @@ -0,0 +1,45 @@ +-- 修复豆包语音合成模型2.0 provider_code 重复问题,添加 ASR 2.0 支持 + +-- ==================== 豆包语音合成模型2.0 ==================== +-- 删除 TTS 2.0 供应器(不再需要单独的供应器) +delete from `ai_model_provider` where id = 'SYSTEM_TTS_HSDSTTS_V2'; + +-- ==================== 豆包语音识别(流式) ==================== +-- 修正原有豆包语音识别(流式)供应器,移除cluster字段,添加resource_id字段 +UPDATE `ai_model_provider` SET `fields` = '[{"key":"appid","type":"string","label":"应用ID"},{"key":"access_token","type":"string","label":"访问令牌"},{"key":"boosting_table_name","type":"string","label":"热词文件名称"},{"key":"correct_table_name","type":"string","label":"替换词文件名称"},{"key":"output_dir","type":"string","label":"输出目录"},{"key":"end_window_size","type":"number","label":"静音判定时长(ms)"},{"key":"enable_multilingual","type":"boolean","label":"是否开启多语种识别模式"},{"key":"language","type":"string","label":"指定语言编码"},{"key":"resource_id","type":"string","label":"资源ID"}]' WHERE `id` = 'SYSTEM_ASR_DoubaoStreamASR'; + +-- 修正原有豆包语音识别(流式)配置,移除cluster字段,添加resource_id默认值 +UPDATE `ai_model_config` SET `config_json` = JSON_REMOVE(JSON_SET(`config_json`, '$.resource_id', 'volc.bigasr.sauc.duration'), '$.cluster') WHERE `id` = 'ASR_DoubaoStreamASR'; + +-- ==================== 豆包语音识别模型2.0 ==================== + +-- 插入豆包语音识别模型2.0配置 +delete from `ai_model_config` where id = 'ASR_DoubaoStreamASRV2'; +INSERT INTO `ai_model_config` VALUES ('ASR_DoubaoStreamASRV2', 'ASR', 'DoubaoStreamASRV2', '豆包语音识别模型2.0', 0, 1, '{ + "type": "doubao_stream", + "appid": "", + "access_token": "", + "resource_id": "volc.seedasr.sauc.duration", + "end_window_size": 200, + "enable_multilingual": false, + "language": "zh-CN", + "output_dir": "tmp/" +}', NULL, NULL, 6, NULL, NULL, NULL, NULL); + +-- 豆包语音识别模型2.0配置说明文档 +UPDATE `ai_model_config` SET +`doc_link` = 'https://www.volcengine.com/docs/6561/109979', +`remark` = '豆包语音识别模型2.0配置说明(基于火山引擎seed-asr): +1. 访问 https://www.volcengine.com/ 注册并开通火山引擎账号 +2. 访问 https://console.volcengine.com/speech/service/10038 开通豆包流式语音识别模型2.0 +3. 在页面底部获取appid和access_token +4. 资源ID有两种:小时版(volc.seedasr.sauc.duration)和并发版(volc.seedasr.sauc.concurrent) + - 小时版:固定为:volc.seedasr.sauc.duration(豆包语音识别模型2.0) + - 并发版:固定为:volc.seedasr.sauc.concurrent(豆包语音识别模型2.0) + +详细参数文档:https://www.volcengine.com/docs/6561/109979 + +注意: +- 豆包语音识别模型2.0使用volc.seedasr.sauc.duration资源ID,与豆包语音识别(流式)(volc.bigasr.sauc.duration)不同 +- 语音识别模型2.0价格更为便宜,建议在高并发场景下使用并发版资源ID +' WHERE `id` = 'ASR_DoubaoStreamASRV2'; diff --git a/main/manager-api/src/main/resources/db/changelog/db.changelog-master.yaml b/main/manager-api/src/main/resources/db/changelog/db.changelog-master.yaml index 9770b029..dff9e0ea 100755 --- a/main/manager-api/src/main/resources/db/changelog/db.changelog-master.yaml +++ b/main/manager-api/src/main/resources/db/changelog/db.changelog-master.yaml @@ -620,3 +620,10 @@ databaseChangeLog: - sqlFile: encoding: utf8 path: classpath:db/changelog/202604201719.sql + - changeSet: + id: 202604211700 + author: RanChen + changes: + - sqlFile: + encoding: utf8 + path: classpath:db/changelog/202604211700.sql diff --git a/main/xiaozhi-server/config.yaml b/main/xiaozhi-server/config.yaml index cc4df131..e82d15f0 100644 --- a/main/xiaozhi-server/config.yaml +++ b/main/xiaozhi-server/config.yaml @@ -396,7 +396,28 @@ ASR: type: doubao_stream appid: 你的火山引擎语音合成服务appid access_token: 你的火山引擎语音合成服务access_token - cluster: volcengine_input_common + # 资源ID:小时版 volc.bigasr.sauc.duration,并发版 volc.bigasr.sauc.concurrent + resource_id: volc.bigasr.sauc.duration + # 热词、替换词使用流程:https://www.volcengine.com/docs/6561/155738 + boosting_table_name: (选填)你的热词文件名称 + correct_table_name: (选填)你的替换词文件名称 + # 是否开启多语种识别模式 + enable_multilingual: False + # 多语种识别当该键为空时,该模型支持中英文、上海话、闽南语,四川、陕西、粤语识别。当将其设置为特定键时,它可以识别指定语言。 + # 详细语言列表参考 https://www.volcengine.com/docs/6561/1354869 + # language: zh-cn + # 静音判定时长(ms),默认200ms + end_window_size: 200 + output_dir: tmp/ + DoubaoStreamASRV2: + # 豆包语音识别模型2.0(基于火山引擎seed-asr) + # 开通地址:https://console.volcengine.com/speech/service/10038 + # 价格更为便宜,建议在高并发场景下使用 + type: doubao_stream + appid: 你的火山引擎语音合成服务appid + access_token: 你的火山引擎语音合成服务access_token + # 资源ID:小时版 volc.seedasr.sauc.duration,并发版 volc.seedasr.sauc.concurrent + resource_id: volc.seedasr.sauc.duration # 热词、替换词使用流程:https://www.volcengine.com/docs/6561/155738 boosting_table_name: (选填)你的热词文件名称 correct_table_name: (选填)你的替换词文件名称 diff --git a/main/xiaozhi-server/core/providers/asr/doubao_stream.py b/main/xiaozhi-server/core/providers/asr/doubao_stream.py index a1d520de..d9a36f4d 100644 --- a/main/xiaozhi-server/core/providers/asr/doubao_stream.py +++ b/main/xiaozhi-server/core/providers/asr/doubao_stream.py @@ -29,8 +29,10 @@ class ASRProvider(ASRProviderBase): # 配置参数 self.appid = str(config.get("appid")) - self.cluster = config.get("cluster") self.access_token = config.get("access_token") + # 资源ID,用于区分不同的ASR模型(默认1.0模型小时版,v2版本使用seed-asr) + self.resource_id = config.get("resource_id", "volc.bigasr.sauc.duration") + self.boosting_table_name = config.get("boosting_table_name", "") self.correct_table_name = config.get("correct_table_name", "") self.output_dir = config.get("output_dir", "tmp/") @@ -44,7 +46,7 @@ class ASRProvider(ASRProviderBase): if self.enable_multilingual: self.ws_url = "wss://openspeech.bytedance.com/api/v3/sauc/bigmodel_nostream" else: - self.ws_url = "wss://openspeech.bytedance.com/api/v3/sauc/bigmodel" + self.ws_url = "wss://openspeech.bytedance.com/api/v3/sauc/bigmodel_async" self.uid = config.get("uid", "streaming_asr_service") self.workflow = config.get( "workflow", "audio_in,resample,partition,vad,fe,decode,itn,nlu_punctuate" @@ -283,7 +285,6 @@ class ASRProvider(ASRProviderBase): req = { "app": { "appid": self.appid, - "cluster": self.cluster, "token": self.access_token, }, "user": {"uid": self.uid}, @@ -322,7 +323,7 @@ class ASRProvider(ASRProviderBase): return { "X-Api-App-Key": self.appid, "X-Api-Access-Key": self.access_token, - "X-Api-Resource-Id": "volc.bigasr.sauc.duration", + "X-Api-Resource-Id": self.resource_id, "X-Api-Connect-Id": str(uuid.uuid4()), } @@ -385,9 +386,17 @@ class ASRProvider(ASRProviderBase): "payload_msg": error_msg, } - # 获取JSON数据(跳过12字节头部) + # 获取JSON数据 try: - json_data = res[12:].decode("utf-8") + # 检查字节8-11是否为有效的JSON长度字段 + # 格式:4字节头 + 4字节序列号 + 4字节长度 + JSON数据 + length = int.from_bytes(res[8:12], "big") + if length > 0 and length <= len(res) - 12: + # 有长度字段,从字节12开始读取指定长度的JSON + json_data = res[12:12 + length].decode("utf-8") + else: + # 无长度字段或长度无效,尝试直接解析 + json_data = res[8:].decode("utf-8") result = json.loads(json_data) logger.bind(tag=TAG).debug(f"成功解析JSON响应: {result}") return {"payload_msg": result} From e9a2489e48010626987abf82d9832d393ff8035f Mon Sep 17 00:00:00 2001 From: FAN-yeB <1442100690@qq.com> Date: Wed, 22 Apr 2026 10:53:48 +0800 Subject: [PATCH 2/2] =?UTF-8?q?update:=E6=B5=8B=E8=AF=95=E5=B7=A5=E5=85=B7?= =?UTF-8?q?=E6=9B=B4=E6=96=B0=E8=B1=86=E5=8C=85=E6=B5=81=E5=BC=8F=E6=B5=8B?= =?UTF-8?q?=E8=AF=95=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../performance_tester_stream_asr.py | 107 ++---------------- 1 file changed, 8 insertions(+), 99 deletions(-) diff --git a/main/xiaozhi-server/performance_tester/performance_tester_stream_asr.py b/main/xiaozhi-server/performance_tester/performance_tester_stream_asr.py index f81f6317..eca5ffb5 100644 --- a/main/xiaozhi-server/performance_tester/performance_tester_stream_asr.py +++ b/main/xiaozhi-server/performance_tester/performance_tester_stream_asr.py @@ -65,69 +65,8 @@ class BaseASRTester: class DoubaoStreamASRTester(BaseASRTester): def __init__(self): super().__init__("DoubaoStreamASR") - - def _generate_header( - self, - version=0x01, - message_type=0x01, - message_type_specific_flags=0x00, - serial_method=0x01, - compression_type=0x01, - reserved_data=0x00, - extension_header: bytes = b"", - ): - """生成协议头(修复:使用正确的Header格式)""" - header = bytearray() - header_size = int(len(extension_header) / 4) + 1 - header.append((version << 4) | header_size) - header.append((message_type << 4) | message_type_specific_flags) - header.append((serial_method << 4) | compression_type) - header.append(reserved_data) - header.extend(extension_header) - return header - - def _generate_audio_default_header(self): - """生成音频数据Header""" - return self._generate_header( - version=0x01, - message_type=0x02, - message_type_specific_flags=0x00, # 普通音频帧 - serial_method=0x01, - compression_type=0x01, - ) - - def _generate_last_audio_header(self): - """生成最后一帧音频的Header(标记音频结束)""" - return self._generate_header( - version=0x01, - message_type=0x02, - message_type_specific_flags=0x02, # 0x02表示这是最后一帧 - serial_method=0x01, - compression_type=0x01, - ) - - def _parse_response(self, res: bytes) -> dict: - try: - if len(res) < 4: - return {"error": "响应数据长度不足"} - header = res[:4] - message_type = header[1] >> 4 - if message_type == 0x0F: - code = int.from_bytes(res[4:8], "big", signed=False) - msg_length = int.from_bytes(res[8:12], "big", signed=False) - error_msg = json.loads(res[12:].decode("utf-8")) - return { - "code": code, - "msg_length": msg_length, - "payload_msg": error_msg - } - try: - json_data = res[12:].decode("utf-8") - return {"payload_msg": json.loads(json_data)} - except (UnicodeDecodeError, json.JSONDecodeError): - return {"error": "JSON解析失败"} - except Exception: - return {"error": "解析响应失败"} + from core.providers.asr.doubao_stream import ASRProvider as DoubaoStreamProvider + self.provider = DoubaoStreamProvider(self.asr_config, delete_audio_file=False) async def test(self, test_count=5): if not self.test_audio_files: @@ -138,59 +77,29 @@ class DoubaoStreamASRTester(BaseASRTester): latencies = [] for i in range(test_count): try: - ws_url = "wss://openspeech.bytedance.com/api/v3/sauc/bigmodel" - appid = self.asr_config["appid"] - access_token = self.asr_config["access_token"] - cluster = self.asr_config.get("cluster", "volcengine_input_common") - uid = self.asr_config.get("uid", "streaming_asr_service") - start_time = time.time() - headers = { - "X-Api-App-Key": appid, - "X-Api-Access-Key": access_token, - "X-Api-Resource-Id": "volc.bigasr.sauc.duration", - "X-Api-Connect-Id": str(uuid.uuid4()) - } + headers = self.provider.token_auth() async with websockets.connect( - ws_url, + self.provider.ws_url, additional_headers=headers, max_size=1000000000, ping_interval=None, ping_timeout=None, close_timeout=10 ) as ws: - request_params = { - "app": {"appid": appid, "cluster": cluster, "token": access_token}, - "user": {"uid": uid}, - "request": { - "reqid": str(uuid.uuid4()), - "workflow": "audio_in,resample,partition,vad,fe,decode,itn,nlu_punctuate", - "show_utterances": True, - "result_type": "single", - "sequence": 1 - }, - "audio": { - "format": "pcm", - "codec": "pcm", - "rate": 16000, - "language": "zh-CN", - "bits": 16, - "channel": 1, - "sample_rate": 16000 - } - } + request_params = self.provider.construct_request(str(uuid.uuid4())) payload_bytes = str.encode(json.dumps(request_params)) payload_bytes = gzip.compress(payload_bytes) - full_client_request = self._generate_header() + full_client_request = bytearray(self.provider.generate_header()) full_client_request.extend((len(payload_bytes)).to_bytes(4, "big")) full_client_request.extend(payload_bytes) await ws.send(full_client_request) init_res = await ws.recv() - result = self._parse_response(init_res) + result = self.provider.parse_response(init_res) if "code" in result and result["code"] != 1000: raise Exception(f"初始化失败: {result.get('payload_msg', {}).get('error', '未知错误')}") @@ -200,7 +109,7 @@ class DoubaoStreamASRTester(BaseASRTester): # 发送音频数据(使用最后一帧标记,告诉服务端音频已结束) payload = gzip.compress(audio_data) - audio_request = bytearray(self._generate_last_audio_header()) # 修复:使用最后一帧Header + audio_request = bytearray(self.provider.generate_last_audio_default_header()) audio_request.extend(len(payload).to_bytes(4, "big")) audio_request.extend(payload) await ws.send(audio_request)