├── .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 | --------------------------------------------------------------------------------