Pārlūkot izejas kodu

加入诗词朗读接口

liyanbo 1 mēnesi atpakaļ
vecāks
revīzija
8309113441

+ 7 - 1
byzs-module-ai/src/main/java/cn/iocoder/byzs/module/ai/controller/admin/vision/vo/AiVisionThinkReqVO.java

@@ -16,10 +16,16 @@ public class AiVisionThinkReqVO implements VO {
     @NotNull(message = "模型编号不能为空")
     private Long modelId;
 
+//    @NotNull(message = "角色编号不能为空")
+    private Long roleId;
+
     @NotEmpty(message = "提示词不能为空")
     @Size(max = 1200, message = "提示词最大 1200")
     private String prompt;
 
-    @NotNull(message = "图片列表")
+//    @NotNull(message = "图片列表")
     private List<String> promptImage;
+
+//    @NotNull(message = "音频路径")
+    private String audioUrl;
 }

+ 6 - 0
byzs-module-ai/src/main/java/cn/iocoder/byzs/module/ai/controller/admin/vision/vo/AiVisionThinkRespVO.java

@@ -22,4 +22,10 @@ public class AiVisionThinkRespVO implements VO {
      */
     @Schema(description = "返回结果", example = "识别到图片内容为:一只黑色的猫")
     private String result;
+
+    /**
+     * Base64编码的音频数据
+     */
+    @Schema(description = "Base64编码的音频数据")
+    private String audioData;
 }

+ 67 - 1
byzs-web/src/main/java/cn/iocoder/byzs/module/web/controller/admin/ai/WebAiController.java

@@ -2,6 +2,7 @@ package cn.iocoder.byzs.module.web.controller.admin.ai;
 
 import cn.hutool.core.util.ObjUtil;
 import cn.iocoder.byzs.framework.common.enums.CommonStatusEnum;
+import cn.iocoder.byzs.framework.common.exception.enums.GlobalErrorCodeConstants;
 import cn.iocoder.byzs.framework.common.pojo.CommonResult;
 import cn.iocoder.byzs.framework.common.pojo.PageParam;
 import cn.iocoder.byzs.framework.common.pojo.PageResult;
@@ -41,6 +42,7 @@ import cn.iocoder.byzs.module.web.controller.admin.ai.vo.TtsVo;
 import cn.iocoder.byzs.module.web.controller.admin.ai.vo.WebAiChatRoleVO;
 import cn.iocoder.byzs.module.web.service.ai.WebAiServiceImpl;
 import com.volcengine.ark.runtime.model.responses.constant.ResponsesConstants;
+import com.volcengine.ark.runtime.model.responses.content.InputContentItemAudio;
 import com.volcengine.ark.runtime.model.responses.content.InputContentItemImage;
 import com.volcengine.ark.runtime.model.responses.content.InputContentItemText;
 import com.volcengine.ark.runtime.model.responses.content.OutputContentItemText;
@@ -60,10 +62,16 @@ import jakarta.annotation.security.PermitAll;
 import jakarta.validation.Valid;
 import jodd.util.StringUtil;
 import org.springframework.http.MediaType;
+import reactor.core.publisher.Flux;
+
+import java.net.URL;
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+import java.util.Base64;
+import java.util.concurrent.TimeUnit;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.validation.annotation.Validated;
 import org.springframework.web.bind.annotation.*;
-import reactor.core.publisher.Flux;
 
 import java.util.List;
 import java.util.Optional;
