Merge branch 'main' into update-tts-voice-data

This commit is contained in:
Sakura-RanChen
2026-03-02 09:19:46 +08:00
committed by GitHub
5 changed files with 149 additions and 33 deletions
@@ -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
@@ -481,9 +482,27 @@ class TTSProvider(TTSProviderBase):
return audio_data return audio_data
except Exception as e: except Exception as e:
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": {