mirror of
https://github.com/xinnan-tech/xiaozhi-esp32-server.git
synced 2026-07-24 16:13:54 +08:00
Merge branch 'main' into update-tts-voice-data
This commit is contained in:
+75
-30
@@ -4,6 +4,7 @@ import java.nio.charset.StandardCharsets;
|
|||||||
import java.security.InvalidKeyException;
|
import java.security.InvalidKeyException;
|
||||||
import java.security.NoSuchAlgorithmException;
|
import java.security.NoSuchAlgorithmException;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.Base64;
|
import java.util.Base64;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
@@ -38,6 +39,8 @@ import cn.hutool.crypto.digest.DigestUtil;
|
|||||||
import cn.hutool.http.ContentType;
|
import cn.hutool.http.ContentType;
|
||||||
import cn.hutool.http.Header;
|
import cn.hutool.http.Header;
|
||||||
import cn.hutool.http.HttpRequest;
|
import cn.hutool.http.HttpRequest;
|
||||||
|
import cn.hutool.json.JSONArray;
|
||||||
|
import cn.hutool.json.JSONObject;
|
||||||
import cn.hutool.json.JSONUtil;
|
import cn.hutool.json.JSONUtil;
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
@@ -700,40 +703,82 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
|
|||||||
// 构建完整的URL
|
// 构建完整的URL
|
||||||
String url = StrUtil.format("http://{}/api/commands/{}", mqttGatewayUrl, clientId);
|
String url = StrUtil.format("http://{}/api/commands/{}", mqttGatewayUrl, clientId);
|
||||||
|
|
||||||
// 构建请求体
|
// 存储所有工具列表
|
||||||
Map<String, Object> payload = MapUtil
|
List<Object> allTools = new ArrayList<>();
|
||||||
.builder(new HashMap<String, Object>())
|
String cursor = null;
|
||||||
.put("jsonrpc", "2.0")
|
|
||||||
.put("id", 2)
|
|
||||||
.put("method", "tools/list")
|
|
||||||
.put("params", MapUtil.builder(new HashMap<String, Object>())
|
|
||||||
.put("withUserTools", true)
|
|
||||||
.build())
|
|
||||||
.build();
|
|
||||||
|
|
||||||
Map<String, Object> requestBody = MapUtil
|
// 循环获取分页数据
|
||||||
.builder(new HashMap<String, Object>())
|
while (true) {
|
||||||
.put("type", "mcp")
|
// 构建params
|
||||||
.put("payload", payload)
|
Map<String, Object> paramsMap = MapUtil.builder(new HashMap<String, Object>())
|
||||||
.build();
|
.put("withUserTools", true)
|
||||||
|
.build();
|
||||||
// 发送请求
|
// 如果有cursor,添加到请求参数中
|
||||||
String resultMessage = HttpRequest.post(url)
|
if (StringUtils.isNotBlank(cursor)) {
|
||||||
.header(Header.CONTENT_TYPE, ContentType.JSON.getValue())
|
paramsMap.put("cursor", cursor);
|
||||||
.header(Header.AUTHORIZATION, "Bearer " + generateBearerToken())
|
|
||||||
.body(JSONUtil.toJsonStr(requestBody))
|
|
||||||
.timeout(10000) // 超时,毫秒
|
|
||||||
.execute().body();
|
|
||||||
|
|
||||||
// 解析响应
|
|
||||||
if (StringUtils.isNotBlank(resultMessage)) {
|
|
||||||
cn.hutool.json.JSONObject jsonObject = JSONUtil.parseObj(resultMessage);
|
|
||||||
if (jsonObject.getBool("success", false)) {
|
|
||||||
return jsonObject.get("data");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 构建请求体
|
||||||
|
Map<String, Object> payload = MapUtil
|
||||||
|
.builder(new HashMap<String, Object>())
|
||||||
|
.put("jsonrpc", "2.0")
|
||||||
|
.put("id", 2)
|
||||||
|
.put("method", "tools/list")
|
||||||
|
.put("params", paramsMap)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
Map<String, Object> requestBody = MapUtil
|
||||||
|
.builder(new HashMap<String, Object>())
|
||||||
|
.put("type", "mcp")
|
||||||
|
.put("payload", payload)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// 发送请求
|
||||||
|
String resultMessage = HttpRequest.post(url)
|
||||||
|
.header(Header.CONTENT_TYPE, ContentType.JSON.getValue())
|
||||||
|
.header(Header.AUTHORIZATION, "Bearer " + generateBearerToken())
|
||||||
|
.body(JSONUtil.toJsonStr(requestBody))
|
||||||
|
.timeout(10000) // 超时,毫秒
|
||||||
|
.execute().body();
|
||||||
|
|
||||||
|
// 解析响应
|
||||||
|
if (StringUtils.isBlank(resultMessage)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
JSONObject jsonObject = JSONUtil.parseObj(resultMessage);
|
||||||
|
if (!jsonObject.getBool("success", false)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
JSONObject data = jsonObject.getJSONObject("data");
|
||||||
|
if (data == null) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取当前页的工具列表
|
||||||
|
JSONArray tools = data.getJSONArray("tools");
|
||||||
|
if (tools != null && !tools.isEmpty()) {
|
||||||
|
allTools.addAll(tools);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取下一页的cursor
|
||||||
|
String nextCursor = data.getStr("nextCursor");
|
||||||
|
if (StringUtils.isBlank(nextCursor)) {
|
||||||
|
// 没有下一页了
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
cursor = nextCursor;
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
// 构建返回结果
|
||||||
|
if (allTools.isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, Object> resultData = new HashMap<>();
|
||||||
|
resultData.put("tools", allTools);
|
||||||
|
return resultData;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import traceback
|
|||||||
import websockets
|
import websockets
|
||||||
|
|
||||||
from asyncio import Task
|
from asyncio import Task
|
||||||
|
from typing import Callable, Any
|
||||||
from config.logger import setup_logging
|
from config.logger import setup_logging
|
||||||
from core.utils.tts import MarkdownCleaner
|
from core.utils.tts import MarkdownCleaner
|
||||||
from core.providers.tts.base import TTSProviderBase
|
from core.providers.tts.base import TTSProviderBase
|
||||||
@@ -392,6 +393,24 @@ class TTSProvider(TTSProviderBase):
|
|||||||
finally:
|
finally:
|
||||||
self._monitor_task = None
|
self._monitor_task = None
|
||||||
|
|
||||||
|
def audio_to_opus_data_stream(
|
||||||
|
self, audio_file_path, callback: Callable[[Any], Any] = None
|
||||||
|
):
|
||||||
|
"""重写父类方法:使用独立的临时编码器处理音频文件,避免与TTS流式编码器并发冲突。
|
||||||
|
双流式TTS中,monitor任务在event loop线程接收TTS音频并使用self.opus_encoder编码,
|
||||||
|
同时tts_text_priority_thread处理音乐文件也使用self.opus_encoder,
|
||||||
|
共享的encoder.buffer非线程安全,并发访问会导致SILK resampler断言失败。
|
||||||
|
"""
|
||||||
|
from core.utils.util import audio_to_data_stream
|
||||||
|
|
||||||
|
return audio_to_data_stream(
|
||||||
|
audio_file_path,
|
||||||
|
is_opus=True,
|
||||||
|
callback=callback,
|
||||||
|
sample_rate=self.conn.sample_rate,
|
||||||
|
opus_encoder=None,
|
||||||
|
)
|
||||||
|
|
||||||
def to_tts(self, text: str) -> list:
|
def to_tts(self, text: str) -> list:
|
||||||
"""非流式生成音频数据,用于生成音频及测试场景"""
|
"""非流式生成音频数据,用于生成音频及测试场景"""
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import websockets
|
|||||||
from asyncio import Task
|
from asyncio import Task
|
||||||
from urllib import parse
|
from urllib import parse
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
from typing import Callable, Any
|
||||||
from config.logger import setup_logging
|
from config.logger import setup_logging
|
||||||
from core.utils.tts import MarkdownCleaner
|
from core.utils.tts import MarkdownCleaner
|
||||||
from core.providers.tts.base import TTSProviderBase
|
from core.providers.tts.base import TTSProviderBase
|
||||||
@@ -471,6 +472,24 @@ class TTSProvider(TTSProviderBase):
|
|||||||
finally:
|
finally:
|
||||||
self._monitor_task = None
|
self._monitor_task = None
|
||||||
|
|
||||||
|
def audio_to_opus_data_stream(
|
||||||
|
self, audio_file_path, callback: Callable[[Any], Any] = None
|
||||||
|
):
|
||||||
|
"""重写父类方法:使用独立的临时编码器处理音频文件,避免与TTS流式编码器并发冲突。
|
||||||
|
双流式TTS中,monitor任务在event loop线程接收TTS音频并使用self.opus_encoder编码,
|
||||||
|
同时tts_text_priority_thread处理音乐文件也使用self.opus_encoder,
|
||||||
|
共享的encoder.buffer非线程安全,并发访问会导致SILK resampler断言失败。
|
||||||
|
"""
|
||||||
|
from core.utils.util import audio_to_data_stream
|
||||||
|
|
||||||
|
return audio_to_data_stream(
|
||||||
|
audio_file_path,
|
||||||
|
is_opus=True,
|
||||||
|
callback=callback,
|
||||||
|
sample_rate=self.conn.sample_rate,
|
||||||
|
opus_encoder=None,
|
||||||
|
)
|
||||||
|
|
||||||
def to_tts(self, text: str) -> list:
|
def to_tts(self, text: str) -> list:
|
||||||
"""非流式TTS处理,用于测试及保存音频文件的场景"""
|
"""非流式TTS处理,用于测试及保存音频文件的场景"""
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -698,6 +698,20 @@ class TTSProvider(TTSProviderBase):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def audio_to_opus_data_stream(
|
||||||
|
self, audio_file_path, callback: Callable[[Any], Any] = None
|
||||||
|
):
|
||||||
|
"""重写父类方法:使用独立的临时编码器处理音频文件,避免与TTS流式编码器并发冲突。
|
||||||
|
双流式TTS中,monitor任务在event loop线程接收TTS音频并使用self.opus_encoder编码,
|
||||||
|
同时tts_text_priority_thread处理音乐文件也使用self.opus_encoder,
|
||||||
|
共享的encoder.buffer非线程安全,并发访问会导致SILK resampler断言失败。
|
||||||
|
"""
|
||||||
|
from core.utils.util import audio_to_data_stream
|
||||||
|
return audio_to_data_stream(
|
||||||
|
audio_file_path, is_opus=True, callback=callback,
|
||||||
|
sample_rate=self.conn.sample_rate, opus_encoder=None
|
||||||
|
)
|
||||||
|
|
||||||
def wav_to_opus_data_audio_raw_stream(self, raw_data_var, is_end=False, callback: Callable[[Any], Any]=None):
|
def wav_to_opus_data_audio_raw_stream(self, raw_data_var, is_end=False, callback: Callable[[Any], Any]=None):
|
||||||
return self.opus_encoder.encode_pcm_to_opus_stream(raw_data_var, is_end, callback=callback)
|
return self.opus_encoder.encode_pcm_to_opus_stream(raw_data_var, is_end, callback=callback)
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import traceback
|
|||||||
import websockets
|
import websockets
|
||||||
|
|
||||||
from asyncio import Task
|
from asyncio import Task
|
||||||
|
from typing import Callable, Any
|
||||||
from config.logger import setup_logging
|
from config.logger import setup_logging
|
||||||
from core.utils.tts import MarkdownCleaner
|
from core.utils.tts import MarkdownCleaner
|
||||||
from urllib.parse import urlencode, urlparse
|
from urllib.parse import urlencode, urlparse
|
||||||
@@ -483,7 +484,25 @@ class TTSProvider(TTSProviderBase):
|
|||||||
logger.bind(tag=TAG).error(f"生成音频数据失败: {str(e)}")
|
logger.bind(tag=TAG).error(f"生成音频数据失败: {str(e)}")
|
||||||
return []
|
return []
|
||||||
|
|
||||||
def _build_base_request(self, status,text=" "):
|
def audio_to_opus_data_stream(
|
||||||
|
self, audio_file_path, callback: Callable[[Any], Any] = None
|
||||||
|
):
|
||||||
|
"""重写父类方法:使用独立的临时编码器处理音频文件,避免与TTS流式编码器并发冲突。
|
||||||
|
双流式TTS中,monitor任务在event loop线程接收TTS音频并使用self.opus_encoder编码,
|
||||||
|
同时tts_text_priority_thread处理音乐文件也使用self.opus_encoder,
|
||||||
|
共享的encoder.buffer非线程安全,并发访问会导致SILK resampler断言失败。
|
||||||
|
"""
|
||||||
|
from core.utils.util import audio_to_data_stream
|
||||||
|
|
||||||
|
return audio_to_data_stream(
|
||||||
|
audio_file_path,
|
||||||
|
is_opus=True,
|
||||||
|
callback=callback,
|
||||||
|
sample_rate=self.conn.sample_rate,
|
||||||
|
opus_encoder=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _build_base_request(self, status, text=" "):
|
||||||
"""构建基础请求结构"""
|
"""构建基础请求结构"""
|
||||||
return {
|
return {
|
||||||
"header": {
|
"header": {
|
||||||
|
|||||||
Reference in New Issue
Block a user