|
|
@@ -0,0 +1,144 @@
|
|
|
+package org.dromara.talk.service.impl;
|
|
|
+
|
|
|
+import com.alibaba.fastjson2.JSON;
|
|
|
+import com.alibaba.fastjson2.JSONObject;
|
|
|
+import lombok.extern.slf4j.Slf4j;
|
|
|
+import org.dromara.talk.service.ITtsService;
|
|
|
+import org.java_websocket.client.WebSocketClient;
|
|
|
+import org.java_websocket.handshake.ServerHandshake;
|
|
|
+import org.springframework.beans.factory.annotation.Value;
|
|
|
+import org.springframework.stereotype.Service;
|
|
|
+
|
|
|
+import javax.crypto.Mac;
|
|
|
+import javax.crypto.spec.SecretKeySpec;
|
|
|
+import java.net.URI;
|
|
|
+import java.net.URL;
|
|
|
+import java.net.URLEncoder;
|
|
|
+import java.nio.charset.StandardCharsets;
|
|
|
+import java.text.SimpleDateFormat;
|
|
|
+import java.util.*;
|
|
|
+
|
|
|
+@Slf4j
|
|
|
+@Service
|
|
|
+public class TtsServiceImpl implements ITtsService {
|
|
|
+
|
|
|
+ @Value("${xfyun.tts.appid:}")
|
|
|
+ private String appId;
|
|
|
+
|
|
|
+ @Value("${xfyun.tts.api-key:}")
|
|
|
+ private String apiKey;
|
|
|
+
|
|
|
+ @Value("${xfyun.tts.api-secret:}")
|
|
|
+ private String apiSecret;
|
|
|
+
|
|
|
+ private static final String HOST_URL = "https://tts-api.xfyun.cn/v2/tts";
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public void synthesize(String text, ITtsService.AudioCallback callback) {
|
|
|
+ try {
|
|
|
+ String wsUrl = getAuthUrl(HOST_URL, apiKey, apiSecret).replace("https://", "wss://");
|
|
|
+ URI uri = new URI(wsUrl);
|
|
|
+ String requestJson = buildRequest(text);
|
|
|
+
|
|
|
+ WebSocketClient client = new WebSocketClient(uri) {
|
|
|
+ @Override
|
|
|
+ public void onOpen(ServerHandshake handshake) {
|
|
|
+ log.info("TTS WebSocket连接成功");
|
|
|
+ send(requestJson);
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public void onMessage(String message) {
|
|
|
+ log.info("收到TTS响应: {}", message);
|
|
|
+ TtsResponse response = JSON.parseObject(message, TtsResponse.class);
|
|
|
+
|
|
|
+ if (response.code != 0) {
|
|
|
+ log.error("TTS返回错误: code={}, message={}", response.code, response.message);
|
|
|
+ callback.onError(response.code, response.message);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (response.data != null && response.data.audio != null) {
|
|
|
+ callback.onAudio(response.data.audio, response.data.status);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public void onClose(int code, String reason, boolean remote) {
|
|
|
+ log.info("TTS WebSocket连接关闭: code={}, reason={}, remote={}", code, reason, remote);
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public void onError(Exception e) {
|
|
|
+ log.error("TTS WebSocket错误", e);
|
|
|
+ callback.onError(-1, e.getMessage());
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ client.connect();
|
|
|
+
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("TTS合成失败", e);
|
|
|
+ callback.onError(-1, e.getMessage());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private String buildRequest(String text) {
|
|
|
+ return "{\n" +
|
|
|
+ " \"common\": {\"app_id\": \"" + appId + "\"},\n" +
|
|
|
+ " \"business\": {\n" +
|
|
|
+ " \"aue\": \"lame\",\n" +
|
|
|
+ " \"tte\": \"UTF8\",\n" +
|
|
|
+ " \"ent\": \"intp65\",\n" +
|
|
|
+ " \"vcn\": \"x4_yezi\",\n" +
|
|
|
+ " \"speed\": 50,\n" +
|
|
|
+ " \"pitch\": 50\n" +
|
|
|
+ " },\n" +
|
|
|
+ " \"data\": {\n" +
|
|
|
+ " \"status\": 2,\n" +
|
|
|
+ " \"text\": \"" + Base64.getEncoder()
|
|
|
+ .encodeToString(text.getBytes(StandardCharsets.UTF_8)) + "\"\n" +
|
|
|
+ " }\n" +
|
|
|
+ "}";
|
|
|
+ }
|
|
|
+
|
|
|
+ private String getAuthUrl(String hostUrl, String apiKey, String apiSecret) throws Exception {
|
|
|
+ URL url = new URL(hostUrl);
|
|
|
+ SimpleDateFormat format = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z", Locale.US);
|
|
|
+ format.setTimeZone(TimeZone.getTimeZone("GMT"));
|
|
|
+ String date = format.format(new Date());
|
|
|
+
|
|
|
+ String preStr = "host: " + url.getHost() + "\n" +
|
|
|
+ "date: " + date + "\n" +
|
|
|
+ "GET " + url.getPath() + " HTTP/1.1";
|
|
|
+
|
|
|
+ Mac mac = Mac.getInstance("hmacsha256");
|
|
|
+ SecretKeySpec spec = new SecretKeySpec(apiSecret.getBytes(StandardCharsets.UTF_8), "hmacsha256");
|
|
|
+ mac.init(spec);
|
|
|
+ byte[] hexDigits = mac.doFinal(preStr.getBytes(StandardCharsets.UTF_8));
|
|
|
+ String sha = Base64.getEncoder().encodeToString(hexDigits);
|
|
|
+
|
|
|
+ String authorization = String.format("api_key=\"%s\", algorithm=\"%s\", headers=\"%s\", signature=\"%s\"",
|
|
|
+ apiKey, "hmac-sha256", "host date request-line", sha);
|
|
|
+
|
|
|
+ String authBase64 = Base64.getEncoder().encodeToString(authorization.getBytes(StandardCharsets.UTF_8));
|
|
|
+
|
|
|
+ return "https://" + url.getHost() + url.getPath() +
|
|
|
+ "?authorization=" + authBase64 +
|
|
|
+ "&date=" + URLEncoder.encode(date, StandardCharsets.UTF_8) +
|
|
|
+ "&host=" + url.getHost();
|
|
|
+ }
|
|
|
+
|
|
|
+ static class TtsResponse {
|
|
|
+ public int code;
|
|
|
+ public String message;
|
|
|
+ public String sid;
|
|
|
+ public TtsData data;
|
|
|
+ }
|
|
|
+
|
|
|
+ static class TtsData {
|
|
|
+ public int status;
|
|
|
+ public String audio;
|
|
|
+ public String ced;
|
|
|
+ }
|
|
|
+}
|