Procházet zdrojové kódy

1、课程列表优化排序
2、tts发声人与角色绑定,ai对话动态读取

liyanbo před 8 měsíci
rodič
revize
834f91491d
22 změnil soubory, kde provedl 617 přidání a 109 odebrání
  1. 3 0
      byzs-course/src/main/java/cn/iocoder/byzs/module/bjdx/controller/admin/course/vo/CoursePageReqVO.java
  2. 3 0
      byzs-course/src/main/java/cn/iocoder/byzs/module/bjdx/controller/admin/course/vo/CourseRespVO.java
  3. 3 0
      byzs-course/src/main/java/cn/iocoder/byzs/module/bjdx/controller/admin/course/vo/CourseSaveReqVO.java
  4. 5 0
      byzs-course/src/main/java/cn/iocoder/byzs/module/bjdx/dal/dataobject/course/CourseDO.java
  5. 1 1
      byzs-course/src/main/resources/mapper/course/CourseMapper.xml
  6. 5 5
      byzs-module-ai/pom.xml
  7. 3 0
      byzs-module-ai/src/main/java/cn/iocoder/byzs/module/ai/controller/admin/model/vo/chatRole/AiChatRoleRespVO.java
  8. 3 0
      byzs-module-ai/src/main/java/cn/iocoder/byzs/module/ai/controller/admin/model/vo/chatRole/AiChatRoleSaveMyReqVO.java
  9. 3 0
      byzs-module-ai/src/main/java/cn/iocoder/byzs/module/ai/controller/admin/model/vo/chatRole/AiChatRoleSaveReqVO.java
  10. 114 0
      byzs-module-ai/src/main/java/cn/iocoder/byzs/module/ai/controller/admin/tts/AiTtsController.java
  11. 47 0
      byzs-module-ai/src/main/java/cn/iocoder/byzs/module/ai/controller/admin/tts/vo/AiTtsPageReqVO.java
  12. 63 0
      byzs-module-ai/src/main/java/cn/iocoder/byzs/module/ai/controller/admin/tts/vo/AiTtsRespVO.java
  13. 45 0
      byzs-module-ai/src/main/java/cn/iocoder/byzs/module/ai/controller/admin/tts/vo/AiTtsSaveReqVO.java
  14. 5 0
      byzs-module-ai/src/main/java/cn/iocoder/byzs/module/ai/dal/dataobject/model/AiChatRoleDO.java
  15. 71 0
      byzs-module-ai/src/main/java/cn/iocoder/byzs/module/ai/dal/dataobject/tts/AiTtsDO.java
  16. 44 0
      byzs-module-ai/src/main/java/cn/iocoder/byzs/module/ai/dal/mysql/tts/AiTtsMapper.java
  17. 4 0
      byzs-module-ai/src/main/java/cn/iocoder/byzs/module/ai/enums/ErrorCodeConstants.java
  18. 13 10
      byzs-module-ai/src/main/java/cn/iocoder/byzs/module/ai/service/chat/AiChatMessageServiceImpl.java
  19. 71 0
      byzs-module-ai/src/main/java/cn/iocoder/byzs/module/ai/service/tts/AiTtsService.java
  20. 93 0
      byzs-module-ai/src/main/java/cn/iocoder/byzs/module/ai/service/tts/AiTtsServiceImpl.java
  21. 9 93
      byzs-module-ai/src/main/java/cn/iocoder/byzs/module/ai/util/tts/StreamTtsService.java
  22. 9 0
      byzs-module-ai/src/main/resources/mapper/tts/AiTtsMapper.xml

+ 3 - 0
byzs-course/src/main/java/cn/iocoder/byzs/module/bjdx/controller/admin/course/vo/CoursePageReqVO.java

