|
|
@@ -0,0 +1,192 @@
|
|
|
+package com.fs.aiSoundReplication.service.impl;
|
|
|
+
|
|
|
+import com.fasterxml.jackson.databind.JsonNode;
|
|
|
+import com.fasterxml.jackson.databind.ObjectMapper;
|
|
|
+import com.fs.aiSoundReplication.config.AsrConfig;
|
|
|
+import com.fs.aiSoundReplication.service.AsrService;
|
|
|
+import lombok.RequiredArgsConstructor;
|
|
|
+import lombok.extern.slf4j.Slf4j;
|
|
|
+import okhttp3.MediaType;
|
|
|
+import okhttp3.OkHttpClient;
|
|
|
+import okhttp3.Request;
|
|
|
+import okhttp3.RequestBody;
|
|
|
+import okhttp3.Response;
|
|
|
+import org.springframework.stereotype.Service;
|
|
|
+import org.springframework.util.StringUtils;
|
|
|
+
|
|
|
+import java.util.HashMap;
|
|
|
+import java.util.Map;
|
|
|
+import java.util.UUID;
|
|
|
+
|
|
|
+/**
|
|
|
+ * 豆包大模型录音文件识别
|
|
|
+ * 状态码见官方文档
|
|
|
+ */
|
|
|
+@Service
|
|
|
+@Slf4j
|
|
|
+@RequiredArgsConstructor
|
|
|
+public class AsrServiceImpl implements AsrService {
|
|
|
+
|
|
|
+ private static final MediaType JSON = MediaType.parse("application/json; charset=utf-8");
|
|
|
+ private static final String STATUS_OK = "20000000";
|
|
|
+ private static final String STATUS_PROCESSING = "20000001";
|
|
|
+ private static final String STATUS_QUEUED = "20000002";
|
|
|
+
|
|
|
+ private final AsrConfig config;
|
|
|
+ private final OkHttpClient okHttpClient;
|
|
|
+ private final ObjectMapper objectMapper;
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public String recognizeFromUrl(String audioUrl) {
|
|
|
+ return recognizeFromUrl(audioUrl, config.getDefaultAudioFormat(), config.getDefaultLanguage());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public String recognizeFromUrl(String audioUrl, String format, String language) {
|
|
|
+ if (!config.isEnabled() || !config.hasCredentials()) {
|
|
|
+ log.warn("ASR 未启用或未配置 doubao.asr,跳过");
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ if (!StringUtils.hasText(audioUrl)) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ String fmt = StringUtils.hasText(format) ? format : config.getDefaultAudioFormat();
|
|
|
+ String taskId = UUID.randomUUID().toString();
|
|
|
+ try {
|
|
|
+ if (!submitTask(taskId, audioUrl, fmt, language)) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ return pollQuery(taskId);
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("ASR 识别异常, url={}", audioUrl, e);
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private boolean submitTask(String taskId, String audioUrl, String format, String language) throws Exception {
|
|
|
+ Map<String, Object> user = new HashMap<>();
|
|
|
+ user.put("uid", config.getUid());
|
|
|
+
|
|
|
+ Map<String, Object> audio = new HashMap<>();
|
|
|
+ audio.put("url", audioUrl);
|
|
|
+ audio.put("format", format);
|
|
|
+ if (StringUtils.hasText(language)) {
|
|
|
+ audio.put("language", language);
|
|
|
+ }
|
|
|
+
|
|
|
+ Map<String, Object> req = new HashMap<>();
|
|
|
+ req.put("model_name", "bigmodel");
|
|
|
+ req.put("enable_itn", config.isEnableItn());
|
|
|
+ req.put("enable_punc", config.isEnablePunc());
|
|
|
+
|
|
|
+ Map<String, Object> body = new HashMap<>();
|
|
|
+ body.put("user", user);
|
|
|
+ body.put("audio", audio);
|
|
|
+ body.put("request", req);
|
|
|
+
|
|
|
+ String json = objectMapper.writeValueAsString(body);
|
|
|
+ Request.Builder builder = new Request.Builder()
|
|
|
+ .url(config.getSubmitUrl())
|
|
|
+ .post(RequestBody.create(json, JSON))
|
|
|
+ .addHeader("Content-Type", "application/json");
|
|
|
+
|
|
|
+ applySubmitHeaders(builder, taskId);
|
|
|
+
|
|
|
+ try (Response response = okHttpClient.newCall(builder.build()).execute()) {
|
|
|
+ String code = headerStatus(response, "X-Api-Status-Code");
|
|
|
+ if (STATUS_OK.equals(code)) {
|
|
|
+ log.info("ASR submit 成功, taskId={}, logId={}", taskId, response.header("X-Tt-Logid"));
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ log.error("ASR submit 失败, taskId={}, status={}, message={}, body={}",
|
|
|
+ taskId, code, response.header("X-Api-Message"), bodyString(response));
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private String pollQuery(String taskId) throws Exception {
|
|
|
+ Request.Builder builder = new Request.Builder()
|
|
|
+ .url(config.getQueryUrl())
|
|
|
+ .post(RequestBody.create("{}", JSON))
|
|
|
+ .addHeader("Content-Type", "application/json");
|
|
|
+
|
|
|
+ applyQueryHeaders(builder, taskId);
|
|
|
+
|
|
|
+ Request request = builder.build();
|
|
|
+ for (int i = 0; i < config.getMaxQueryAttempts(); i++) {
|
|
|
+ try (Response response = okHttpClient.newCall(request).execute()) {
|
|
|
+ String code = headerStatus(response, "X-Api-Status-Code");
|
|
|
+ String bodyStr = bodyString(response);
|
|
|
+
|
|
|
+ if (STATUS_OK.equals(code)) {
|
|
|
+ String text = extractText(bodyStr);
|
|
|
+ if (StringUtils.hasText(text)) {
|
|
|
+ return text.trim();
|
|
|
+ }
|
|
|
+ log.warn("ASR query 成功但 result.text 为空, taskId={}", taskId);
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ if (STATUS_PROCESSING.equals(code) || STATUS_QUEUED.equals(code)) {
|
|
|
+ Thread.sleep(config.getPollIntervalMs());
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ log.error("ASR query 失败, taskId={}, status={}, message={}, body={}",
|
|
|
+ taskId, code, response.header("X-Api-Message"), bodyStr);
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ log.error("ASR query 超时, taskId={}, attempts={}", taskId, config.getMaxQueryAttempts());
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ private void applySubmitHeaders(Request.Builder builder, String taskId) {
|
|
|
+ builder.addHeader("X-Api-Resource-Id", config.getResourceId());
|
|
|
+ builder.addHeader("X-Api-Request-Id", taskId);
|
|
|
+ if (config.useNewConsoleApiKey()) {
|
|
|
+ builder.addHeader("X-Api-Key", config.getApiKey());
|
|
|
+ builder.addHeader("X-Api-Sequence", "-1");
|
|
|
+ } else {
|
|
|
+ builder.addHeader("X-Api-App-Key", config.getAppKey());
|
|
|
+ builder.addHeader("X-Api-Access-Key", config.getAccessToken());
|
|
|
+ builder.addHeader("X-Api-Sequence", "-1");
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private void applyQueryHeaders(Request.Builder builder, String taskId) {
|
|
|
+ builder.addHeader("X-Api-Resource-Id", config.getResourceId());
|
|
|
+ builder.addHeader("X-Api-Request-Id", taskId);
|
|
|
+ if (config.useNewConsoleApiKey()) {
|
|
|
+ builder.addHeader("X-Api-Key", config.getApiKey());
|
|
|
+ } else {
|
|
|
+ builder.addHeader("X-Api-App-Key", config.getAppKey());
|
|
|
+ builder.addHeader("X-Api-Access-Key", config.getAccessToken());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private static String headerStatus(Response response, String name) {
|
|
|
+ String v = response.header(name);
|
|
|
+ if (v == null) {
|
|
|
+ v = response.header(name.toLowerCase());
|
|
|
+ }
|
|
|
+ return v != null ? v.trim() : "";
|
|
|
+ }
|
|
|
+
|
|
|
+ private static String bodyString(Response response) throws java.io.IOException {
|
|
|
+ if (response.body() == null) {
|
|
|
+ return "";
|
|
|
+ }
|
|
|
+ return response.body().string();
|
|
|
+ }
|
|
|
+
|
|
|
+ private String extractText(String json) throws Exception {
|
|
|
+ if (!StringUtils.hasText(json)) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ JsonNode root = objectMapper.readTree(json);
|
|
|
+ JsonNode text = root.path("result").path("text");
|
|
|
+ if (text.isMissingNode() || text.isNull()) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ return text.asText();
|
|
|
+ }
|
|
|
+}
|