@@ -224,6 +232,9 @@ public class WebAiController {
         ArkService arkService = ArkService.builder().apiKey(apiKey).baseUrl("https://ark.cn-beijing.volces.com/api/v3").build();
 
         List<String> promptImages = visionThinkReqVO.getPromptImage();
+        if (promptImages == null || promptImages.isEmpty()) {
+
+        }
         String prompt = visionThinkReqVO.getPrompt();
 
         // 构建消息内容
@@ -297,6 +308,61 @@ public class WebAiController {
         return "";
     }
 
+    // ================ 朗读评价 ================
+
+    @Operation(summary = "朗读评价", description = "前端直接传入Base64音频数据,后端直接调用AI模型评价")
+    @PostMapping("/reading-evaluate")
+    public CommonResult<AiVisionThinkRespVO> evaluateReading(@Valid @RequestBody AiVisionThinkReqVO visionThinkReqVO) {
+        String apiKey = "4bb6060f-b4a1-4e0a-bf7b-71dcaa6311cb";
+        ArkService arkService = ArkService.builder().apiKey(apiKey).baseUrl("https://ark.cn-beijing.volces.com/api/v3").build();
+
+        try {
+            // 前端已完成录音并转Base64,直接获取音频数据
+            String base64Audio = visionThinkReqVO.getAudioUrl();
+
+            // 调用AI模型进行评价
+            CreateResponsesRequest request = CreateResponsesRequest.builder()
+                    .model("doubao-seed-2-0-lite-260428")
+                    .input(ResponsesInput.builder()
+                            .addListItem(ItemEasyMessage.builder()
+                                    .role(ResponsesConstants.MESSAGE_ROLE_SYSTEM)
+                                    .content(MessageContent.builder()
+                                            .addListItem(InputContentItemText.builder()
+                                                    .text("你是小学老师,接下来你要评价学生朗读诗词语句并打分,请用小学生的年级水平来评价,回复字数尽量控制在20字以内")
+                                                    .build())
+                                            .build())
+                                    .build())
+                            .addListItem(ItemEasyMessage.builder()
+                                    .role(ResponsesConstants.MESSAGE_ROLE_USER)
+                                    .content(MessageContent.builder()
+                                            .addListItem(InputContentItemAudio.builder().audioUrl(base64Audio).build())
+                                            .addListItem(InputContentItemText.builder().text("请评价这段朗读").build())
+                                            .build())
+                                    .build())
+                            .build())
+                    .build();
+
+            ResponseObject resp = arkService.createResponse(request);
+            String text = extractTextFromResponse(resp);
+
+            AiVisionThinkRespVO response = new AiVisionThinkRespVO();
+            response.setResult(text);
+
+            // 调用 TTS 服务将评价结果转换为语音
+            Long roleId = visionThinkReqVO.getRoleId();
+            if (roleId != null) {
+                String ttsAudioData = webAiService.convertTextToSpeechBase64(roleId, text, null);
+                response.setAudioData(ttsAudioData);
+            }
+
+            return success(response);
+        } catch (Exception e) {
+            return CommonResult.error(GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR.getCode(), "音频处理失败: " + e.getMessage());
+        } finally {
+            arkService.shutdownExecutor();
+        }
+    }
+
     // ================ 虚拟实验室 ================
 
     @GetMapping("/selectVirtualDevice")

+ 43 - 0
byzs-web/src/main/java/cn/iocoder/byzs/module/web/service/ai/WebAiServiceImpl.java

@@ -13,6 +13,7 @@ import cn.iocoder.byzs.module.ai.dal.mysql.tts.AiTtsMapper;
 import cn.iocoder.byzs.module.ai.enums.ErrorCodeConstants;
 import cn.iocoder.byzs.module.ai.service.chat.AiChatConversationService;
 import cn.iocoder.byzs.module.ai.service.model.AiChatRoleService;
+import cn.iocoder.byzs.module.ai.service.tts.AliyunTtsService;
 import cn.iocoder.byzs.module.ai.service.tts.DouBaoTtsService;
 import cn.iocoder.byzs.module.ai.util.tts.StreamingAliyunTtsService;
 import cn.iocoder.byzs.module.ai.util.tts.StreamingDouBaoTtsService;
@@ -68,6 +69,12 @@ public class WebAiServiceImpl {
     @Resource
     private StreamingDouBaoTtsService streamingDouBaoTtsService;
 
+    @Resource
+    private DouBaoTtsService douBaoTtsService;
+
+    @Resource
+    private AliyunTtsService aliyunTtsService;
+
     /**
      * 发送指定回答的SSE流式响应
      * 确保TEXT类型文本数据优先且可靠地发送到前端,同时提供AUDIO音频流
@@ -908,4 +915,40 @@ public class WebAiServiceImpl {
 
         return success(fallbackTextResp);
     }
+
+    /**
+     * 将文本转换为语音并返回Base64编码的音频数据
+     * 格式与流式接口保持一致:纯Base64编码的WAV格式音频(无前缀)
+     *
+     * @param roleId 角色编号
+     * @param content 需要转语音的内容
+     * @param command 命令(语音指令)
+     * @return Base64编码的音频数据(纯Base64,无前缀)
+     */
+    public String convertTextToSpeechBase64(Long roleId, String content, String command) {
+        // 1. 获取TTS配置
+        AiTtsDO aiTtsDO = getTtsConfig(roleId);
+
+        byte[] audioData;
+        // 2. 根据平台选择不同的TTS服务
+        if ("DouBao".equals(aiTtsDO.getPlatform())) {
+            // 使用豆包TTS服务
+            try {
+                audioData = douBaoTtsService.convertTextToSpeech(aiTtsDO, content, command);
+            } catch (Exception e) {
+                throw new RuntimeException("豆包文本转语音失败", e);
+            }
+        } else {
+            // 使用阿里云TTS服务
+            try {
+                audioData = aliyunTtsService.convertTextToSpeech(aiTtsDO, content);
+            } catch (Exception e) {
+                throw new RuntimeException("阿里云文本转语音失败", e);
+            }
+        }
+
+        // 3. 添加WAV头并转换为Base64(纯Base64,无前缀,与流式接口格式一致)
+        byte[] wavData = WavHeader.addWavHeader(audioData, SampleRateEnum.SAMPLE_RATE_16K.value, 16, 1);
+        return java.util.Base64.getEncoder().encodeToString(wavData);
+    }
 }