|
|
@@ -35,14 +35,22 @@ import javax.crypto.spec.SecretKeySpec;
|
|
|
import javax.servlet.http.HttpServletRequest;
|
|
|
import java.io.ByteArrayOutputStream;
|
|
|
import java.io.IOException;
|
|
|
+import java.io.InputStream;
|
|
|
+import java.io.OutputStream;
|
|
|
+import java.net.HttpURLConnection;
|
|
|
+import java.net.URL;
|
|
|
import java.net.URLEncoder;
|
|
|
import java.nio.charset.StandardCharsets;
|
|
|
+import java.nio.file.Files;
|
|
|
+import java.nio.file.Path;
|
|
|
+import java.nio.file.Paths;
|
|
|
import java.security.MessageDigest;
|
|
|
import java.time.ZoneId;
|
|
|
import java.time.ZonedDateTime;
|
|
|
import java.time.format.DateTimeFormatter;
|
|
|
import java.util.Base64;
|
|
|
import java.util.LinkedHashMap;
|
|
|
+import java.util.Locale;
|
|
|
import java.util.Map;
|
|
|
import java.util.concurrent.CountDownLatch;
|
|
|
import java.util.concurrent.TimeUnit;
|
|
|
@@ -68,6 +76,8 @@ public class XfVoiceCloneController extends BaseController {
|
|
|
private static final String CLONE_TTS_WS_URL = "wss://cn-huabei-1.xf-yun.com/v1/private/voice_clone";
|
|
|
private static final String CLONE_TTS_HOST = "cn-huabei-1.xf-yun.com";
|
|
|
private static final String CLONE_TTS_PATH = "/v1/private/voice_clone";
|
|
|
+ private static final DateTimeFormatter CLONE_AUTH_DATE_FORMATTER =
|
|
|
+ DateTimeFormatter.ofPattern("EEE, dd MMM yyyy HH:mm:ss 'GMT'", Locale.US);
|
|
|
private static final Long DEFAULT_TEXT_ID = 5001L;
|
|
|
private static final OkHttpClient HTTP_CLIENT = new OkHttpClient.Builder()
|
|
|
.connectTimeout(30, TimeUnit.SECONDS)
|
|
|
@@ -324,15 +334,48 @@ public class XfVoiceCloneController extends BaseController {
|
|
|
AtomicReference<String> errRef = new AtomicReference<>();
|
|
|
ByteArrayOutputStream audioOutput = new ByteArrayOutputStream();
|
|
|
|
|
|
+ // #region debug-point A:clone-auth-shape
|
|
|
+ reportDebugEvent("A", "XfVoiceCloneController.synthesizeCloneAudio",
|
|
|
+ "[DEBUG] clone websocket auth built",
|
|
|
+ new JSONObject(true)
|
|
|
+ .fluentPut("url", auth.url)
|
|
|
+ .fluentPut("date", auth.date)
|
|
|
+ .fluentPut("authorizationLength", auth.authorization == null ? 0 : auth.authorization.length())
|
|
|
+ .fluentPut("systemZone", ZoneId.systemDefault().getId())
|
|
|
+ .fluentPut("systemNow", ZonedDateTime.now().toString())
|
|
|
+ .fluentPut("engineVersion", engineVersion)
|
|
|
+ .fluentPut("assetIdSuffix", StringUtils.right(assetId, 8)));
|
|
|
+ log.info("clone websocket auth built url={}, date={}, authorizationLength={}, systemZone={}, systemNow={}, engineVersion={}, assetIdSuffix={}",
|
|
|
+ auth.url,
|
|
|
+ auth.date,
|
|
|
+ auth.authorization == null ? 0 : auth.authorization.length(),
|
|
|
+ ZoneId.systemDefault().getId(),
|
|
|
+ ZonedDateTime.now(),
|
|
|
+ engineVersion,
|
|
|
+ StringUtils.right(assetId, 8));
|
|
|
+ // #endregion
|
|
|
+
|
|
|
Request request = new Request.Builder()
|
|
|
.url(auth.url)
|
|
|
.addHeader("Host", CLONE_TTS_HOST)
|
|
|
.addHeader("Date", auth.date)
|
|
|
- .addHeader("X-Date", auth.date)
|
|
|
+ .addHeader("Authorization", auth.authorization)
|
|
|
.build();
|
|
|
HTTP_CLIENT.newWebSocket(request, new WebSocketListener() {
|
|
|
@Override
|
|
|
public void onOpen(WebSocket webSocket, Response response) {
|
|
|
+ // #region debug-point B:clone-ws-open
|
|
|
+ reportDebugEvent("B", "XfVoiceCloneController.onOpen",
|
|
|
+ "[DEBUG] clone websocket opened",
|
|
|
+ new JSONObject(true)
|
|
|
+ .fluentPut("httpCode", response == null ? -1 : response.code())
|
|
|
+ .fluentPut("serverDate", response == null ? "" : String.valueOf(response.header("Date")))
|
|
|
+ .fluentPut("requestDate", auth.date));
|
|
|
+ log.info("clone websocket opened httpCode={}, serverDate={}, requestDate={}",
|
|
|
+ response == null ? -1 : response.code(),
|
|
|
+ response == null ? "" : String.valueOf(response.header("Date")),
|
|
|
+ auth.date);
|
|
|
+ // #endregion
|
|
|
JSONObject payload = buildCloneTtsPayload(account.getString("appId"), assetId, text, engineVersion);
|
|
|
webSocket.send(payload.toJSONString());
|
|
|
}
|
|
|
@@ -340,8 +383,10 @@ public class XfVoiceCloneController extends BaseController {
|
|
|
@Override
|
|
|
public void onMessage(WebSocket webSocket, String textMessage) {
|
|
|
try {
|
|
|
+ log.info("clone websocket message text={}", textMessage);
|
|
|
JSONObject json = JSON.parseObject(textMessage);
|
|
|
- int code = json.getIntValue("code");
|
|
|
+ JSONObject header = json.getJSONObject("header");
|
|
|
+ int code = header == null ? json.getIntValue("code") : header.getIntValue("code");
|
|
|
if (code != 0) {
|
|
|
errRef.set("讯飞试音接口返回错误: " + json.toJSONString());
|
|
|
webSocket.close(1000, "error");
|
|
|
@@ -379,12 +424,29 @@ public class XfVoiceCloneController extends BaseController {
|
|
|
|
|
|
@Override
|
|
|
public void onFailure(WebSocket webSocket, Throwable t, Response response) {
|
|
|
+ // #region debug-point C:clone-ws-failure
|
|
|
+ reportDebugEvent("C", "XfVoiceCloneController.onFailure",
|
|
|
+ "[DEBUG] clone websocket failure",
|
|
|
+ new JSONObject(true)
|
|
|
+ .fluentPut("throwable", t == null ? "" : StringUtils.defaultString(t.getMessage()))
|
|
|
+ .fluentPut("httpCode", response == null ? -1 : response.code())
|
|
|
+ .fluentPut("responseBody", safeReadResponseBody(response))
|
|
|
+ .fluentPut("requestDate", auth.date)
|
|
|
+ .fluentPut("authorizationLength", auth.authorization == null ? 0 : auth.authorization.length()));
|
|
|
+ log.warn("clone websocket failure throwable={}, httpCode={}, responseBody={}, requestDate={}, authorizationLength={}",
|
|
|
+ t == null ? "" : StringUtils.defaultString(t.getMessage()),
|
|
|
+ response == null ? -1 : response.code(),
|
|
|
+ safeReadResponseBody(response),
|
|
|
+ auth.date,
|
|
|
+ auth.authorization == null ? 0 : auth.authorization.length());
|
|
|
+ // #endregion
|
|
|
errRef.set(buildWebsocketFailureMessage(t, response));
|
|
|
latch.countDown();
|
|
|
}
|
|
|
|
|
|
@Override
|
|
|
public void onClosed(WebSocket webSocket, int code, String reason) {
|
|
|
+ log.info("clone websocket closed code={}, reason={}", code, reason);
|
|
|
latch.countDown();
|
|
|
}
|
|
|
});
|
|
|
@@ -435,7 +497,7 @@ public class XfVoiceCloneController extends BaseController {
|
|
|
textNode.put("format", "plain");
|
|
|
textNode.put("status", 2);
|
|
|
textNode.put("seq", 0);
|
|
|
- textNode.put("text", text);
|
|
|
+ textNode.put("text", Base64.getEncoder().encodeToString(text.getBytes(StandardCharsets.UTF_8)));
|
|
|
|
|
|
JSONObject payload = new JSONObject(true);
|
|
|
payload.put("text", textNode);
|
|
|
@@ -444,7 +506,7 @@ public class XfVoiceCloneController extends BaseController {
|
|
|
}
|
|
|
|
|
|
private CloneWebsocketAuth buildCloneWebsocketAuth(String apiKey, String apiSecret) throws Exception {
|
|
|
- String date = DateTimeFormatter.RFC_1123_DATE_TIME.format(ZonedDateTime.now(ZoneId.of("GMT")));
|
|
|
+ String date = CLONE_AUTH_DATE_FORMATTER.format(ZonedDateTime.now(ZoneId.of("GMT")));
|
|
|
String signatureOrigin = "host: " + CLONE_TTS_HOST + "\n" +
|
|
|
"date: " + date + "\n" +
|
|
|
"GET " + CLONE_TTS_PATH + " HTTP/1.1";
|
|
|
@@ -457,7 +519,7 @@ public class XfVoiceCloneController extends BaseController {
|
|
|
String url = CLONE_TTS_WS_URL + "?authorization=" + urlEncode(authorization) +
|
|
|
"&date=" + urlEncode(date) +
|
|
|
"&host=" + urlEncode(CLONE_TTS_HOST);
|
|
|
- return new CloneWebsocketAuth(url, date);
|
|
|
+ return new CloneWebsocketAuth(url, date, authorization);
|
|
|
}
|
|
|
|
|
|
private String buildWebsocketFailureMessage(Throwable t, Response response) {
|
|
|
@@ -487,13 +549,73 @@ public class XfVoiceCloneController extends BaseController {
|
|
|
return sb.toString();
|
|
|
}
|
|
|
|
|
|
+ // #region debug-point D:debug-report-helper
|
|
|
+ private void reportDebugEvent(String hypothesisId, String location, String msg, JSONObject data) {
|
|
|
+ HttpURLConnection connection = null;
|
|
|
+ try {
|
|
|
+ String debugUrl = "http://127.0.0.1:7777/event";
|
|
|
+ String sessionId = "xfvoiceclone-403";
|
|
|
+ Path envPath = Paths.get(System.getProperty("user.dir"), ".dbg", "xfvoiceclone-403.env");
|
|
|
+ if (Files.exists(envPath)) {
|
|
|
+ for (String line : Files.readAllLines(envPath, StandardCharsets.UTF_8)) {
|
|
|
+ if (line.startsWith("DEBUG_SERVER_URL=")) {
|
|
|
+ debugUrl = line.substring("DEBUG_SERVER_URL=".length()).trim();
|
|
|
+ } else if (line.startsWith("DEBUG_SESSION_ID=")) {
|
|
|
+ sessionId = line.substring("DEBUG_SESSION_ID=".length()).trim();
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ JSONObject payload = new JSONObject(true);
|
|
|
+ payload.put("sessionId", sessionId);
|
|
|
+ payload.put("runId", "pre-fix");
|
|
|
+ payload.put("hypothesisId", hypothesisId);
|
|
|
+ payload.put("location", location);
|
|
|
+ payload.put("msg", msg);
|
|
|
+ payload.put("data", data == null ? new JSONObject(true) : data);
|
|
|
+ payload.put("ts", System.currentTimeMillis());
|
|
|
+
|
|
|
+ connection = (HttpURLConnection) new URL(debugUrl).openConnection();
|
|
|
+ connection.setRequestMethod("POST");
|
|
|
+ connection.setConnectTimeout(800);
|
|
|
+ connection.setReadTimeout(800);
|
|
|
+ connection.setDoOutput(true);
|
|
|
+ connection.setRequestProperty("Content-Type", "application/json");
|
|
|
+ try (OutputStream os = connection.getOutputStream()) {
|
|
|
+ os.write(payload.toJSONString().getBytes(StandardCharsets.UTF_8));
|
|
|
+ }
|
|
|
+ try (InputStream ignored = connection.getInputStream()) {
|
|
|
+ // no-op
|
|
|
+ }
|
|
|
+ } catch (Exception ignore) {
|
|
|
+ // ignore debug reporting errors
|
|
|
+ } finally {
|
|
|
+ if (connection != null) {
|
|
|
+ connection.disconnect();
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private String safeReadResponseBody(Response response) {
|
|
|
+ if (response == null || response.body() == null) {
|
|
|
+ return "";
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ return response.peekBody(4096).string();
|
|
|
+ } catch (Exception ignore) {
|
|
|
+ return "";
|
|
|
+ }
|
|
|
+ }
|
|
|
+ // #endregion
|
|
|
+
|
|
|
private static class CloneWebsocketAuth {
|
|
|
private final String url;
|
|
|
private final String date;
|
|
|
+ private final String authorization;
|
|
|
|
|
|
- private CloneWebsocketAuth(String url, String date) {
|
|
|
+ private CloneWebsocketAuth(String url, String date, String authorization) {
|
|
|
this.url = url;
|
|
|
this.date = date;
|
|
|
+ this.authorization = authorization;
|
|
|
}
|
|
|
}
|
|
|
|