MediaAnalysisServiceImpl.java 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502
  1. package com.njuzr.eaibackend.service.impl;
  2. import cn.xfyun.api.IseClient;
  3. import cn.xfyun.model.response.ise.IseResponseData;
  4. import cn.xfyun.service.ise.AbstractIseWebSocketListener;
  5. import com.alibaba.fastjson2.JSONObject;
  6. import com.fasterxml.jackson.core.JsonProcessingException;
  7. import com.fasterxml.jackson.databind.ObjectMapper;
  8. import com.njuzr.eaibackend.config.DoubaoConfig;
  9. import com.njuzr.eaibackend.config.IseClientFactory;
  10. import com.njuzr.eaibackend.exception.MyException;
  11. import com.njuzr.eaibackend.po.SpeakingAIDialogue;
  12. import com.njuzr.eaibackend.service.MediaAnalysisService;
  13. import com.njuzr.eaibackend.utils.XmlParasUtil;
  14. import com.njuzr.eaibackend.vo.VideoAnalysisVo;
  15. import com.volcengine.ark.runtime.model.completion.chat.ChatCompletionRequest;
  16. import com.volcengine.ark.runtime.model.completion.chat.ChatMessage;
  17. import com.volcengine.ark.runtime.model.completion.chat.ChatMessageRole;
  18. import lombok.extern.slf4j.Slf4j;
  19. import okhttp3.*;
  20. import org.springframework.beans.factory.annotation.Autowired;
  21. import org.springframework.beans.factory.annotation.Qualifier;
  22. import org.springframework.data.mongodb.core.MongoTemplate;
  23. import org.springframework.data.mongodb.core.query.Criteria;
  24. import org.springframework.data.mongodb.core.query.Query;
  25. import org.springframework.data.mongodb.core.query.Update;
  26. import org.springframework.http.HttpStatus;
  27. import org.springframework.scheduling.annotation.Async;
  28. import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
  29. import org.springframework.stereotype.Service;
  30. import java.io.IOException;
  31. import java.io.InputStream;
  32. import java.net.URL;
  33. import java.net.URLConnection;
  34. import java.security.SignatureException;
  35. import java.util.*;
  36. import java.util.concurrent.*;
  37. /**
  38. * @author Liululin
  39. * @date 2025/3/13 - 15:04
  40. */
  41. @Slf4j
  42. @Service
  43. public class MediaAnalysisServiceImpl implements MediaAnalysisService {
  44. private static final String VIDEO_URL = "http://8.130.28.19:2002/deepface";
  45. private static final String AUDIO_URL = "http://8.130.28.19:2003/asr";
  46. private static final HashSet<String> emotionSet = new HashSet<>();
  47. private static final Base64.Decoder decoder = Base64.getDecoder();
  48. private static final long DEFAULT_TIMEOUT_SECONDS = 30;
  49. private static final long VIDEO_ANALYSIS_TIMEOUT_SECONDS = 600;
  50. private static final String AUDIO_SCORE_HEADER = "[audio-score]";
  51. private static final String AUDIO_SENTIMENT_HEADER = "[audio-sentiment]";
  52. private static final String VIDEO_SENTIMENT_HEADER = "[video-sentiment]";
  53. private static final String SPEAKING_SUGGESTION_HEADER = "[speaking-suggestion]";
  54. static {
  55. emotionSet.add("😊");
  56. emotionSet.add("😔");
  57. emotionSet.add("😡");
  58. emotionSet.add("😰");
  59. emotionSet.add("🤢");
  60. emotionSet.add("😮");
  61. }
  62. @Autowired
  63. DoubaoConfig doubaoConfig;
  64. @Autowired
  65. MongoTemplate mongoTemplate;
  66. @Autowired
  67. IseClientFactory iseClientFactory;
  68. @Autowired
  69. @Qualifier("doubaoTaskExecutor") // 指定 Bean 名称
  70. private ThreadPoolTaskExecutor doubaoTaskExecutor; // 注入线程池
  71. private final Map<String, CompletableFuture<String>> pendingSuggestionRequests = new ConcurrentHashMap<>();
  72. private final Map<String, CompletableFuture<VideoAnalysisVo>> pendingVideoAnalysisRequests = new ConcurrentHashMap<>();
  73. private final OkHttpClient httpClient = new OkHttpClient.Builder()
  74. .connectTimeout(20, TimeUnit.SECONDS)
  75. .readTimeout(600, TimeUnit.SECONDS)
  76. .writeTimeout(300, TimeUnit.SECONDS)
  77. .build();
  78. @Override
  79. public CompletableFuture<SpeakingAIDialogue.DialogueEntry> getAudioScore(Long userId, Long assignmentId, Integer index) {
  80. log.info("{} 开始获取音频评分, userId: {}, assignmentId: {}, index: {}",
  81. AUDIO_SCORE_HEADER, userId, assignmentId, index);
  82. Query query = new Query(Criteria.where("userId").is(userId).and("assignmentId").is(assignmentId));
  83. SpeakingAIDialogue dialogue = mongoTemplate.findOne(query, SpeakingAIDialogue.class);
  84. if (dialogue == null) {
  85. log.error("{} 未找到对应的对话记录, userId: {}, assignmentId: {}",
  86. AUDIO_SCORE_HEADER, userId, assignmentId);
  87. throw new MyException(HttpStatus.BAD_REQUEST.value(), "不存在相应记录");
  88. }
  89. if (index == -1) {
  90. index = Math.max(dialogue.getDialogues().size() - 2, 0);
  91. }
  92. SpeakingAIDialogue.DialogueEntry entry = dialogue.getDialogues().get(index);
  93. Integer finalIndex = index;
  94. return CompletableFuture.supplyAsync(() -> {
  95. try {
  96. log.debug("{} 开始处理音频评分, 音频URL: {}", AUDIO_SCORE_HEADER, entry.getAudioUrl());
  97. URL url = new URL(entry.getAudioUrl());
  98. URLConnection connection = url.openConnection();
  99. InputStream inputStream = connection.getInputStream();
  100. IseClient client = iseClientFactory.getIseClient(entry.getContent());
  101. CompletableFuture<SpeakingAIDialogue.DialogueEntry> future = new CompletableFuture<>();
  102. client.send(inputStream, new AbstractIseWebSocketListener() {
  103. @Override
  104. public void onSuccess(WebSocket webSocket, IseResponseData iseResponseData) {
  105. try {
  106. String decodedData = new String(decoder.decode(iseResponseData.getData().getData()));
  107. XmlParasUtil.paras(decodedData, entry);
  108. updateDialogueEntry(userId, assignmentId, finalIndex, entry);
  109. log.info("{} 音频评分处理成功, userId: {}, assignmentId: {}, index: {}",
  110. AUDIO_SCORE_HEADER, userId, assignmentId, finalIndex);
  111. future.complete(entry);
  112. } catch (Exception e) {
  113. log.error("{} 音频评分数据处理异常", AUDIO_SCORE_HEADER, e);
  114. future.completeExceptionally(e);
  115. } finally {
  116. client.closeWebsocket();
  117. }
  118. }
  119. @Override
  120. public void onFail(WebSocket webSocket, Throwable throwable, Response response) {
  121. try {
  122. log.error("{} ISE服务调用失败, 响应: {}, 错误: {}",
  123. AUDIO_SCORE_HEADER, response, throwable.getMessage());
  124. future.completeExceptionally(throwable);
  125. } finally {
  126. client.closeWebsocket();
  127. }
  128. }
  129. });
  130. return future.get();
  131. } catch (IOException | SignatureException e) {
  132. log.error("{} 音频评分处理IO异常", AUDIO_SCORE_HEADER, e);
  133. throw new MyException(HttpStatus.INTERNAL_SERVER_ERROR.value(), "ISEService调用失败:" + e.getMessage());
  134. } catch (InterruptedException | ExecutionException e) {
  135. log.error("{} 音频评分处理中断异常", AUDIO_SCORE_HEADER, e);
  136. Thread.currentThread().interrupt();
  137. throw new MyException(HttpStatus.INTERNAL_SERVER_ERROR.value(), "音频评分处理中断");
  138. }
  139. }, doubaoTaskExecutor);
  140. }
  141. @Async("doubaoTaskExecutor")
  142. public CompletableFuture<String> getAudioSentimentAsync(Long userId, Long assignmentId, Integer index) {
  143. return CompletableFuture.supplyAsync(() -> getAudioSentiment(userId, assignmentId, index), doubaoTaskExecutor);
  144. }
  145. public String getAudioSentiment(Long userId, Long assignmentId, Integer index) {
  146. log.info("{} 开始获取音频情感分析, userId: {}, assignmentId: {}, index: {}",
  147. AUDIO_SENTIMENT_HEADER, userId, assignmentId, index);
  148. Query query = new Query(Criteria.where("userId").is(userId).and("assignmentId").is(assignmentId));
  149. SpeakingAIDialogue dialogue = mongoTemplate.findOne(query, SpeakingAIDialogue.class);
  150. if (dialogue == null) {
  151. log.error("{} 未找到对话记录, userId: {}, assignmentId: {}",
  152. AUDIO_SENTIMENT_HEADER, userId, assignmentId);
  153. throw new MyException(HttpStatus.BAD_REQUEST.value(), "不存在相应记录");
  154. }
  155. if (index == -1) {
  156. index = Math.max(dialogue.getDialogues().size() - 2, 0);
  157. log.debug("{} 自动计算index: {}", AUDIO_SENTIMENT_HEADER, index);
  158. }
  159. SpeakingAIDialogue.DialogueEntry entry = dialogue.getDialogues().get(index);
  160. JSONObject json = new JSONObject();
  161. json.put("url", entry.getAudioUrl());
  162. RequestBody body = RequestBody.create(MediaType.get("application/json; charset=utf-8"), json.toString());
  163. Request request = new Request.Builder()
  164. .url(AUDIO_URL)
  165. .post(body)
  166. .build();
  167. try (Response response = httpClient.newCall(request).execute()) {
  168. if (!response.isSuccessful()) {
  169. log.error("{} 音频情感分析请求失败, 状态码: {}, 响应: {}",
  170. AUDIO_SENTIMENT_HEADER, response.code(), response.body().string());
  171. throw new IOException("Unexpected code " + response);
  172. }
  173. String res = response.body().string();
  174. log.debug("{} 音频情感分析原始响应: {}", AUDIO_SENTIMENT_HEADER, res);
  175. int endIndex = res.lastIndexOf("\"");
  176. String emotion = res.substring(endIndex - 2, endIndex);
  177. if (!emotionSet.contains(emotion)) {
  178. emotion = "😐";
  179. log.warn("{} 未识别的表情符号, 使用默认表情", AUDIO_SENTIMENT_HEADER);
  180. }
  181. entry.setAudioSentiment(emotion);
  182. updateDialogueEntry(userId, assignmentId, index, entry);
  183. log.info("{} 音频情感分析完成, 结果: {}", AUDIO_SENTIMENT_HEADER, emotion);
  184. return emotion;
  185. } catch (IOException e) {
  186. log.error("{} 音频情感分析IO异常", AUDIO_SENTIMENT_HEADER, e);
  187. throw new MyException(HttpStatus.BAD_REQUEST.value(), "情感识别失败: " + e.getMessage());
  188. }
  189. }
  190. @Override
  191. public String getAudioSentimentWithTimeout(Long userId, Long assignmentId, Integer index) {
  192. log.info("{} 开始带超时的音频情感分析, userId: {}, assignmentId: {}, index: {}",
  193. AUDIO_SENTIMENT_HEADER, userId, assignmentId, index);
  194. CompletableFuture<String> future = getAudioSentimentAsync(userId, assignmentId, index);
  195. try {
  196. return future.get(DEFAULT_TIMEOUT_SECONDS, TimeUnit.SECONDS);
  197. } catch (TimeoutException e) {
  198. log.error("{} 音频情感分析请求超时", AUDIO_SENTIMENT_HEADER, e);
  199. throw new MyException(HttpStatus.REQUEST_TIMEOUT.value(), "请求超时");
  200. } catch (Exception e) {
  201. log.error("{} 音频情感分析异常", AUDIO_SENTIMENT_HEADER, e);
  202. throw new MyException(HttpStatus.BAD_REQUEST.value(), "情感识别失败: " + e.getMessage());
  203. }
  204. }
  205. @Async("doubaoTaskExecutor")
  206. public CompletableFuture<String> getSpeakingSuggestionAsync(Long userId, Long assignmentId) {
  207. return CompletableFuture.supplyAsync(() -> getSpeakingSuggestion(userId, assignmentId), doubaoTaskExecutor);
  208. }
  209. public String getSpeakingSuggestion(Long userId, Long assignmentId) {
  210. String requestKey = userId + "-" + assignmentId + "-suggestion";
  211. log.info("{} 开始获取口语建议, userId: {}, assignmentId: {}",
  212. SPEAKING_SUGGESTION_HEADER, userId, assignmentId);
  213. // 检查是否有相同的请求正在进行
  214. CompletableFuture<String> existingFuture = pendingSuggestionRequests.get(requestKey);
  215. if (existingFuture != null) {
  216. log.debug("{} 发现重复请求, 等待已有请求完成", SPEAKING_SUGGESTION_HEADER);
  217. try {
  218. return existingFuture.get();
  219. } catch (Exception e) {
  220. pendingSuggestionRequests.remove(requestKey);
  221. log.error("{} 等待重复请求时发生异常", SPEAKING_SUGGESTION_HEADER, e);
  222. throw new MyException(HttpStatus.BAD_REQUEST.value(), "请求次数过多,请稍后再试");
  223. }
  224. }
  225. SpeakingAIDialogue existingDialogue = mongoTemplate.findOne(
  226. Query.query(Criteria.where("assignmentId").is(assignmentId).and("userId").is(userId)),
  227. SpeakingAIDialogue.class
  228. );
  229. if (existingDialogue == null) {
  230. log.error("{} 未找到对话记录, userId: {}, assignmentId: {}",
  231. SPEAKING_SUGGESTION_HEADER, userId, assignmentId);
  232. throw new MyException(HttpStatus.BAD_REQUEST.value(), "不存在相应记录");
  233. }
  234. if (existingDialogue.getSuggestion() != null && existingDialogue.getSuggestion().length() != 0) {
  235. log.debug("{} 返回已存在的建议", SPEAKING_SUGGESTION_HEADER);
  236. return existingDialogue.getSuggestion();
  237. }
  238. // 创建新的Future并放入缓存
  239. CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
  240. try {
  241. String message = buildSuggestionMessage(existingDialogue);
  242. log.debug("{} 构建的建议请求消息: {}", SPEAKING_SUGGESTION_HEADER, message);
  243. ChatMessage chatMessage = ChatMessage.builder()
  244. .role(ChatMessageRole.USER)
  245. .content(message)
  246. .build();
  247. List<ChatMessage> list = Collections.singletonList(chatMessage);
  248. ChatCompletionRequest chatCompletionRequest = ChatCompletionRequest.builder()
  249. .model(doubaoConfig.getEndpointId())
  250. .messages(list)
  251. .build();
  252. String response = (String) doubaoConfig.arkService()
  253. .createChatCompletion(chatCompletionRequest)
  254. .getChoices().get(0).getMessage().getContent();
  255. log.info("{} 获取到口语建议响应: {}", SPEAKING_SUGGESTION_HEADER, response);
  256. existingDialogue.setSuggestion(response);
  257. mongoTemplate.save(existingDialogue);
  258. return response;
  259. } catch (Exception e) {
  260. log.error("{} 获取口语建议异常", SPEAKING_SUGGESTION_HEADER, e);
  261. throw new RuntimeException("获取建议失败: " + e.getMessage());
  262. } finally {
  263. pendingSuggestionRequests.remove(requestKey);
  264. }
  265. }, doubaoTaskExecutor);
  266. pendingSuggestionRequests.put(requestKey, future);
  267. try {
  268. return future.get();
  269. } catch (Exception e) {
  270. pendingSuggestionRequests.remove(requestKey);
  271. log.error("{} 处理口语建议时异常", SPEAKING_SUGGESTION_HEADER, e);
  272. throw new MyException(HttpStatus.BAD_REQUEST.value(), "获取建议失败: " + e.getMessage());
  273. }
  274. }
  275. @Override
  276. public String getSpeakingSuggestionWithTimeout(Long userId, Long assignmentId) {
  277. log.info("{} 开始带超时的口语建议获取, userId: {}, assignmentId: {}",
  278. SPEAKING_SUGGESTION_HEADER, userId, assignmentId);
  279. CompletableFuture<String> future = getSpeakingSuggestionAsync(userId, assignmentId);
  280. try {
  281. return future.get(DEFAULT_TIMEOUT_SECONDS, TimeUnit.SECONDS);
  282. } catch (TimeoutException e) {
  283. log.error("{} 获取口语建议请求超时", SPEAKING_SUGGESTION_HEADER, e);
  284. throw new MyException(HttpStatus.REQUEST_TIMEOUT.value(), "获取建议请求超时");
  285. } catch (Exception e) {
  286. log.error("{} 获取口语建议异常", SPEAKING_SUGGESTION_HEADER, e);
  287. throw new MyException(HttpStatus.BAD_REQUEST.value(), "获取建议失败: " + e.getMessage());
  288. }
  289. }
  290. public VideoAnalysisVo getVideoSentiment(Long userId, Long assignmentId) {
  291. String requestKey = userId + "-" + assignmentId + "-video-sentiment";
  292. log.info("{} 开始获取视频情感分析, userId: {}, assignmentId: {}",
  293. VIDEO_SENTIMENT_HEADER, userId, assignmentId);
  294. // 检查是否有相同的请求正在进行
  295. CompletableFuture<VideoAnalysisVo> existingFuture = pendingVideoAnalysisRequests.get(requestKey);
  296. if (existingFuture != null) {
  297. log.debug("{} 发现重复视频分析请求, 等待已有请求完成", VIDEO_SENTIMENT_HEADER);
  298. try {
  299. return existingFuture.get(VIDEO_ANALYSIS_TIMEOUT_SECONDS, TimeUnit.SECONDS);
  300. } catch (Exception e) {
  301. pendingVideoAnalysisRequests.remove(requestKey);
  302. log.error("{} 等待重复视频分析请求时异常", VIDEO_SENTIMENT_HEADER, e);
  303. throw new MyException(HttpStatus.BAD_REQUEST.value(), "请求次数过多,请稍后再试");
  304. }
  305. }
  306. Query query = new Query(Criteria.where("userId").is(userId).and("assignmentId").is(assignmentId));
  307. SpeakingAIDialogue dialogue = mongoTemplate.findOne(query, SpeakingAIDialogue.class);
  308. if (dialogue == null) {
  309. log.error("{} 未找到对话记录, userId: {}, assignmentId: {}",
  310. VIDEO_SENTIMENT_HEADER, userId, assignmentId);
  311. throw new MyException(HttpStatus.BAD_REQUEST.value(), "不存在相应记录");
  312. }
  313. if (dialogue.getVideoAnalysis() != null && dialogue.getVideoAnalysis().getNewAudioUrl() != null) {
  314. log.debug("{} 返回已存在的视频分析结果", VIDEO_SENTIMENT_HEADER);
  315. return dialogue.getVideoAnalysis();
  316. }
  317. // 提交任务到线程池
  318. CompletableFuture<VideoAnalysisVo> future = CompletableFuture.supplyAsync(() -> {
  319. try {
  320. VideoAnalysisVo result = sendRequestToDeepface(dialogue.getAudioSigment(), dialogue);
  321. log.info("{} 视频情感分析完成, 结果: {}", VIDEO_SENTIMENT_HEADER, result);
  322. return result;
  323. } catch (IOException e) {
  324. log.error("{} 视频情感分析IO异常", VIDEO_SENTIMENT_HEADER, e);
  325. throw new RuntimeException("视频情感识别失败: " + e.getMessage());
  326. } finally {
  327. pendingVideoAnalysisRequests.remove(requestKey);
  328. }
  329. }, doubaoTaskExecutor);
  330. pendingVideoAnalysisRequests.put(requestKey, future);
  331. try {
  332. return future.get(VIDEO_ANALYSIS_TIMEOUT_SECONDS, TimeUnit.SECONDS);
  333. } catch (TimeoutException e) {
  334. log.error("{} 视频情感分析请求超时", VIDEO_SENTIMENT_HEADER, e);
  335. throw new MyException(HttpStatus.REQUEST_TIMEOUT.value(), "请求超时");
  336. } catch (Exception e) {
  337. pendingVideoAnalysisRequests.remove(requestKey);
  338. log.error("{} 视频情感分析异常", VIDEO_SENTIMENT_HEADER, e);
  339. throw new MyException(HttpStatus.BAD_REQUEST.value(), "情感识别失败: " + e.getMessage());
  340. }
  341. }
  342. private VideoAnalysisVo sendRequestToDeepface(List<String> audioSegment, SpeakingAIDialogue dialogue) throws IOException {
  343. log.debug("{} 开始发送视频分析请求, 音频片段数量: {}", VIDEO_SENTIMENT_HEADER, audioSegment.size());
  344. JSONObject json = new JSONObject();
  345. json.put("video_urls", audioSegment);
  346. RequestBody body = RequestBody.create(MediaType.get("application/json; charset=utf-8"), json.toString());
  347. Request request = new Request.Builder()
  348. .url(VIDEO_URL)
  349. .post(body)
  350. .build();
  351. try (Response response = httpClient.newCall(request).execute()) {
  352. if (!response.isSuccessful()) {
  353. String errorBody = response.body() != null ? response.body().string() : "empty body";
  354. log.error("{} 视频分析请求失败, 状态码: {}, 响应: {}",
  355. VIDEO_SENTIMENT_HEADER, response.code(), errorBody);
  356. throw new IOException("Unexpected code " + response);
  357. }
  358. String res = response.body().string();
  359. log.debug("{} 视频分析原始响应: {}", VIDEO_SENTIMENT_HEADER, res);
  360. VideoAnalysisVo videoAnalysisVo = parseResponse(res);
  361. dialogue.setVideoAnalysis(videoAnalysisVo);
  362. mongoTemplate.save(dialogue);
  363. return videoAnalysisVo;
  364. }
  365. }
  366. private VideoAnalysisVo parseResponse(String response) throws JsonProcessingException {
  367. try {
  368. ObjectMapper objectMapper = new ObjectMapper();
  369. Map<String, Object> jsonMap = objectMapper.readValue(response, Map.class);
  370. VideoAnalysisVo videoAnalysisVo = new VideoAnalysisVo();
  371. videoAnalysisVo.setNewAudioUrl((String) jsonMap.get("merged_video_path"));
  372. Map<String, Integer> emotionPercentageDict = (Map<String, Integer>) jsonMap.get("emotion_percentage_dict");
  373. List<VideoAnalysisVo.EmotionPercentage> emotionPercentageList = new ArrayList<>();
  374. for (Map.Entry<String, Integer> entry : emotionPercentageDict.entrySet()) {
  375. VideoAnalysisVo.EmotionPercentage emotionPercentage = new VideoAnalysisVo.EmotionPercentage();
  376. emotionPercentage.setEmotion(entry.getKey());
  377. emotionPercentage.setPercentage(entry.getValue().floatValue());
  378. emotionPercentageList.add(emotionPercentage);
  379. }
  380. videoAnalysisVo.setEmotionPercentageList(emotionPercentageList);
  381. return videoAnalysisVo;
  382. } catch (Exception e) {
  383. log.error("{} 解析视频分析响应异常, 响应内容: {}", VIDEO_SENTIMENT_HEADER, response, e);
  384. throw e;
  385. }
  386. }
  387. private void updateDialogueEntry(Long userId, Long assignmentId, int index, SpeakingAIDialogue.DialogueEntry updatedEntry) {
  388. try {
  389. Query query = new Query(Criteria.where("userId").is(userId).and("assignmentId").is(assignmentId));
  390. Update update = new Update().set("dialogues." + index, updatedEntry);
  391. mongoTemplate.updateFirst(query, update, SpeakingAIDialogue.class);
  392. log.debug("{} 成功更新对话条目, userId: {}, assignmentId: {}, index: {}",
  393. AUDIO_SCORE_HEADER, userId, assignmentId, index);
  394. } catch (Exception e) {
  395. log.error("{} 更新对话条目失败", AUDIO_SCORE_HEADER, e);
  396. throw new MyException(HttpStatus.INTERNAL_SERVER_ERROR.value(), "更新对话条目失败");
  397. }
  398. }
  399. private String buildSuggestionMessage(SpeakingAIDialogue dialogue) {
  400. return "你是一位英语助教老师,下面是一位学生口语练习时的口语记录和口语得分," + dialogue
  401. + "\n请严格按照以下格式返回详细且实用的口语和交流建议:"
  402. + "1. 语法错误:[评估句法复杂性与准确性,提出修改建议,提供修改后的样本,提供提升建议]\n"
  403. + "2. 主题相关性:[分析对话内容与主题的关联程度,是否紧扣主题,有无偏离主题的情况,提出改进方向]\n"
  404. + "3. 逻辑表现情况:[判断对话逻辑是否清晰,过渡是否自然,给出优化逻辑的建议]\n"
  405. + "4. 词汇量:[评估说话人的词汇量、词汇复杂性、词汇准确性、词汇搭配,提供修改建议,提供修改后的样本,提供提升建议]\n"
  406. + "5. 语用能力评估和反馈:[根据语境、身份等评估说话人话语在内容和语言表达上的恰当性,提供修改建议,提供修改后的样本,提出提升建议]"
  407. + "6. 发音准确性:"
  408. + "- 具体表现:详细描述学生的发音准确性,例如哪些单词或音标发音正确(如元音、辅音、连读等),哪些发音存在问题(如常见的 /θ/、/ð/、/r/ 等音标)。"
  409. + "- 改进建议:提供具体的练习方法,例如:"
  410. + " - 针对错误发音,推荐使用音标练习工具(如 IPA 图表)或模仿母语者的发音视频。"
  411. + " - 练习单词时,建议使用“慢速-正常速”对比法,逐步纠正发音。"
  412. + " - 对于连读和弱读问题,推荐练习常见的连读规则(如 \"want to\" → \"wanna\")。"
  413. + "7. 对话流利度:"
  414. + "- 具体表现:评估学生的流利度,例如是否存在长时间停顿、重复、自我纠正或语法错误导致的卡顿。"
  415. + "- 改进建议:提供实用的提升方法"
  416. + "8. 语音情绪分析:"
  417. + "- 具体表现:分析学生在对话中的情绪表达,例如是否表现出自信、紧张、平淡或热情。"
  418. + "- 改进建议:提供情绪表达的优化建议,例如:"
  419. + " - 如果学生语气平淡,建议练习通过语调变化(如升调、降调)增强表达力。"
  420. + " - 如果学生表现出紧张,建议通过深呼吸和放松练习缓解压力,同时多进行模拟对话以增强信心。"
  421. + " - 鼓励学生在对话中加入情感词汇(如 \"I'm really excited about...\" 或 \"I feel a bit unsure about...\")以增强情感表达。";
  422. }
  423. }