@@ -27,6 +27,9 @@ public class CoursePageReqVO extends PageParam {
     @Schema(description = "课程视频路径")
     private String courseVideoPath;
 
+    @Schema(description = "课程文件路径")
+    private String courseFilePath;
+
     @Schema(description = "课程内容")
     private String courseContent;
 

+ 3 - 0
byzs-course/src/main/java/cn/iocoder/byzs/module/bjdx/controller/admin/course/vo/CourseRespVO.java

@@ -31,6 +31,9 @@ public class CourseRespVO {
     @Schema(description = "课程视频路径")
     private String courseVideoPath;
 
+    @Schema(description = "课程文件路径")
+    private String courseFilePath;
+
     @Schema(description = "课程内容")
     private String courseContent;
 

+ 3 - 0
byzs-course/src/main/java/cn/iocoder/byzs/module/bjdx/controller/admin/course/vo/CourseSaveReqVO.java

@@ -27,6 +27,9 @@ public class CourseSaveReqVO {
     @Schema(description = "课程视频路径")
     private String courseVideoPath;
 
+    @Schema(description = "课程文件路径")
+    private String courseFilePath;
+
     @Schema(description = "课程内容")
     private String courseContent;
 

+ 5 - 0
byzs-course/src/main/java/cn/iocoder/byzs/module/bjdx/dal/dataobject/course/CourseDO.java

@@ -50,6 +50,11 @@ public class CourseDO extends BaseDO {
      */
     private String courseVideoPath;
 
+    /**
+     * 课程文件路径
+     */
+    private String courseFilePath;
+
     /**
      * 课程内容
      */

+ 1 - 1
byzs-course/src/main/resources/mapper/course/CourseMapper.xml

@@ -27,7 +27,7 @@
                 AND c.course_content_type = #{courseContentType}
             </if>
         </where>
-        ORDER BY c.course_order
+        ORDER BY ct.ct_type_sort, c.course_order
 
     </select>
 </mapper>

+ 5 - 5
byzs-module-ai/pom.xml

@@ -124,11 +124,11 @@
 
         <!-- 豆包 文生图 -->
         <!-- 火山引擎豆包 SDK(取消注释并添加) -->
-<!--        <dependency>-->
-<!--            <groupId>com.volcengine</groupId>-->
-<!--            <artifactId>volcengine-java-sdk-ark-runtime</artifactId>-->
-<!--            <version>1.0.5</version>-->
-<!--        </dependency>-->
+        <dependency>
+            <groupId>com.volcengine</groupId>
+            <artifactId>volcengine-java-sdk-ark-runtime</artifactId>
+            <version>LATEST</version>
+        </dependency>
 <!--        <dependency>-->
 <!--            <groupId>com.volcengine</groupId>-->
 <!--            <artifactId>volcengine-java-sdk-core</artifactId>-->

+ 3 - 0
byzs-module-ai/src/main/java/cn/iocoder/byzs/module/ai/controller/admin/model/vo/chatRole/AiChatRoleRespVO.java

@@ -32,6 +32,9 @@ public class AiChatRoleRespVO implements VO {
     @Schema(description = "模型3d路径", example = "**.glb")
     private String model3dPath;
 
+    @Schema(description = "发声人编号", requiredMode = Schema.RequiredMode.REQUIRED)
+    private Long ttsId;
+
     @Schema(description = "角色名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "李四")
     private String name;
 

+ 3 - 0
byzs-module-ai/src/main/java/cn/iocoder/byzs/module/ai/controller/admin/model/vo/chatRole/AiChatRoleSaveMyReqVO.java

@@ -29,6 +29,9 @@ public class AiChatRoleSaveMyReqVO {
     @Schema(description = "模型3d路径", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/1.png")
     private String model3dPath;
 
+    @Schema(description = "发声人编号", requiredMode = Schema.RequiredMode.REQUIRED)
+    private Long ttsId;
+
     @Schema(description = "角色描述", requiredMode = Schema.RequiredMode.REQUIRED, example = "你说的对")
     @NotEmpty(message = "角色描述不能为空")
     private String description;

+ 3 - 0
byzs-module-ai/src/main/java/cn/iocoder/byzs/module/ai/controller/admin/model/vo/chatRole/AiChatRoleSaveReqVO.java

@@ -38,6 +38,9 @@ public class AiChatRoleSaveReqVO {
     @Schema(description = "模型3d路径", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/1.png")
     private String model3dPath;
 
+    @Schema(description = "发声人编号", requiredMode = Schema.RequiredMode.REQUIRED)
+    private Long ttsId;
+
     @Schema(description = "角色排序", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
     @NotNull(message = "角色排序不能为空")
     private Integer sort;

+ 114 - 0
byzs-module-ai/src/main/java/cn/iocoder/byzs/module/ai/controller/admin/tts/AiTtsController.java

@@ -0,0 +1,114 @@
+package cn.iocoder.byzs.module.ai.controller.admin.tts;
+
+import cn.iocoder.byzs.framework.apilog.core.annotation.ApiAccessLog;
+import cn.iocoder.byzs.framework.common.enums.CommonStatusEnum;
+import cn.iocoder.byzs.framework.common.pojo.CommonResult;
+import cn.iocoder.byzs.framework.common.pojo.PageParam;
+import cn.iocoder.byzs.framework.common.pojo.PageResult;
+import cn.iocoder.byzs.framework.common.util.object.BeanUtils;
+import cn.iocoder.byzs.framework.excel.core.util.ExcelUtils;
+import cn.iocoder.byzs.module.ai.controller.admin.model.vo.tool.AiToolRespVO;
+import cn.iocoder.byzs.module.ai.controller.admin.tts.vo.AiTtsPageReqVO;
+import cn.iocoder.byzs.module.ai.controller.admin.tts.vo.AiTtsRespVO;
+import cn.iocoder.byzs.module.ai.controller.admin.tts.vo.AiTtsSaveReqVO;
+import cn.iocoder.byzs.module.ai.dal.dataobject.tts.AiTtsDO;
+import cn.iocoder.byzs.module.ai.service.tts.AiTtsService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.annotation.Resource;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.validation.Valid;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import java.io.IOException;
+import java.util.List;
+
+import static cn.iocoder.byzs.framework.apilog.core.enums.OperateTypeEnum.EXPORT;
+import static cn.iocoder.byzs.framework.common.pojo.CommonResult.success;
+import static cn.iocoder.byzs.framework.common.util.collection.CollectionUtils.convertList;
+
+@Tag(name = "管理后台 - AI TTS文转音")
+@RestController
+@RequestMapping("/ai/tts")
+@Validated
+public class AiTtsController {
+
+    @Resource
+    private AiTtsService ttsService;
+
+    @PostMapping("/create")
+    @Operation(summary = "创建AI TTS文转音")
+    @PreAuthorize("@ss.hasPermission('ai:tts:create')")
+    public CommonResult<Long> createTts(@Valid @RequestBody AiTtsSaveReqVO createReqVO) {
+        return success(ttsService.createTts(createReqVO));
+    }
+
+    @PutMapping("/update")
+    @Operation(summary = "更新AI TTS文转音")
+    @PreAuthorize("@ss.hasPermission('ai:tts:update')")
+    public CommonResult<Boolean> updateTts(@Valid @RequestBody AiTtsSaveReqVO updateReqVO) {
+        ttsService.updateTts(updateReqVO);
+        return success(true);
+    }
+
+    @DeleteMapping("/delete")
+    @Operation(summary = "删除AI TTS文转音")
+    @Parameter(name = "id", description = "编号", required = true)
+    @PreAuthorize("@ss.hasPermission('ai:tts:delete')")
+    public CommonResult<Boolean> deleteTts(@RequestParam("id") Long id) {
+        ttsService.deleteTts(id);
+        return success(true);
+    }
+
+    @DeleteMapping("/delete-list")
+    @Parameter(name = "ids", description = "编号", required = true)
+    @Operation(summary = "批量删除AI TTS文转音")
+                @PreAuthorize("@ss.hasPermission('ai:tts:delete')")
+    public CommonResult<Boolean> deleteTtsList(@RequestParam("ids") List<Long> ids) {
+        ttsService.deleteTtsListByIds(ids);
+        return success(true);
+    }
+
+    @GetMapping("/get")
+    @Operation(summary = "获得AI TTS文转音")
+    @Parameter(name = "id", description = "编号", required = true, example = "1024")
+    @PreAuthorize("@ss.hasPermission('ai:tts:query')")
+    public CommonResult<AiTtsRespVO> getTts(@RequestParam("id") Long id) {
+        AiTtsDO tts = ttsService.getTts(id);
+        return success(BeanUtils.toBean(tts, AiTtsRespVO.class));
+    }
+
+    @GetMapping("/page")
+    @Operation(summary = "获得AI TTS文转音分页")
+    @PreAuthorize("@ss.hasPermission('ai:tts:query')")
+    public CommonResult<PageResult<AiTtsRespVO>> getTtsPage(@Valid AiTtsPageReqVO pageReqVO) {
+        PageResult<AiTtsDO> pageResult = ttsService.getTtsPage(pageReqVO);
+        return success(BeanUtils.toBean(pageResult, AiTtsRespVO.class));
+    }
+
+    @GetMapping("/export-excel")
+    @Operation(summary = "导出AI TTS文转音 Excel")
+    @PreAuthorize("@ss.hasPermission('ai:tts:export')")
+    @ApiAccessLog(operateType = EXPORT)
+    public void exportTtsExcel(@Valid AiTtsPageReqVO pageReqVO,
+              HttpServletResponse response) throws IOException {
+        pageReqVO.setPageSize(PageParam.PAGE_SIZE_NONE);
+        List<AiTtsDO> list = ttsService.getTtsPage(pageReqVO).getList();
+        // 导出 Excel
+        ExcelUtils.write(response, "AI TTS文转音.xls", "数据", AiTtsRespVO.class,
+                        BeanUtils.toBean(list, AiTtsRespVO.class));
+    }
+
+
+    @GetMapping("/simple-list")
+    @Operation(summary = "获得tts的精简列表")
+    public CommonResult<List<AiToolRespVO>> getTtsSimpleList() {
+        List<AiTtsDO> list = ttsService.getTtsSimpleListByStatus(CommonStatusEnum.ENABLE.getStatus());
+        return success(convertList(list, tool -> new AiToolRespVO()
+                .setId(tool.getId()).setName(tool.getName())));
+    }
+
+}

+ 47 - 0
byzs-module-ai/src/main/java/cn/iocoder/byzs/module/ai/controller/admin/tts/vo/AiTtsPageReqVO.java

@@ -0,0 +1,47 @@
+package cn.iocoder.byzs.module.ai.controller.admin.tts.vo;
+
+import lombok.*;
+import java.util.*;
+import io.swagger.v3.oas.annotations.media.Schema;
+import cn.iocoder.byzs.framework.common.pojo.PageParam;
+import org.springframework.format.annotation.DateTimeFormat;
+import java.time.LocalDateTime;
+
+import static cn.iocoder.byzs.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
+
+@Schema(description = "管理后台 - AI TTS文转音分页 Request VO")
+@Data
+public class AiTtsPageReqVO extends PageParam {
+
+    @Schema(description = "发音人名字")
+    private String name;
+
+    @Schema(description = "发音人标识")
+    private String model;
+
+    @Schema(description = "TTS平台")
+    private String platform;
+
+    @Schema(description = "发音人类型")
+    private Integer type;
+
+    @Schema(description = "排序")
+    private Integer sort;
+
+    @Schema(description = "状态")
+    private Integer status;
+
+    @Schema(description = "语速")
+    private Integer speechRate;
+
+    @Schema(description = "语调")
+    private Integer volume;
+
+    @Schema(description = "音量")
+    private Integer pitchRate;
+
+    @Schema(description = "创建时间")
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    private LocalDateTime[] createTime;
+
+}

+ 63 - 0
byzs-module-ai/src/main/java/cn/iocoder/byzs/module/ai/controller/admin/tts/vo/AiTtsRespVO.java

@@ -0,0 +1,63 @@
+package cn.iocoder.byzs.module.ai.controller.admin.tts.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.*;
+import java.util.*;
+import org.springframework.format.annotation.DateTimeFormat;
+import java.time.LocalDateTime;
+import com.alibaba.excel.annotation.*;
+import cn.iocoder.byzs.framework.excel.core.annotations.DictFormat;
+import cn.iocoder.byzs.framework.excel.core.convert.DictConvert;
+
+@Schema(description = "管理后台 - AI TTS文转音 Response VO")
+@Data
+@ExcelIgnoreUnannotated
+public class AiTtsRespVO {
+
+    @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED)
+    @ExcelProperty("编号")
+    private Long id;
+
+    @Schema(description = "发音人名字", requiredMode = Schema.RequiredMode.REQUIRED)
+    @ExcelProperty("发音人名字")
+    private String name;
+
+    @Schema(description = "发音人标识", requiredMode = Schema.RequiredMode.REQUIRED)
+    @ExcelProperty("发音人标识")
+    private String model;
+
+    @Schema(description = "TTS平台", requiredMode = Schema.RequiredMode.REQUIRED)
+    @ExcelProperty(value = "TTS平台", converter = DictConvert.class)
+    @DictFormat("ai_platform") // TODO 代码优化:建议设置到对应的 DictTypeConstants 枚举类中
+    private String platform;
+
+    @Schema(description = "发音人类型")
+    @ExcelProperty(value = "发音人类型", converter = DictConvert.class)
+    @DictFormat("ai_tts_type") // TODO 代码优化:建议设置到对应的 DictTypeConstants 枚举类中
+    private Integer type;
+
+    @Schema(description = "排序")
+    @ExcelProperty("排序")
+    private Integer sort;
+
+    @Schema(description = "状态")
+    @ExcelProperty("状态")
+    private Integer status;
+
+    @Schema(description = "语速")
+    @ExcelProperty("语速")
+    private Integer speechRate;
+
+    @Schema(description = "语调")
+    @ExcelProperty("语调")
+    private Integer volume;
+
+    @Schema(description = "音量")
+    @ExcelProperty("音量")
+    private Integer pitchRate;
+
+    @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
+    @ExcelProperty("创建时间")
+    private LocalDateTime createTime;
+
+}

+ 45 - 0
byzs-module-ai/src/main/java/cn/iocoder/byzs/module/ai/controller/admin/tts/vo/AiTtsSaveReqVO.java

@@ -0,0 +1,45 @@
+package cn.iocoder.byzs.module.ai.controller.admin.tts.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.*;
+import java.util.*;
+import jakarta.validation.constraints.*;
+
+@Schema(description = "管理后台 - AI TTS文转音新增/修改 Request VO")
+@Data
+public class AiTtsSaveReqVO {
+
+    @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED)
+    private Long id;
+
+    @Schema(description = "发音人名字", requiredMode = Schema.RequiredMode.REQUIRED)
+    @NotEmpty(message = "发音人名字不能为空")
+    private String name;
+
+    @Schema(description = "发音人标识", requiredMode = Schema.RequiredMode.REQUIRED)
+    @NotEmpty(message = "发音人标识不能为空")
+    private String model;
+
+    @Schema(description = "TTS平台", requiredMode = Schema.RequiredMode.REQUIRED)
+    @NotEmpty(message = "TTS平台不能为空")
+    private String platform;
+
+    @Schema(description = "发音人类型")
+    private Integer type;
+
+    @Schema(description = "排序")
+    private Integer sort;
+
+    @Schema(description = "状态")
+    private Integer status;
+
+    @Schema(description = "语速")
+    private Integer speechRate;
+
+    @Schema(description = "语调")
+    private Integer volume;
+
+    @Schema(description = "音量")
+    private Integer pitchRate;
+
+}

+ 5 - 0
byzs-module-ai/src/main/java/cn/iocoder/byzs/module/ai/dal/dataobject/model/AiChatRoleDO.java

@@ -58,6 +58,11 @@ public class AiChatRoleDO extends BaseDO {
      * 角色3d模型
      */
     private String model3dPath;
+
+    /**
+     * 发声人编号
+     */
+    private Long ttsId;
     /**
      * 角色设定
      */

+ 71 - 0
byzs-module-ai/src/main/java/cn/iocoder/byzs/module/ai/dal/dataobject/tts/AiTtsDO.java

@@ -0,0 +1,71 @@
+package cn.iocoder.byzs.module.ai.dal.dataobject.tts;
+
+import cn.iocoder.byzs.framework.mybatis.core.dataobject.BaseDO;
+import com.baomidou.mybatisplus.annotation.KeySequence;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.*;
+
+/**
+ * AI TTS文转音 DO
+ *
+ * @author lyb
+ */
+@TableName("ai_tts")
+@KeySequence("ai_tts_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class AiTtsDO extends BaseDO {
+
+    /**
+     * 编号
+     */
+    @TableId
+    private Long id;
+    /**
+     * 发音人名字
+     */
+    private String name;
+    /**
+     * 发音人标识
+     */
+    private String model;
+    /**
+     * TTS平台
+     *
+     * 枚举 {@link TODO ai_platform 对应的类}
+     */
+    private String platform;
+    /**
+     * 发音人类型
+     *
+     * 枚举 {@link TODO ai_tts_type 对应的类}
+     */
+    private Integer type;
+    /**
+     * 排序
+     */
+    private Integer sort;
+    /**
+     * 状态
+     */
+    private Integer status;
+    /**
+     * 语速
+     */
+    private Integer speechRate;
+    /**
+     * 语调
+     */
+    private Integer volume;
+    /**
+     * 音量
+     */
+    private Integer pitchRate;
+
+
+}

+ 44 - 0
byzs-module-ai/src/main/java/cn/iocoder/byzs/module/ai/dal/mysql/tts/AiTtsMapper.java

@@ -0,0 +1,44 @@
+package cn.iocoder.byzs.module.ai.dal.mysql.tts;
+
+import java.util.*;
+
+import cn.iocoder.byzs.framework.common.pojo.PageResult;
+import cn.iocoder.byzs.framework.mybatis.core.query.LambdaQueryWrapperX;
+import cn.iocoder.byzs.framework.mybatis.core.mapper.BaseMapperX;
+import cn.iocoder.byzs.module.ai.dal.dataobject.tts.AiTtsDO;
+import org.apache.ibatis.annotations.Mapper;
+import cn.iocoder.byzs.module.ai.controller.admin.tts.vo.*;
+
+/**
+ * AI TTS文转音 Mapper
+ *
+ * @author lyb
+ */
+@Mapper
+public interface AiTtsMapper extends BaseMapperX<AiTtsDO> {
+
+    default PageResult<AiTtsDO> selectPage(AiTtsPageReqVO reqVO) {
+        return selectPage(reqVO, new LambdaQueryWrapperX<AiTtsDO>()
+                .likeIfPresent(AiTtsDO::getName, reqVO.getName())
+                .eqIfPresent(AiTtsDO::getModel, reqVO.getModel())
+                .eqIfPresent(AiTtsDO::getPlatform, reqVO.getPlatform())
+                .eqIfPresent(AiTtsDO::getType, reqVO.getType())
+                .eqIfPresent(AiTtsDO::getSort, reqVO.getSort())
+                .eqIfPresent(AiTtsDO::getStatus, reqVO.getStatus())
+                .eqIfPresent(AiTtsDO::getSpeechRate, reqVO.getSpeechRate())
+                .eqIfPresent(AiTtsDO::getVolume, reqVO.getVolume())
+                .eqIfPresent(AiTtsDO::getPitchRate, reqVO.getPitchRate())
+                .betweenIfPresent(AiTtsDO::getCreateTime, reqVO.getCreateTime())
+                .orderByAsc(AiTtsDO::getSort));
+    }
+
+
+    /**
+     * 获得AI TTS文转音精简列表,用于前端的选择框
+     *
+     * @param status 状态
+     * @return AI TTS文转音精简列表
+     */
+    List<AiTtsDO> getTtsSimpleListByStatus(Integer status);
+
+}

+ 4 - 0
byzs-module-ai/src/main/java/cn/iocoder/byzs/module/ai/enums/ErrorCodeConstants.java

@@ -65,4 +65,8 @@ public interface ErrorCodeConstants {
     ErrorCode WORKFLOW_NOT_EXISTS = new ErrorCode(1_040_011_000, "工作流不存在");
     ErrorCode WORKFLOW_CODE_EXISTS = new ErrorCode(1_040_011_001, "工作流标识已存在");
 
+
+    // ========== AI tts 1-050-011-000 ==========
+    ErrorCode TTS_NOT_EXISTS = new ErrorCode(1_050_011_001, "AI TTS文转音不存在");
+
 }

+ 13 - 10
byzs-module-ai/src/main/java/cn/iocoder/byzs/module/ai/service/chat/AiChatMessageServiceImpl.java

@@ -3,7 +3,9 @@ package cn.iocoder.byzs.module.ai.service.chat;
 import cn.hutool.core.collection.CollUtil;
 import cn.hutool.core.util.ObjUtil;
 import cn.hutool.core.util.StrUtil;
-import cn.iocoder.byzs.module.ai.controller.admin.tts.StreamTtsService;
+import cn.iocoder.byzs.module.ai.dal.dataobject.tts.AiTtsDO;
+import cn.iocoder.byzs.module.ai.dal.mysql.tts.AiTtsMapper;
+import cn.iocoder.byzs.module.ai.util.tts.StreamTtsService;
 import cn.iocoder.byzs.module.ai.enums.model.AiPlatformEnum;
 import cn.iocoder.byzs.module.ai.util.AiUtils;
 import cn.iocoder.byzs.framework.common.pojo.CommonResult;
@@ -42,7 +44,6 @@ import org.springframework.ai.chat.prompt.ChatOptions;
 import org.springframework.ai.chat.prompt.Prompt;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
-import reactor.core.publisher.ConnectableFlux;
 import reactor.core.publisher.DirectProcessor;
 import reactor.core.publisher.Flux;
 import reactor.core.publisher.FluxSink;
@@ -54,7 +55,6 @@ import java.util.*;
 import java.util.concurrent.Executors;
 import java.util.concurrent.ScheduledExecutorService;
 import java.util.concurrent.ScheduledFuture;
-import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicReference;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
@@ -101,6 +101,8 @@ public class AiChatMessageServiceImpl implements AiChatMessageService {
     private AiToolService toolService;
     @Resource
     private StreamTtsService streamTtsService;  // 注入TTS服务
+    @Resource
+    private AiTtsMapper ttsMapper;
 
 
     @Transactional(rollbackFor = Exception.class)
@@ -163,6 +165,10 @@ public class AiChatMessageServiceImpl implements AiChatMessageService {
         // 1.2 校验模型
         AiModelDO model = modalService.validateModel(conversation.getModelId());
         StreamingChatModel chatModel = modalService.getChatModel(model.getId());
+        //角色
+        AiChatRoleDO chatRole = chatRoleService.getChatRole(conversation.getRoleId());
+        //发声人
+        AiTtsDO aiTtsDO = ttsMapper.selectById(chatRole.getTtsId());
 
         // 2. 知识库找回
         List<AiKnowledgeSegmentSearchRespBO> knowledgeSegments = recallKnowledgeSegment(sendReqVO.getContent(),
@@ -190,7 +196,7 @@ public class AiChatMessageServiceImpl implements AiChatMessageService {
         FluxSink<CommonResult<AiChatMessageSendRespVO>> sink = processor.sink();
 
         // 4.3 初始化TTS服务
-        streamTtsService.startTts();
+        streamTtsService.startTts(aiTtsDO);
 
         // 4.4 流式返回并处理TTS
         StringBuffer contentBuffer = new StringBuffer();
@@ -220,7 +226,7 @@ public class AiChatMessageServiceImpl implements AiChatMessageService {
 
             contentBuffer.append(newContent);
             contentTTSBuffer.append(newContent);
-            System.out.println("==============newContent: " + newContent);
+//            System.out.println("==============newContent: " + newContent);
 
             // 发送新内容到TTS服务进行语音合成
             if (ttsTask.get() != null) {
@@ -313,22 +319,20 @@ public class AiChatMessageServiceImpl implements AiChatMessageService {
 
     // 处理完整句子
     private void processCompleteSentence(StringBuffer buffer, Matcher matcher) {
-        System.out.println("==============[处理完整句子[[buffer: " + buffer.toString());
         String sentence = buffer.substring(0, matcher.end());
+        System.out.println("==============[处理完整句子[[buffer: " + sentence);
         streamTtsService.sendText(sentence);
         buffer.delete(0, matcher.end());
         log.info("TTS合成完整句: {}", sentence); // 替换System.out为日志
-        System.out.println("==============[处理完整句子[[buffer: " + buffer);
     }
 
     // 处理指定长度文本
     private void processCompleteSentence(StringBuffer buffer, int length) {
-        System.out.println("==============[处理指定长度文本[[buffer: " + buffer.toString());
         String sentence = buffer.substring(0, length);
+        System.out.println("==============[处理指定长度文本[[buffer: " + sentence);
         streamTtsService.sendText(sentence);
         buffer.delete(0, length);
         log.info("TTS合成长文本: {}", sentence); // 替换System.out为日志
-        System.out.println("==============[处理指定长度文本[[buffer: " + buffer.toString());
     }
 
     // 处理剩余文本
@@ -337,7 +341,6 @@ public class AiChatMessageServiceImpl implements AiChatMessageService {
             System.out.println("TTS合成剩余文本: " + buffer);
             streamTtsService.sendText(buffer.toString());
             buffer.setLength(0);
-            System.out.println("TTS合成剩余文本: " + buffer);
         }
     }
 

+ 71 - 0
byzs-module-ai/src/main/java/cn/iocoder/byzs/module/ai/service/tts/AiTtsService.java

@@ -0,0 +1,71 @@
+package cn.iocoder.byzs.module.ai.service.tts;
+
+import cn.iocoder.byzs.framework.common.pojo.PageResult;
+import cn.iocoder.byzs.module.ai.controller.admin.tts.vo.AiTtsPageReqVO;
+import cn.iocoder.byzs.module.ai.controller.admin.tts.vo.AiTtsSaveReqVO;
+import cn.iocoder.byzs.module.ai.dal.dataobject.tts.AiTtsDO;
+import jakarta.validation.Valid;
+
+import java.util.List;
+
+/**
+ * AI TTS文转音 Service 接口
+ *
+ * @author lyb
+ */
+public interface AiTtsService {
+
+    /**
+     * 创建AI TTS文转音
+     *
+     * @param createReqVO 创建信息
+     * @return 编号
+     */
+    Long createTts(@Valid AiTtsSaveReqVO createReqVO);
+
+    /**
+     * 更新AI TTS文转音
+     *
+     * @param updateReqVO 更新信息
+     */
+    void updateTts(@Valid AiTtsSaveReqVO updateReqVO);
+
+    /**
+     * 删除AI TTS文转音
+     *
+     * @param id 编号
+     */
+    void deleteTts(Long id);
+
+    /**
+    * 批量删除AI TTS文转音
+    *
+    * @param ids 编号
+    */
+    void deleteTtsListByIds(List<Long> ids);
+
+    /**
+     * 获得AI TTS文转音
+     *
+     * @param id 编号
+     * @return AI TTS文转音
+     */
+    AiTtsDO getTts(Long id);
+
+    /**
+     * 获得AI TTS文转音分页
+     *
+     * @param pageReqVO 分页查询
+     * @return AI TTS文转音分页
+     */
+    PageResult<AiTtsDO> getTtsPage(AiTtsPageReqVO pageReqVO);
+
+    /**
+     * 获得AI TTS文转音精简列表,用于前端的选择框
+     *
+     * @param status 状态
+     * @return AI TTS文转音精简列表
+     */
+    List<AiTtsDO> getTtsSimpleListByStatus(Integer status);
+
+}

+ 93 - 0
byzs-module-ai/src/main/java/cn/iocoder/byzs/module/ai/service/tts/AiTtsServiceImpl.java

@@ -0,0 +1,93 @@
+package cn.iocoder.byzs.module.ai.service.tts;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.iocoder.byzs.framework.common.pojo.PageResult;
+import cn.iocoder.byzs.framework.common.util.object.BeanUtils;
+import cn.iocoder.byzs.module.ai.controller.admin.tts.vo.AiTtsPageReqVO;
+import cn.iocoder.byzs.module.ai.controller.admin.tts.vo.AiTtsSaveReqVO;
+import cn.iocoder.byzs.module.ai.dal.dataobject.tts.AiTtsDO;
+import cn.iocoder.byzs.module.ai.dal.mysql.tts.AiTtsMapper;
+import jakarta.annotation.Resource;
+import org.springframework.stereotype.Service;
+import org.springframework.validation.annotation.Validated;
+
+import java.util.List;
+
+import static cn.iocoder.byzs.framework.common.exception.util.ServiceExceptionUtil.exception;
+import static cn.iocoder.byzs.module.ai.enums.ErrorCodeConstants.TTS_NOT_EXISTS;
+
+/**
+ * AI TTS文转音 Service 实现类
+ *
+ * @author lyb
+ */
+@Service
+@Validated
+public class AiTtsServiceImpl implements AiTtsService {
+
+    @Resource
+    private AiTtsMapper ttsMapper;
+
+    @Override
+    public Long createTts(AiTtsSaveReqVO createReqVO) {
+        // 插入
+        AiTtsDO tts = BeanUtils.toBean(createReqVO, AiTtsDO.class);
+        ttsMapper.insert(tts);
+        // 返回
+        return tts.getId();
+    }
+
+    @Override
+    public void updateTts(AiTtsSaveReqVO updateReqVO) {
+        // 校验存在
+        validateTtsExists(updateReqVO.getId());
+        // 更新
+        AiTtsDO updateObj = BeanUtils.toBean(updateReqVO, AiTtsDO.class);
+        ttsMapper.updateById(updateObj);
+    }
+
+    @Override
+    public void deleteTts(Long id) {
+        // 校验存在
+        validateTtsExists(id);
+        // 删除
+        ttsMapper.deleteById(id);
+    }
+
+    @Override
+        public void deleteTtsListByIds(List<Long> ids) {
+        // 校验存在
+        validateTtsExists(ids);
+        // 删除
+        ttsMapper.deleteByIds(ids);
+        }
+
+    private void validateTtsExists(List<Long> ids) {
+        List<AiTtsDO> list = ttsMapper.selectByIds(ids);
+        if (CollUtil.isEmpty(list) || list.size() != ids.size()) {
+            throw exception(TTS_NOT_EXISTS);
+        }
+    }
+
+    private void validateTtsExists(Long id) {
+        if (ttsMapper.selectById(id) == null) {
+            throw exception(TTS_NOT_EXISTS);
+        }
+    }
+
+    @Override
+    public AiTtsDO getTts(Long id) {
+        return ttsMapper.selectById(id);
+    }
+
+    @Override
+    public PageResult<AiTtsDO> getTtsPage(AiTtsPageReqVO pageReqVO) {
+        return ttsMapper.selectPage(pageReqVO);
+    }
+
+    @Override
+    public List<AiTtsDO> getTtsSimpleListByStatus(Integer status) {
+        return ttsMapper.getTtsSimpleListByStatus(status);
+    }
+
+}

+ 9 - 93
byzs-module-ai/src/main/java/cn/iocoder/byzs/module/ai/controller/admin/tts/StreamTtsService.java → byzs-module-ai/src/main/java/cn/iocoder/byzs/module/ai/util/tts/StreamTtsService.java

@@ -1,5 +1,6 @@
-package cn.iocoder.byzs.module.ai.controller.admin.tts;
+package cn.iocoder.byzs.module.ai.util.tts;
 
+import cn.iocoder.byzs.module.ai.dal.dataobject.tts.AiTtsDO;
 import com.alibaba.nls.client.AccessToken;
 import com.alibaba.nls.client.protocol.NlsClient;
 import com.alibaba.nls.client.protocol.OutputFormatEnum;
@@ -13,11 +14,8 @@ import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Value;
 import org.springframework.stereotype.Service;
 
-import javax.sound.sampled.*;
 import java.io.IOException;
 import java.nio.ByteBuffer;
-import java.util.concurrent.ConcurrentLinkedQueue;
-import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.function.Consumer;
 
 @Service
@@ -40,7 +38,6 @@ public class StreamTtsService {
     private String ALIYUN_AK_SECRET;
 
     private NlsClient client;
-    private PlaybackRunnable playbackRunnable;
     private Thread playbackThread;
     private StreamInputTts synthesizer;
     // ==== 添加音频数据回调 ====
@@ -71,21 +68,10 @@ public class StreamTtsService {
     /**
      * 开始TTS语音合成
      */
-    public void startTts() {
-        // 初始化播放器
-        playbackRunnable = new PlaybackRunnable(24000);
-        try {
-            playbackRunnable.prepare();
-        } catch (LineUnavailableException e) {
-            log.error("初始化音频播放器失败", e);
-            throw new RuntimeException("初始化音频播放器失败", e);
-        }
-        playbackThread = new Thread(playbackRunnable);
-        playbackThread.start();
-
+    public void startTts(AiTtsDO aiTtsDO) {
         // 创建TTS实例
         try {
-            synthesizer = new StreamInputTts(client, getSynthesizerListener(playbackRunnable));
+            synthesizer = new StreamInputTts(client, getSynthesizerListener());
         } catch (Exception e) {
             log.error("创建TTS实例", e);
             throw new RuntimeException("创建TTS实例", e);
@@ -93,10 +79,10 @@ public class StreamTtsService {
         synthesizer.setAppKey(appKey);
         synthesizer.setFormat(OutputFormatEnum.PCM);
         synthesizer.setSampleRate(SampleRateEnum.SAMPLE_RATE_24K);
-        synthesizer.setVoice("aitong");
-        synthesizer.setVolume(50);
-        synthesizer.setPitchRate(0);
-        synthesizer.setSpeechRate(50);
+        synthesizer.setVoice(aiTtsDO.getModel());
+        synthesizer.setVolume(aiTtsDO.getVolume());
+        synthesizer.setPitchRate(aiTtsDO.getPitchRate());
+        synthesizer.setSpeechRate(aiTtsDO.getSpeechRate());
 //        synthesizer.setSplitText(true); // 如有类似配置需设为false
 //        synthesizer.setEnableSplit(false); // 禁用文本拆分,确保整句合成
 
@@ -132,9 +118,6 @@ public class StreamTtsService {
                 synthesizer.stopStreamInputTts();
                 synthesizer.close();
             }
-            if (playbackRunnable != null) {
-                playbackRunnable.stop();
-            }
             if (playbackThread != null) {
                 playbackThread.join();
             }
@@ -142,12 +125,11 @@ public class StreamTtsService {
             log.error("停止TTS服务失败", e);
         } finally {
             synthesizer = null;
-            playbackRunnable = null;
             playbackThread = null;
         }
     }
 
-    private StreamInputTtsListener getSynthesizerListener(final PlaybackRunnable audioPlayer) {
+    private StreamInputTtsListener getSynthesizerListener() {
         return new StreamInputTtsListener() {
             private boolean firstRecvBinary = true;
 
@@ -169,7 +151,6 @@ public class StreamTtsService {
             @Override
             public void onSynthesisComplete(StreamInputTtsResponse response) {
                 log.info("TTS合成完成: name={}, status={}", response.getName(), response.getStatus());
-                audioPlayer.stop();
                 // 调用完成回调
                 if (onCompleteCallback != null) {
                     onCompleteCallback.run();
@@ -184,7 +165,6 @@ public class StreamTtsService {
                 }
                 byte[] bytesArray = new byte[message.remaining()];
                 message.get(bytesArray, 0, bytesArray.length);
-//                audioPlayer.put(ByteBuffer.wrap(bytesArray));
 
                 // ==== 调用回调传递音频数据 ====
                 log.info("生成音频数据: 长度={} bytes", bytesArray.length);
@@ -212,7 +192,6 @@ public class StreamTtsService {
                         response.getTaskId(),
                         response.getStatus(),
                         response.getStatusText());
-                audioPlayer.stop();
             }
         };
     }
@@ -221,69 +200,6 @@ public class StreamTtsService {
         this.audioDataCallback = callback;
     }
 
-    class PlaybackRunnable implements Runnable {
-        private AudioFormat af;
-        private DataLine.Info info;
-        private SourceDataLine targetSource;
-        private AtomicBoolean runFlag;
-        private ConcurrentLinkedQueue<ByteBuffer> queue;
-
-        public PlaybackRunnable(int sample_rate) {
-            af = new AudioFormat(sample_rate, 16, 1, true, false);
-            info = new DataLine.Info(SourceDataLine.class, af);
-            targetSource = null;
-            runFlag = new AtomicBoolean(true);
-            queue = new ConcurrentLinkedQueue<>();
-        }
-
-        public void prepare() throws LineUnavailableException {
-            targetSource = (SourceDataLine) AudioSystem.getLine(info);
-            targetSource.open(af, 4096);
-            targetSource.start();
-        }
-
-        public void put(ByteBuffer buffer) {
-            queue.add(buffer);
-        }
-
-        public void stop() {
-            runFlag.set(false);
-        }
-
-        @Override
-        public void run() {
-            if (targetSource == null) {
-                return;
-            }
-            while (runFlag.get()) {
-                if (queue.isEmpty()) {
-                    try {
-                        Thread.sleep(10);
-                    } catch (InterruptedException e) {
-                        Thread.currentThread().interrupt();
-                        break;
-                    }
-                    continue;
-                }
-                ByteBuffer buffer = queue.poll();
-                if (buffer == null) {
-                    continue;
-                }
-                byte[] data = buffer.array();
-                targetSource.write(data, 0, data.length);
-            }
-            if (!queue.isEmpty()) {
-                ByteBuffer buffer;
-                while ((buffer = queue.poll()) != null) {
-                    byte[] data = buffer.array();
-                    targetSource.write(data, 0, data.length);
-                }
-            }
-            targetSource.drain();
-            targetSource.stop();
-            targetSource.close();
-        }
-    }
 
     @PreDestroy
     public void destroy() {

+ 9 - 0
byzs-module-ai/src/main/resources/mapper/tts/AiTtsMapper.xml

@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="cn.iocoder.byzs.module.ai.dal.mysql.tts.AiTtsMapper">
+
+
+    <select id="getTtsSimpleListByStatus" resultType="cn.iocoder.byzs.module.ai.dal.dataobject.tts.AiTtsDO">
+        select id, name from ai_tts where status = #{status} order by sort
+    </select>
+</mapper>