├── .gitignore
├── pom.xml
├── readme.md
├── src
├── main
│ ├── java
│ │ └── com
│ │ │ └── taoing
│ │ │ └── ttsserver
│ │ │ ├── TtsServerApplication.java
│ │ │ ├── controller
│ │ │ └── TTSController.java
│ │ │ ├── service
│ │ │ └── TTSService.java
│ │ │ ├── utils
│ │ │ └── Tool.java
│ │ │ ├── vo
│ │ │ ├── GeneralResponse.java
│ │ │ └── TextTTSReq.java
│ │ │ └── ws
│ │ │ ├── TTSConfig.java
│ │ │ ├── TTSWebSocket.java
│ │ │ ├── TTSWebSocketListener.java
│ │ │ └── WebSocketManager.java
│ └── resources
│ │ ├── application-dev.yml
│ │ ├── application-pro.yml
│ │ ├── application-test.yml
│ │ ├── application.yml
│ │ └── logback-spring.xml
└── test
│ └── java
│ └── com
│ └── taoing
│ └── ttsserver
│ └── TtsServerApplicationTests.java
└── startup.sh
/.gitignore:
--------------------------------------------------------------------------------
1 | target/
2 |
3 | ### IntelliJ IDEA ###
4 | .idea
5 | *.iws
6 | *.iml
7 | *.ipr
8 |
9 | logs
10 |
--------------------------------------------------------------------------------
/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 | 4.0.0
5 |
6 | org.springframework.boot
7 | spring-boot-starter-parent
8 | 2.5.4
9 |
10 |
11 | com.taoing
12 | tts-server
13 | 1.0.0
14 | tts-server
15 | microsoft edge tts(xiaoxiao)
16 |
17 | 1.8
18 |
19 |
20 |
21 | org.springframework.boot
22 | spring-boot-starter-web
23 |
24 |
25 |
26 | com.squareup.okhttp3
27 | okhttp
28 | 4.9.1
29 |
30 |
31 |
32 | org.projectlombok
33 | lombok
34 | true
35 |
36 |
37 |
38 | org.springframework.boot
39 | spring-boot-configuration-processor
40 | true
41 |
42 |
43 |
44 | org.springframework.boot
45 | spring-boot-starter-test
46 | test
47 |
48 |
49 |
50 |
51 |
52 |
53 | org.springframework.boot
54 | spring-boot-maven-plugin
55 |
56 |
57 |
58 |
59 |
60 |
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 | # TTS Server
2 |
3 | 利用微软edge的朗读功能, 文本合成语音. 可配合爱阅书香app使用, 实现"微软晓晓"语音在线听书...
4 |
5 | 了解更多微软tts:
6 | https://azure.microsoft.com/zh-cn/services/cognitive-services/text-to-speech
7 |
--------------------------------------------------------------------------------
/src/main/java/com/taoing/ttsserver/TtsServerApplication.java:
--------------------------------------------------------------------------------
1 | package com.taoing.ttsserver;
2 |
3 | import org.springframework.boot.SpringApplication;
4 | import org.springframework.boot.autoconfigure.SpringBootApplication;
5 |
6 | @SpringBootApplication
7 | public class TtsServerApplication {
8 |
9 | public static void main(String[] args) {
10 | SpringApplication.run(TtsServerApplication.class, args);
11 | }
12 |
13 | }
14 |
--------------------------------------------------------------------------------
/src/main/java/com/taoing/ttsserver/controller/TTSController.java:
--------------------------------------------------------------------------------
1 | package com.taoing.ttsserver.controller;
2 |
3 | import com.taoing.ttsserver.service.TTSService;
4 | import com.taoing.ttsserver.vo.GeneralResponse;
5 | import com.taoing.ttsserver.vo.TextTTSReq;
6 | import lombok.extern.slf4j.Slf4j;
7 | import org.springframework.beans.factory.annotation.Autowired;
8 | import org.springframework.util.StringUtils;
9 | import org.springframework.web.bind.annotation.GetMapping;
10 | import org.springframework.web.bind.annotation.PostMapping;
11 | import org.springframework.web.bind.annotation.RequestMapping;
12 | import org.springframework.web.bind.annotation.RestController;
13 |
14 | import javax.servlet.http.HttpServletRequest;
15 | import javax.servlet.http.HttpServletResponse;
16 |
17 | @Slf4j
18 | @RequestMapping(value = "/tests")
19 | @RestController
20 | public class TTSController {
21 |
22 | @Autowired
23 | private TTSService service;
24 |
25 | /**
26 | * 合成文本语音
27 | * 接口不支持多线程, 因此, 一个key只支持一个用户调用
28 | * 可用key: "xxttsa01", "xxttsa02", "xxttsa03"
29 | * @param response
30 | * @param param
31 | */
32 | @PostMapping(value = "/text2Speech")
33 | public void text2Speech(HttpServletResponse response, TextTTSReq param) {
34 | if (log.isDebugEnabled()) {
35 | log.debug("req: {}", param.toString());
36 | }
37 | if (!StringUtils.hasLength(param.getKey())) {
38 | response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
39 | return;
40 | }
41 | if (!StringUtils.hasLength(param.getText())) {
42 | return;
43 | }
44 | this.service.synthesisAudio(response, param);
45 | }
46 |
47 | @GetMapping(value = "/test")
48 | public GeneralResponse apiTest(HttpServletRequest request) {
49 | log.info("request uri: {}", request.getRequestURI() + "?" + request.getQueryString());
50 | return new GeneralResponse<>();
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/src/main/java/com/taoing/ttsserver/service/TTSService.java:
--------------------------------------------------------------------------------
1 | package com.taoing.ttsserver.service;
2 |
3 | import com.taoing.ttsserver.vo.TextTTSReq;
4 | import com.taoing.ttsserver.ws.TTSWebSocket;
5 | import com.taoing.ttsserver.ws.WebSocketManager;
6 | import lombok.extern.slf4j.Slf4j;
7 | import org.springframework.beans.factory.annotation.Autowired;
8 | import org.springframework.stereotype.Service;
9 |
10 | import javax.servlet.http.HttpServletResponse;
11 |
12 | @Slf4j
13 | @Service
14 | public class TTSService {
15 |
16 | @Autowired
17 | private WebSocketManager webSocketManager;
18 |
19 | /**
20 | * 合成文本语音
21 | * @param response
22 | * @param param
23 | */
24 | public void synthesisAudio(HttpServletResponse response, TextTTSReq param) {
25 | TTSWebSocket client = webSocketManager.getClient(param.getKey());
26 | if (client == null) {
27 | log.warn("请求key非法");
28 | response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
29 | return;
30 | }
31 | client.send(param.getText());
32 | client.writeAudioStream(response);
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/main/java/com/taoing/ttsserver/utils/Tool.java:
--------------------------------------------------------------------------------
1 | package com.taoing.ttsserver.utils;
2 |
3 | public class Tool {
4 |
5 | /**
6 | * 移除字符串首尾空字符的高效方法(利用ASCII值判断,包括全角空格)
7 | * @param s
8 | * @return
9 | */
10 | public static String fixTrim(String s) {
11 | if (s == null || s.isEmpty()) {
12 | return "";
13 | }
14 | int start = 0;
15 | int len = s.length();
16 | int end = len - 1;
17 | while (start < end && (s.charAt(start) <= 0x20 || s.charAt(start) == ' ')) {
18 | ++start;
19 | }
20 | while (start < end && (s.charAt(end) <= 0x20 || s.charAt(end) == ' ')) {
21 | --end;
22 | }
23 | ++end;
24 | return (start > 0 || end < len) ? s.substring(start, end) : s;
25 |
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/main/java/com/taoing/ttsserver/vo/GeneralResponse.java:
--------------------------------------------------------------------------------
1 | package com.taoing.ttsserver.vo;
2 |
3 | import lombok.Data;
4 |
5 | @Data
6 | public class GeneralResponse {
7 | private boolean success;
8 |
9 | private String code;
10 |
11 | private String message;
12 |
13 | private String errorMessage;
14 |
15 | private T data;
16 |
17 |
18 | public GeneralResponse() {
19 | this.success = true;
20 | this.code = "1";
21 | this.message = "请求成功";
22 | }
23 |
24 | public GeneralResponse error(String code, String errorMessage) {
25 | this.success = false;
26 | this.code = code;
27 | this.message = "请求失败";
28 | this.errorMessage = errorMessage;
29 | return this;
30 | }
31 |
32 | }
33 |
--------------------------------------------------------------------------------
/src/main/java/com/taoing/ttsserver/vo/TextTTSReq.java:
--------------------------------------------------------------------------------
1 | package com.taoing.ttsserver.vo;
2 |
3 | import lombok.Data;
4 |
5 | @Data
6 | public class TextTTSReq {
7 |
8 | /**
9 | * 文本
10 | */
11 | private String text;
12 |
13 | /**
14 | * tts服务key
15 | */
16 | private String key;
17 | }
18 |
--------------------------------------------------------------------------------
/src/main/java/com/taoing/ttsserver/ws/TTSConfig.java:
--------------------------------------------------------------------------------
1 | package com.taoing.ttsserver.ws;
2 |
3 | import lombok.Getter;
4 | import lombok.Setter;
5 | import org.springframework.boot.context.properties.ConfigurationProperties;
6 | import org.springframework.context.annotation.Configuration;
7 |
8 | import java.text.SimpleDateFormat;
9 | import java.util.Date;
10 | import java.util.Locale;
11 |
12 | @Getter
13 | @Setter
14 | @ConfigurationProperties(prefix = "tts.edge")
15 | @Configuration
16 | public class TTSConfig {
17 |
18 | /**
19 | * 微软可用声音tts列表查询url
20 | */
21 | private String voiceListUrl;
22 |
23 | /**
24 | * 语音合成websocket服务
25 | */
26 | private String wssUrl;
27 |
28 | /**
29 | * token
30 | */
31 | private String trustedClientToken;
32 |
33 | /**
34 | * 默认使用的声音tts(微软晓晓)
35 | */
36 | private String voiceName;
37 |
38 | /**
39 | * 默认使用的声音tts(微软晓晓简写)
40 | */
41 | private String voiceShortName;
42 |
43 | /**
44 | * 声音编码
45 | */
46 | private String codec;
47 |
48 | /**
49 | * 区域
50 | */
51 | private String local;
52 |
53 | private boolean sentenceBoundaryEnabled;
54 |
55 | private boolean wordBoundaryEnabled;
56 |
57 | /**
58 | * 语音合成配置
59 | * @return
60 | */
61 | public String getSynthesizeConfig() {
62 | String prefix = "X-Timestamp:+" + getTime() + "\r\n" +
63 | "Content-Type:application/json; charset=utf-8\r\n" +
64 | "Path:speech.config\r\n\r\n";
65 | String config = "{\"context\":{\"synthesis\":{\"audio\":{\"metadataoptions\":{\"sentenceBoundaryEnabled\":\"%s\",\"wordBoundaryEnabled\":\"%s\"},\"outputFormat\":\"%s\"}}}}\r\n";
66 | config = String.format(config, sentenceBoundaryEnabled ? "true" : "false",
67 | wordBoundaryEnabled ? "true" : "false", codec);
68 | return prefix + config;
69 | }
70 |
71 | public String getTime() {
72 | SimpleDateFormat sdf = new SimpleDateFormat("EEE MMM dd yyyy HH:mm:ss 'GMT'Z (中国标准时间)", Locale.ENGLISH);
73 | Date date = new Date();
74 | return sdf.format(date);
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/src/main/java/com/taoing/ttsserver/ws/TTSWebSocket.java:
--------------------------------------------------------------------------------
1 | package com.taoing.ttsserver.ws;
2 |
3 | import com.taoing.ttsserver.utils.Tool;
4 | import lombok.Getter;
5 | import lombok.Setter;
6 | import lombok.extern.slf4j.Slf4j;
7 | import okhttp3.OkHttpClient;
8 | import okhttp3.Request;
9 | import okhttp3.WebSocket;
10 | import okio.Buffer;
11 | import okio.ByteString;
12 |
13 | import javax.servlet.http.HttpServletResponse;
14 | import java.io.IOException;
15 | import java.io.OutputStream;
16 | import java.util.UUID;
17 |
18 | /**
19 | * websocket client包装
20 | */
21 | @Slf4j
22 | @Getter
23 | @Setter
24 | public class TTSWebSocket {
25 |
26 | private static int serialNum = 1;
27 |
28 | private Integer id;
29 |
30 | /**
31 | * 编码(自定义取值, 用以识别websocket client)
32 | */
33 | private String code;
34 |
35 | /**
36 | * websocket client
37 | */
38 | private WebSocket webSocket;
39 |
40 | /**
41 | * websocket是否可用
42 | */
43 | private boolean available;
44 |
45 | /**
46 | * tts配置
47 | */
48 | private TTSConfig ttsConfig;
49 |
50 | /**
51 | * 是否正在合成语音
52 | */
53 | private boolean synthesizing;
54 |
55 | /**
56 | * 音频的mime类型
57 | */
58 | private String mime;
59 |
60 | /**
61 | * 音频数据缓存
62 | */
63 | private Buffer buffer;
64 |
65 | public TTSWebSocket() {
66 | }
67 |
68 | public TTSWebSocket(String code, TTSConfig ttsConfig) {
69 | this.id = this.getSerialNum();
70 | this.code = code;
71 | this.available = true;
72 | this.ttsConfig = ttsConfig;
73 | this.webSocket = this.createWs();
74 | }
75 |
76 | @Override
77 | public String toString() {
78 | return "TTSWebSocket{" +
79 | "id=" + id +
80 | ", code='" + code + '\'' +
81 | ", available=" + available +
82 | ", synthesizing=" + synthesizing +
83 | ", mime='" + mime + '\'' +
84 | '}';
85 | }
86 |
87 | /**
88 | * 部分属性初始化
89 | */
90 | public void init() {
91 | this.mime = null;
92 | this.buffer = new Buffer();
93 | }
94 |
95 | public void send(String originText) {
96 | this.init();
97 | String text = Tool.fixTrim(originText);
98 | // 替换一些会导致不返回数据的特殊字符\p{N}
99 | /**
100 | * \s matches any whitespace character (equivalent to [\r\n\t\f\v ])
101 | * \p{P} matches any kind of punctuation character
102 | * \p{Z} matches any kind of whitespace or invisible separator
103 | * \p{S} matches any math symbols, currency signs, dingbats, box-drawing characters, etc
104 | */
105 | String temp = text.replaceAll("[\\s\\p{P}\\p{Z}\\p{S}]", "");
106 | if (temp.length() < 1) {
107 | // 可识别长度为0
108 | this.synthesizing = false;
109 | return;
110 | }
111 | text = this.escapeXmlChar(text);
112 | text = this.makeSsmlMsg(text);
113 |
114 | this.synthesizing = true;
115 | this.webSocket.send(text);
116 | }
117 |
118 | /**
119 | * 等待语音合成完成, 写入response
120 | * @param response
121 | */
122 | public void writeAudioStream(HttpServletResponse response) {
123 | long startTime = System.currentTimeMillis();
124 | while (this.synthesizing) {
125 | try {
126 | Thread.sleep(100);
127 | long time = System.currentTimeMillis() - startTime;
128 | if (time > 30 * 1000) {
129 | // 超时30s
130 | this.synthesizing = false;
131 | }
132 | } catch (InterruptedException ex) {
133 | log.error("等待合成语音流异常", ex);
134 | }
135 | }
136 |
137 | byte[] bytes = buffer.readByteArray();
138 | if (bytes.length > 0) {
139 | response.setHeader("Content-Type", this.mime);
140 | try (OutputStream out = response.getOutputStream()) {
141 | out.write(bytes, 0, bytes.length);
142 | } catch (IOException ex) {
143 | log.error("语音流转储响应流失败: {}", ex.getMessage());
144 | response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
145 | }
146 | }
147 | // 释放资源
148 | this.buffer = null;
149 | }
150 |
151 | /**
152 | * 音频流写入缓存
153 | * @param byteString
154 | */
155 | public void writeBuffer(ByteString byteString) {
156 | this.buffer.write(byteString);
157 | }
158 |
159 | /**
160 | * websocket不可再使用, 丢弃
161 | */
162 | public void drop() {
163 | this.available = false;
164 | // 关闭websocket, 释放资源
165 | this.webSocket.close(1000, null);
166 | this.webSocket = null;
167 | }
168 |
169 | /**
170 | * 构造格式化消息
171 | * @param text
172 | * @return
173 | */
174 | private String makeSsmlMsg(String text) {
175 | // 默认 "+0Hz"
176 | int pitch = 0;
177 | // 语速+, 默认 "+0%"
178 | int rate = 0;
179 | // 音量+, 默认 "+0%"
180 | int volumn = 0;
181 | String pitchStr = pitch >= 0 ? "+" + pitch + "Hz" : pitch + "Hz";
182 | String rateStr = rate >= 0 ? "+" + rate + "%" : rate + "%";
183 | String volumnStr = volumn >= 0 ? "+" + volumn + "%" : volumn + "%";
184 |
185 | String str = "X-RequestId:" + connectId() + "\r\n" +
186 | "Content-Type:application/ssml+xml\r\n" +
187 | "X-Timestamp:" + ttsConfig.getTime() + "Z\r\n" +
188 | "Path:ssml\r\n\r\n" +
189 | "" +
190 | "" +
191 | ""
194 | + text
195 | + "";
196 | if (log.isDebugEnabled()) {
197 | log.debug("send:\n{}", str);
198 | }
199 | return str;
200 | }
201 |
202 | /**
203 | * get uuid
204 | * @return
205 | */
206 | private String connectId() {
207 | return UUID.randomUUID().toString().replace("-", "");
208 | }
209 |
210 | /**
211 | * 创建连接服务的websocket
212 | * @return
213 | */
214 | private WebSocket createWs() {
215 | String url = ttsConfig.getWssUrl() + "?TrustedClientToken=" + ttsConfig.getTrustedClientToken() +
216 | "&ConnectionId=" + connectId();
217 | Request request = new Request.Builder()
218 | .url(url)
219 | .header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.159 Safari/537.36 Edg/92.0.902.84")
220 | .header("Origin", "chrome-extension://jdiccldimpdaibmpdkjnbmckianbfold")
221 | .header("Pragma", "no-cache")
222 | .header("Cache-Control", "no-cache")
223 | .header("Accept-Encoding", "gzip, deflate, br")
224 | .header("Accept-Language", "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6")
225 | .build();
226 | // 可以使用匿名内部类
227 | WebSocket webSocket = new OkHttpClient().newWebSocket(request, new TTSWebSocketListener(this));
228 | String configMsg = ttsConfig.getSynthesizeConfig();
229 | if (log.isDebugEnabled()) {
230 | log.debug("send:\n{}", configMsg);
231 | }
232 | webSocket.send(configMsg);
233 | return webSocket;
234 | }
235 |
236 | /**
237 | * 转义xml特殊字符
238 | * @param str
239 | * @return
240 | */
241 | private String escapeXmlChar(String str) {
242 | str = str.replace("&", "&");
243 | str = str.replace(">", ">");
244 | str = str.replace("<", "<");
245 | return str;
246 | }
247 |
248 | private int getSerialNum() {
249 | int n = serialNum++;
250 | if (serialNum == Integer.MAX_VALUE) {
251 | serialNum = 1;
252 | }
253 | return n;
254 | }
255 | }
256 |
--------------------------------------------------------------------------------
/src/main/java/com/taoing/ttsserver/ws/TTSWebSocketListener.java:
--------------------------------------------------------------------------------
1 | package com.taoing.ttsserver.ws;
2 |
3 | import lombok.extern.slf4j.Slf4j;
4 | import okhttp3.Response;
5 | import okhttp3.WebSocket;
6 | import okhttp3.WebSocketListener;
7 | import okio.ByteString;
8 | import org.jetbrains.annotations.NotNull;
9 | import org.jetbrains.annotations.Nullable;
10 |
11 | import java.nio.charset.StandardCharsets;
12 |
13 | @Slf4j
14 | public class TTSWebSocketListener extends WebSocketListener {
15 |
16 | /**
17 | * websocket client
18 | */
19 | private TTSWebSocket client;
20 |
21 |
22 | public TTSWebSocketListener() {
23 | }
24 |
25 | public TTSWebSocketListener(TTSWebSocket client) {
26 | this.client = client;
27 | }
28 |
29 | /**
30 | * websocket完全关闭
31 | * @param webSocket
32 | * @param code
33 | * @param reason
34 | */
35 | @Override
36 | public void onClosed(@NotNull WebSocket webSocket, int code, @NotNull String reason) {
37 | // 触发onFailure后, 再调用close方法, 也不会再触发该方法
38 | log.info("onClosed: {}-{}", code, reason);
39 | if (log.isDebugEnabled()) {
40 | log.debug("websocket完全关闭: {}", client.toString());
41 | }
42 | client.setAvailable(false);
43 | }
44 |
45 | /**
46 | * 服务端关闭连接, 会触发该方法
47 | * @param webSocket
48 | * @param code
49 | * @param reason
50 | */
51 | @Override
52 | public void onClosing(@NotNull WebSocket webSocket, int code, @NotNull String reason) {
53 | log.info("onClosing: {}-{}", code, reason);
54 | if (log.isDebugEnabled()) {
55 | log.debug("websocket服务端关闭: {}", client.toString());
56 | }
57 | // 设置websocket不可用, 进一步关闭该websocket以释放资源
58 | client.setAvailable(false);
59 | }
60 |
61 | @Override
62 | public void onFailure(@NotNull WebSocket webSocket, @NotNull Throwable t, @Nullable Response response) {
63 | // websocket连接空闲30s后, 服务端主动断开连接, 触发了该方法, 后续该websocket不再可用
64 | client.drop();
65 |
66 | String message = t.getMessage();
67 | if (response != null) {
68 | message += "-" + response.message();
69 | }
70 | log.warn("websocket与服务端中断连接: {}, onFailure: {}", client.toString(), message);
71 | }
72 |
73 | @Override
74 | public void onMessage(@NotNull WebSocket webSocket, @NotNull String text) {
75 | if (log.isDebugEnabled()) {
76 | log.debug("recv:\n{}", text);
77 | }
78 | String startTag = "turn.start";
79 | String endTag = "turn.end";
80 | int startIndex = text.indexOf(startTag);
81 | int endIndex = text.indexOf(endTag);
82 | // 生成开始
83 | if (startIndex != -1) {
84 | if (log.isDebugEnabled()) {
85 | log.debug("turn.start");
86 | }
87 | } else if (endIndex != -1) {
88 | // 生成结束
89 | client.setSynthesizing(false);
90 | if (log.isDebugEnabled()) {
91 | log.debug("turn.end");
92 | }
93 | }
94 | }
95 |
96 | @Override
97 | public void onMessage(@NotNull WebSocket webSocket, @NotNull ByteString bytes) {
98 | if (log.isDebugEnabled()) {
99 | log.debug("recv:\n{}", bytes.utf8());
100 | }
101 | // 音频数据流标志头
102 | String audioTag = "Path:audio\r\n";
103 | String startTag = "Content-Type:";
104 | String endTag = "\r\nX-StreamId";
105 |
106 | int audioIndex = bytes.indexOf(audioTag.getBytes(StandardCharsets.UTF_8));
107 | int startIndex = bytes.indexOf(startTag.getBytes(StandardCharsets.UTF_8));
108 | int endIndex = bytes.indexOf(endTag.getBytes(StandardCharsets.UTF_8));
109 | if (audioIndex != -1) {
110 | audioIndex += audioTag.length();
111 | if (client.getMime() == null) {
112 | client.setMime(bytes.substring(startIndex + startTag.length(), endIndex).utf8());
113 | }
114 | if ("audio/x-wav".equals(client.getMime()) && bytes.indexOf("RIFF".getBytes(StandardCharsets.UTF_8)) != -1) {
115 | // 去除WAV文件的文件头,解决播放开头时的杂音
116 | audioIndex += 44;
117 | }
118 | client.writeBuffer(bytes.substring(audioIndex));
119 | }
120 | }
121 |
122 | @Override
123 | public void onOpen(@NotNull WebSocket webSocket, @NotNull Response response) {
124 | if (log.isDebugEnabled()) {
125 | log.debug("微软tts websocket服务已连接: {}", client.toString());
126 | }
127 | }
128 | }
129 |
--------------------------------------------------------------------------------
/src/main/java/com/taoing/ttsserver/ws/WebSocketManager.java:
--------------------------------------------------------------------------------
1 | package com.taoing.ttsserver.ws;
2 |
3 | import lombok.Getter;
4 | import lombok.Setter;
5 | import lombok.extern.slf4j.Slf4j;
6 | import org.springframework.beans.factory.annotation.Autowired;
7 | import org.springframework.stereotype.Component;
8 |
9 | import java.util.HashMap;
10 | import java.util.Map;
11 |
12 | /**
13 | * websocket client 管理
14 | */
15 | @Slf4j
16 | @Component
17 | @Getter
18 | @Setter
19 | public class WebSocketManager {
20 | /**
21 | * 可用websocket client识别编码, 最多3个
22 | */
23 | public static final String[] AVAILABLE_CODES = {"xxttsa01", "xxttsa02", "xxttsa03"};
24 |
25 | @Autowired
26 | private TTSConfig ttsConfig;
27 |
28 | private Map webSocketMap;
29 |
30 | public WebSocketManager() {
31 | webSocketMap = new HashMap<>();
32 | }
33 |
34 | public TTSWebSocket getClient(String code) {
35 | boolean isMatch = false;
36 | for (String item : AVAILABLE_CODES) {
37 | if (item.equals(code)) {
38 | isMatch = true;
39 | break;
40 | }
41 | }
42 | if (!isMatch) {
43 | return null;
44 | }
45 | TTSWebSocket webSocket = webSocketMap.get(code);
46 | if (webSocket == null || !webSocket.isAvailable()) {
47 | webSocket = new TTSWebSocket(code, ttsConfig);
48 | webSocketMap.put(code, webSocket);
49 | }
50 | return webSocket;
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/src/main/resources/application-dev.yml:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taoing/tts-server/616d91fde597c1ab5bfae3552cb03613306e3df9/src/main/resources/application-dev.yml
--------------------------------------------------------------------------------
/src/main/resources/application-pro.yml:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taoing/tts-server/616d91fde597c1ab5bfae3552cb03613306e3df9/src/main/resources/application-pro.yml
--------------------------------------------------------------------------------
/src/main/resources/application-test.yml:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taoing/tts-server/616d91fde597c1ab5bfae3552cb03613306e3df9/src/main/resources/application-test.yml
--------------------------------------------------------------------------------
/src/main/resources/application.yml:
--------------------------------------------------------------------------------
1 | server:
2 | port: 9027
3 |
4 | spring:
5 | application:
6 | name: tts-server
7 | profiles:
8 | active: dev
9 | jackson:
10 | time-zone: GMT+8
11 | date-format: yyyy-MM-dd HH:mm:ss
12 | deserialization:
13 | # 允许对象忽略json中不存在的属性
14 | fail_on_unknown_properties: false
15 |
16 | logging:
17 | config: classpath:logback-spring.xml
18 |
19 | # 微软edge浏览器tts语音朗读配置
20 | tts:
21 | edge:
22 | # https://speech.platform.bing.com/consumer/speech/synthesize/readaloud/voices/list?trustedclienttoken=6A5AA1D4EAFF4E9FB37E23D68491D6F4
23 | # 可查看可用tts列表
24 | voice-list-url: https://speech.platform.bing.com/consumer/speech/synthesize/readaloud/voices/list # 微软可用声音tts列表查询url
25 | wss-url: wss://speech.platform.bing.com/consumer/speech/synthesize/readaloud/edge/v1 # 语音合成websocket服务
26 | trusted-client-token: 6A5AA1D4EAFF4E9FB37E23D68491D6F4 # token
27 | voice-name: Microsoft Server Speech Text to Speech Voice (zh-CN, XiaoxiaoNeural) # 默认使用的声音tts(微软晓晓)
28 | voice-short-name: zh-CN-XiaoxiaoNeural # 默认使用的声音tts(微软晓晓简写)
29 | codec: audio-24khz-48kbitrate-mono-mp3 # 声音编码
30 | local: zh-CN # 区域
31 | sentenceBoundaryEnabled: false # 可以试试true或false的效果(感知不到差别)
32 | wordBoundaryEnabled: false # 可以试试true或false的效果(感知不到差别)
33 |
--------------------------------------------------------------------------------
/src/main/resources/logback-spring.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | %d %p (%file:%line\)- %m%n
7 |
8 | UTF-8
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | ./logs/tts-server.log
17 |
18 |
19 |
20 |
21 |
22 | ./logs/tts-server.%d.%i.log
23 |
24 | 30
25 |
26 |
27 | 10MB
28 |
29 |
30 |
31 |
32 |
33 | %d %p (%file:%line\)- %m%n
34 |
35 |
36 | UTF-8
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
--------------------------------------------------------------------------------
/src/test/java/com/taoing/ttsserver/TtsServerApplicationTests.java:
--------------------------------------------------------------------------------
1 | package com.taoing.ttsserver;
2 |
3 | import org.junit.jupiter.api.Test;
4 | import org.springframework.boot.test.context.SpringBootTest;
5 |
6 | @SpringBootTest
7 | class TtsServerApplicationTests {
8 |
9 | @Test
10 | void contextLoads() {
11 | }
12 |
13 | }
14 |
--------------------------------------------------------------------------------
/startup.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | #发布tts-server, jar包上传到 ./upload 目录
3 |
4 | set -x
5 | set -e
6 |
7 | uploadDir=./upload
8 | jarfile=tts-server-1.0.0.jar
9 |
10 | #kill进程
11 | if [[ $(ps -ef | grep "${jarfile}" | grep -v "grep") != "" ]]; then
12 | ps -ef | grep "${jarfile}" | grep -v "grep" | awk '{print $2}' | xargs kill
13 | fi
14 | sleep 2
15 | if [ -f ./$jarfile ]; then
16 | rm -rf ./$jarfile
17 | fi
18 | cp -f $uploadDir/$jarfile ./$jarfile
19 |
20 | nohup java -jar -Dspring.profiles.active=test $jarfile > /dev/null 2>&1 &
21 | echo "${jarfile} 发布完成"
22 |
--------------------------------------------------------------------------------