|
@@ -0,0 +1,468 @@
|
|
|
|
|
+package com.telerobot.fs.tts.viitor;
|
|
|
|
|
+
|
|
|
|
|
+import com.alibaba.fastjson.JSON;
|
|
|
|
|
+import com.alibaba.fastjson.JSONObject;
|
|
|
|
|
+import com.telerobot.fs.config.SystemConfig;
|
|
|
|
|
+import okhttp3.OkHttpClient;
|
|
|
|
|
+import okhttp3.Request;
|
|
|
|
|
+import okhttp3.Response;
|
|
|
|
|
+import okhttp3.WebSocket;
|
|
|
|
|
+import okhttp3.WebSocketListener;
|
|
|
|
|
+import okio.ByteString;
|
|
|
|
|
+import org.apache.commons.lang.StringUtils;
|
|
|
|
|
+import org.slf4j.Logger;
|
|
|
|
|
+import org.slf4j.LoggerFactory;
|
|
|
|
|
+
|
|
|
|
|
+import javax.crypto.Mac;
|
|
|
|
|
+import javax.crypto.spec.SecretKeySpec;
|
|
|
|
|
+import javax.net.ssl.SSLContext;
|
|
|
|
|
+import javax.net.ssl.TrustManager;
|
|
|
|
|
+import javax.net.ssl.X509TrustManager;
|
|
|
|
|
+import java.io.ByteArrayOutputStream;
|
|
|
|
|
+import java.io.File;
|
|
|
|
|
+import java.io.FileOutputStream;
|
|
|
|
|
+import java.nio.charset.StandardCharsets;
|
|
|
|
|
+import java.security.MessageDigest;
|
|
|
|
|
+import java.security.SecureRandom;
|
|
|
|
|
+import java.time.Instant;
|
|
|
|
|
+import java.time.ZoneOffset;
|
|
|
|
|
+import java.time.format.DateTimeFormatter;
|
|
|
|
|
+import java.util.Base64;
|
|
|
|
|
+import java.util.UUID;
|
|
|
|
|
+import java.util.concurrent.CountDownLatch;
|
|
|
|
|
+import java.util.concurrent.TimeUnit;
|
|
|
|
|
+
|
|
|
|
|
+public class ViiTorTTSWebApi {
|
|
|
|
|
+ private static final Logger log = LoggerFactory.getLogger(ViiTorTTSWebApi.class);
|
|
|
|
|
+ private static final MediaTypeHolder MEDIA_TYPE_HOLDER = new MediaTypeHolder();
|
|
|
|
|
+ private static final DateTimeFormatter UTC_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss'Z'")
|
|
|
|
|
+ .withZone(ZoneOffset.UTC);
|
|
|
|
|
+ private static final String DEFAULT_TOKEN_URL = "https://tts.ilivedata.com/api/v2/speech/synthesis/ws-token";
|
|
|
|
|
+ private static final String DEFAULT_SIGN_PATH = "/api/v2/speech/synthesis/ws-token";
|
|
|
|
|
+ private static final String DEFAULT_WS_URL = "wss://tts.ilivedata.com/api/v1/speech/synthesis/ws";
|
|
|
|
|
+ private static final OkHttpClient CLIENT = new OkHttpClient.Builder()
|
|
|
|
|
+ .connectTimeout(10, TimeUnit.SECONDS)
|
|
|
|
|
+ .readTimeout(0, TimeUnit.MILLISECONDS)
|
|
|
|
|
+ .build();
|
|
|
|
|
+
|
|
|
|
|
+ private static volatile String ttsAccountJson;
|
|
|
|
|
+ private static volatile JSONObject ttsAccount;
|
|
|
|
|
+
|
|
|
|
|
+ private ViiTorTTSWebApi() {
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private static final class MediaTypeHolder {
|
|
|
|
|
+ private final okhttp3.MediaType jsonType = okhttp3.MediaType.parse("application/json; charset=utf-8");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private static final class WsResult {
|
|
|
|
|
+ private final CountDownLatch latch = new CountDownLatch(1);
|
|
|
|
|
+ private final ByteArrayOutputStream audioOutput = new ByteArrayOutputStream();
|
|
|
|
|
+ private volatile boolean initReceived = false;
|
|
|
|
|
+ private volatile boolean completed = false;
|
|
|
|
|
+ private volatile boolean failed = false;
|
|
|
|
|
+ private volatile String errorMessage = "";
|
|
|
|
|
+ private volatile int sampleRate = 8000;
|
|
|
|
|
+ private volatile WebSocket webSocket;
|
|
|
|
|
+
|
|
|
|
|
+ private void fail(String message) {
|
|
|
|
|
+ if (!failed) {
|
|
|
|
|
+ failed = true;
|
|
|
|
|
+ completed = true;
|
|
|
|
|
+ errorMessage = message;
|
|
|
|
|
+ latch.countDown();
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private void finish() {
|
|
|
|
|
+ if (!completed) {
|
|
|
|
|
+ completed = true;
|
|
|
|
|
+ latch.countDown();
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private static JSONObject getAccount() {
|
|
|
|
|
+ String latestJson = SystemConfig.getValue("vt-tts-account-json", "");
|
|
|
|
|
+ if (StringUtils.isBlank(latestJson)) {
|
|
|
|
|
+ log.error("param `vt-tts-account-json` can not be blank.");
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!latestJson.equals(ttsAccountJson) || ttsAccount == null) {
|
|
|
|
|
+ synchronized (ViiTorTTSWebApi.class) {
|
|
|
|
|
+ latestJson = SystemConfig.getValue("vt-tts-account-json", "");
|
|
|
|
|
+ if (!latestJson.equals(ttsAccountJson) || ttsAccount == null) {
|
|
|
|
|
+ ttsAccountJson = latestJson;
|
|
|
|
|
+ try {
|
|
|
|
|
+ ttsAccount = JSON.parseObject(latestJson);
|
|
|
|
|
+ } catch (Throwable e) {
|
|
|
|
|
+ log.error("parse `vt-tts-account-json` error: {}", e.toString(), e);
|
|
|
|
|
+ ttsAccount = null;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return ttsAccount;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private static String config(JSONObject account, String key, String defaultValue) {
|
|
|
|
|
+ if (account == null) {
|
|
|
|
|
+ return defaultValue;
|
|
|
|
|
+ }
|
|
|
|
|
+ String value = account.getString(key);
|
|
|
|
|
+ return StringUtils.isBlank(value) ? defaultValue : value.trim();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private static int configInt(JSONObject account, String key, int defaultValue) {
|
|
|
|
|
+ String value = config(account, key, "");
|
|
|
|
|
+ if (StringUtils.isBlank(value)) {
|
|
|
|
|
+ return defaultValue;
|
|
|
|
|
+ }
|
|
|
|
|
+ try {
|
|
|
|
|
+ return Integer.parseInt(value);
|
|
|
|
|
+ } catch (Throwable ignore) {
|
|
|
|
|
+ return defaultValue;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private static boolean configBool(JSONObject account, String key, boolean defaultValue) {
|
|
|
|
|
+ String value = config(account, key, String.valueOf(defaultValue));
|
|
|
|
|
+ if (StringUtils.isBlank(value)) {
|
|
|
|
|
+ return defaultValue;
|
|
|
|
|
+ }
|
|
|
|
|
+ return "1".equals(value) || "true".equalsIgnoreCase(value) || "yes".equalsIgnoreCase(value);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private static String normalizeFormat(String value) {
|
|
|
|
|
+ String format = StringUtils.trimToEmpty(value).toLowerCase();
|
|
|
|
|
+ return StringUtils.isBlank(format) ? "pcm" : format;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private static String sha256Hex(String value) throws Exception {
|
|
|
|
|
+ MessageDigest digest = MessageDigest.getInstance("SHA-256");
|
|
|
|
|
+ byte[] hash = digest.digest(value.getBytes(StandardCharsets.UTF_8));
|
|
|
|
|
+ return bytesToHex(hash);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private static byte[] hmacSha256(String key, String message) throws Exception {
|
|
|
|
|
+ Mac mac = Mac.getInstance("HmacSHA256");
|
|
|
|
|
+ mac.init(new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
|
|
|
|
|
+ return mac.doFinal(message.getBytes(StandardCharsets.UTF_8));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private static String hmacSha256Base64(String key, String message) throws Exception {
|
|
|
|
|
+ return Base64.getEncoder().encodeToString(hmacSha256(key, message));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private static String bytesToHex(byte[] bytes) {
|
|
|
|
|
+ StringBuilder sb = new StringBuilder(bytes.length * 2);
|
|
|
|
|
+ for (byte b : bytes) {
|
|
|
|
|
+ sb.append(String.format("%02x", b));
|
|
|
|
|
+ }
|
|
|
|
|
+ return sb.toString();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private static String formatUtcNow() {
|
|
|
|
|
+ return UTC_FORMATTER.format(Instant.now());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private static OkHttpClient buildClient(boolean verifyPeer, int connectTimeoutMs) throws Exception {
|
|
|
|
|
+ OkHttpClient.Builder builder = CLIENT.newBuilder()
|
|
|
|
|
+ .connectTimeout(connectTimeoutMs, TimeUnit.MILLISECONDS)
|
|
|
|
|
+ .readTimeout(0, TimeUnit.MILLISECONDS);
|
|
|
|
|
+ if (verifyPeer) {
|
|
|
|
|
+ return builder.build();
|
|
|
|
|
+ }
|
|
|
|
|
+ final TrustManager[] trustAllCerts = new TrustManager[]{
|
|
|
|
|
+ new X509TrustManager() {
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public void checkClientTrusted(java.security.cert.X509Certificate[] chain, String authType) {
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public void checkServerTrusted(java.security.cert.X509Certificate[] chain, String authType) {
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public java.security.cert.X509Certificate[] getAcceptedIssuers() {
|
|
|
|
|
+ return new java.security.cert.X509Certificate[0];
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+ SSLContext sslContext = SSLContext.getInstance("TLS");
|
|
|
|
|
+ sslContext.init(null, trustAllCerts, new SecureRandom());
|
|
|
|
|
+ builder.sslSocketFactory(sslContext.getSocketFactory(), (X509TrustManager) trustAllCerts[0]);
|
|
|
|
|
+ builder.hostnameVerifier((hostname, session) -> true);
|
|
|
|
|
+ return builder.build();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private static String parseHost(String url, String defaultHost) {
|
|
|
|
|
+ if (StringUtils.isBlank(url)) {
|
|
|
|
|
+ return defaultHost;
|
|
|
|
|
+ }
|
|
|
|
|
+ String target = url;
|
|
|
|
|
+ int schemeIndex = target.indexOf("://");
|
|
|
|
|
+ if (schemeIndex >= 0) {
|
|
|
|
|
+ target = target.substring(schemeIndex + 3);
|
|
|
|
|
+ }
|
|
|
|
|
+ int slashIndex = target.indexOf('/');
|
|
|
|
|
+ return slashIndex >= 0 ? target.substring(0, slashIndex) : target;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private static String parsePath(String url, String defaultPath) {
|
|
|
|
|
+ if (StringUtils.isBlank(url)) {
|
|
|
|
|
+ return defaultPath;
|
|
|
|
|
+ }
|
|
|
|
|
+ int schemeIndex = url.indexOf("://");
|
|
|
|
|
+ int hostStart = schemeIndex >= 0 ? schemeIndex + 3 : 0;
|
|
|
|
|
+ int slashIndex = url.indexOf('/', hostStart);
|
|
|
|
|
+ if (slashIndex < 0) {
|
|
|
|
|
+ return defaultPath;
|
|
|
|
|
+ }
|
|
|
|
|
+ String path = url.substring(slashIndex);
|
|
|
|
|
+ int queryIndex = path.indexOf('?');
|
|
|
|
|
+ return queryIndex >= 0 ? path.substring(0, queryIndex) : path;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private static String fetchWsUrl(JSONObject account, OkHttpClient client) throws Exception {
|
|
|
|
|
+ String tokenUrl = config(account, "token-url", DEFAULT_TOKEN_URL);
|
|
|
|
|
+ String host = parseHost(tokenUrl, "tts.ilivedata.com");
|
|
|
|
|
+ String path = parsePath(tokenUrl, DEFAULT_SIGN_PATH);
|
|
|
|
|
+ String appId = config(account, "appid", "");
|
|
|
|
|
+ String secretKey = config(account, "secret-key", "");
|
|
|
|
|
+ String timestamp = formatUtcNow();
|
|
|
|
|
+ String stringToSign = "GET\n" +
|
|
|
|
|
+ host + "\n" +
|
|
|
|
|
+ path + "\n" +
|
|
|
|
|
+ "X-AppId:" + appId + "\n" +
|
|
|
|
|
+ "X-TimeStamp:" + timestamp;
|
|
|
|
|
+ String authorization = hmacSha256Base64(secretKey, stringToSign);
|
|
|
|
|
+
|
|
|
|
|
+ Request request = new Request.Builder()
|
|
|
|
|
+ .url(tokenUrl)
|
|
|
|
|
+ .get()
|
|
|
|
|
+ .header("Accept", "application/json")
|
|
|
|
|
+ .header("X-AppId", appId)
|
|
|
|
|
+ .header("X-TimeStamp", timestamp)
|
|
|
|
|
+ .header("Authorization", authorization)
|
|
|
|
|
+ .build();
|
|
|
|
|
+ try (Response response = client.newCall(request).execute()) {
|
|
|
|
|
+ String body = response.body() == null ? "" : response.body().string();
|
|
|
|
|
+ log.info("viitor fetch token status={}, body={}", response.code(), body);
|
|
|
|
|
+ if (!response.isSuccessful()) {
|
|
|
|
|
+ throw new IllegalStateException("fetch ws token failed http=" + response.code() + ", body=" + body);
|
|
|
|
|
+ }
|
|
|
|
|
+ JSONObject json = JSON.parseObject(body);
|
|
|
|
|
+ Integer errorCode = json.getInteger("errorCode");
|
|
|
|
|
+ if (errorCode != null && errorCode != 0) {
|
|
|
|
|
+ throw new IllegalStateException("fetch ws token errorCode=" + errorCode + ", msg=" + json.getString("errorMessage"));
|
|
|
|
|
+ }
|
|
|
|
|
+ String token = json.getString("token");
|
|
|
|
|
+ String wsUrl = json.getString("wsUrl");
|
|
|
|
|
+ if (StringUtils.isBlank(token) || StringUtils.isBlank(wsUrl)) {
|
|
|
|
|
+ throw new IllegalStateException("fetch ws token response missing token/wsUrl");
|
|
|
|
|
+ }
|
|
|
|
|
+ return wsUrl.contains("?") ? wsUrl + "&token=" + token : wsUrl + "?token=" + token;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private static JSONObject buildRequestPayload(JSONObject account, String voiceCode, String text) {
|
|
|
|
|
+ JSONObject root = new JSONObject(true);
|
|
|
|
|
+ JSONObject request = new JSONObject(true);
|
|
|
|
|
+ JSONObject voice = new JSONObject(true);
|
|
|
|
|
+ JSONObject output = new JSONObject(true);
|
|
|
|
|
+ String appId = config(account, "appid", "");
|
|
|
|
|
+ String sessionId = UUID.randomUUID().toString().replace("-", "");
|
|
|
|
|
+ String voiceName = StringUtils.isNotBlank(voiceCode) ? voiceCode : config(account, "voice-name", "");
|
|
|
|
|
+ String language = config(account, "language", "zh-CN");
|
|
|
|
|
+ String outputFormat = normalizeFormat(config(account, "format", "pcm"));
|
|
|
|
|
+
|
|
|
|
|
+ root.put("appId", Long.parseLong(appId));
|
|
|
|
|
+ root.put("sessionId", sessionId);
|
|
|
|
|
+ request.put("appId", Long.parseLong(appId));
|
|
|
|
|
+ request.put("text", text);
|
|
|
|
|
+ request.put("language", language);
|
|
|
|
|
+ voice.put("name", voiceName);
|
|
|
|
|
+ output.put("format", outputFormat);
|
|
|
|
|
+ request.put("voice", voice);
|
|
|
|
|
+ request.put("output", output);
|
|
|
|
|
+ root.put("request", request);
|
|
|
|
|
+ return root;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private static void writeLittleEndianInt(ByteArrayOutputStream out, int value) {
|
|
|
|
|
+ out.write(value & 0xFF);
|
|
|
|
|
+ out.write((value >> 8) & 0xFF);
|
|
|
|
|
+ out.write((value >> 16) & 0xFF);
|
|
|
|
|
+ out.write((value >> 24) & 0xFF);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private static void writeLittleEndianShort(ByteArrayOutputStream out, int value) {
|
|
|
|
|
+ out.write(value & 0xFF);
|
|
|
|
|
+ out.write((value >> 8) & 0xFF);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private static void writeWavFile(String targetPath, byte[] audioBytes, int sampleRate) throws Exception {
|
|
|
|
|
+ int channels = 1;
|
|
|
|
|
+ int bitsPerSample = 16;
|
|
|
|
|
+ int blockAlign = channels * bitsPerSample / 8;
|
|
|
|
|
+ int byteRate = sampleRate * blockAlign;
|
|
|
|
|
+ ByteArrayOutputStream out = new ByteArrayOutputStream(44 + audioBytes.length);
|
|
|
|
|
+ out.write(new byte[]{'R', 'I', 'F', 'F'});
|
|
|
|
|
+ writeLittleEndianInt(out, 36 + audioBytes.length);
|
|
|
|
|
+ out.write(new byte[]{'W', 'A', 'V', 'E'});
|
|
|
|
|
+ out.write(new byte[]{'f', 'm', 't', ' '});
|
|
|
|
|
+ writeLittleEndianInt(out, 16);
|
|
|
|
|
+ writeLittleEndianShort(out, 1);
|
|
|
|
|
+ writeLittleEndianShort(out, channels);
|
|
|
|
|
+ writeLittleEndianInt(out, sampleRate);
|
|
|
|
|
+ writeLittleEndianInt(out, byteRate);
|
|
|
|
|
+ writeLittleEndianShort(out, blockAlign);
|
|
|
|
|
+ writeLittleEndianShort(out, bitsPerSample);
|
|
|
|
|
+ out.write(new byte[]{'d', 'a', 't', 'a'});
|
|
|
|
|
+ writeLittleEndianInt(out, audioBytes.length);
|
|
|
|
|
+ out.write(audioBytes);
|
|
|
|
|
+ try (FileOutputStream fout = new FileOutputStream(new File(targetPath))) {
|
|
|
|
|
+ fout.write(out.toByteArray());
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private static boolean saveAudioFile(String targetPath, byte[] audioBytes, int sampleRate) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ writeWavFile(targetPath, audioBytes, sampleRate > 0 ? sampleRate : 8000);
|
|
|
|
|
+ return true;
|
|
|
|
|
+ } catch (Throwable e) {
|
|
|
|
|
+ log.error("save viitor wav file failed, path={}, err={}", targetPath, e.toString(), e);
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private static boolean processWebSocketRequest(JSONObject account, String voiceCode, String text, String audioSaveFile) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ String appId = config(account, "appid", "");
|
|
|
|
|
+ String secretKey = config(account, "secret-key", "");
|
|
|
|
|
+ String voiceName = StringUtils.isNotBlank(voiceCode) ? voiceCode : config(account, "voice-name", "");
|
|
|
|
|
+ if (StringUtils.isBlank(appId) || StringUtils.isBlank(secretKey) || StringUtils.isBlank(voiceName)) {
|
|
|
|
|
+ log.error("viitor tts config missing appid/secret-key/voice-name");
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ boolean verifyPeer = configBool(account, "verify-peer", false);
|
|
|
|
|
+ int connectTimeoutMs = configInt(account, "connect-timeout-ms", 10000);
|
|
|
|
|
+ String wsUrl = fetchWsUrl(account, buildClient(verifyPeer, connectTimeoutMs));
|
|
|
|
|
+ OkHttpClient wsClient = buildClient(verifyPeer, connectTimeoutMs);
|
|
|
|
|
+ JSONObject payload = buildRequestPayload(account, voiceCode, text);
|
|
|
|
|
+ WsResult result = new WsResult();
|
|
|
|
|
+ Request request = new Request.Builder()
|
|
|
|
|
+ .url(wsUrl)
|
|
|
|
|
+ .header("User-Agent", "ViiTorTTSWebApi/1.0")
|
|
|
|
|
+ .build();
|
|
|
|
|
+
|
|
|
|
|
+ wsClient.newWebSocket(request, new WebSocketListener() {
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public void onOpen(WebSocket webSocket, Response response) {
|
|
|
|
|
+ result.webSocket = webSocket;
|
|
|
|
|
+ if (!webSocket.send(payload.toJSONString())) {
|
|
|
|
|
+ result.fail("send viitor websocket payload failed");
|
|
|
|
|
+ webSocket.close(1000, "send-failed");
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public void onMessage(WebSocket webSocket, String textMessage) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ JSONObject json = JSON.parseObject(textMessage);
|
|
|
|
|
+ Integer errorCode = json.getInteger("errorCode");
|
|
|
|
|
+ if (errorCode != null && errorCode != 0) {
|
|
|
|
|
+ result.fail("viitor errorCode=" + errorCode + ", msg=" + json.getString("errorMessage"));
|
|
|
|
|
+ webSocket.close(1000, "error-code");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ String event = json.getString("event");
|
|
|
|
|
+ if ("init".equals(event)) {
|
|
|
|
|
+ result.initReceived = true;
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ if ("audio".equals(event)) {
|
|
|
|
|
+ Integer sampleRate = json.getInteger("sampleRate");
|
|
|
|
|
+ if (sampleRate != null && sampleRate > 0) {
|
|
|
|
|
+ result.sampleRate = sampleRate;
|
|
|
|
|
+ }
|
|
|
|
|
+ String audioBase64 = json.getString("audioBase64");
|
|
|
|
|
+ if (StringUtils.isNotBlank(audioBase64)) {
|
|
|
|
|
+ result.audioOutput.write(Base64.getDecoder().decode(audioBase64));
|
|
|
|
|
+ }
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ if ("done".equals(event)) {
|
|
|
|
|
+ result.finish();
|
|
|
|
|
+ webSocket.close(1000, "done");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ if ("error".equals(event)) {
|
|
|
|
|
+ result.fail("viitor ws error: " + json.toJSONString());
|
|
|
|
|
+ webSocket.close(1000, "ws-error");
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (Throwable e) {
|
|
|
|
|
+ result.fail("parse viitor websocket message failed: " + e.getMessage());
|
|
|
|
|
+ webSocket.close(1000, "parse-error");
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public void onMessage(WebSocket webSocket, ByteString bytes) {
|
|
|
|
|
+ result.fail("unexpected binary frame from viitor websocket");
|
|
|
|
|
+ webSocket.close(1000, "unexpected-binary");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public void onFailure(WebSocket webSocket, Throwable t, Response response) {
|
|
|
|
|
+ result.fail("viitor websocket failure: " + (t == null ? "" : t.getMessage()));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public void onClosed(WebSocket webSocket, int code, String reason) {
|
|
|
|
|
+ if (!result.completed && !result.failed) {
|
|
|
|
|
+ result.fail("viitor websocket closed before done, code=" + code + ", reason=" + reason);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ if (!result.latch.await(40, TimeUnit.SECONDS)) {
|
|
|
|
|
+ if (result.webSocket != null) {
|
|
|
|
|
+ result.webSocket.close(1000, "timeout");
|
|
|
|
|
+ }
|
|
|
|
|
+ log.error("viitor tts websocket timeout, voiceName={}, text={}", voiceName, text);
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (result.failed) {
|
|
|
|
|
+ log.error("viitor tts websocket synthesize failed: {}", result.errorMessage);
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!result.initReceived) {
|
|
|
|
|
+ log.error("viitor tts websocket missing init event");
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (result.audioOutput.size() == 0) {
|
|
|
|
|
+ log.error("viitor tts websocket audio payload is empty");
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+ return saveAudioFile(audioSaveFile, result.audioOutput.toByteArray(), result.sampleRate);
|
|
|
|
|
+ } catch (Throwable e) {
|
|
|
|
|
+ log.error("viitor tts synthesize failed: {}", e.toString(), e);
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public static boolean shortTextTTSWebAPI(String voiceCode, String text, String ttsPath) {
|
|
|
|
|
+ if (!StringUtils.isNotBlank(StringUtils.trim(text))) {
|
|
|
|
|
+ log.info("tts text can not be null, ttsPath={}", ttsPath);
|
|
|
|
|
+ return true;
|
|
|
|
|
+ }
|
|
|
|
|
+ JSONObject account = getAccount();
|
|
|
|
|
+ if (account == null) {
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+ return processWebSocketRequest(account, voiceCode, text, ttsPath);
|
|
|
|
|
+ }
|
|
|
|
|
+}
|