公开项目接口:

Apifox Hub:

查看OpenAI接口参数(语音、转录、翻译):

可以看到,文字转语音的接口,提交参数是通过body方式提交的,是需要待转换的文字内容。

文字转语音:

引入OpenAI

<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-starter-model-openai</artifactId>
</dependency>

改造SpringAI配置类:

    @Bean
    public ChatClient chatClient(@Qualifier("dashscopeChatModel") ChatModel dashScopeChatModel, //注意这里@Qualifier的指定的名称,不要写错了
                                 Advisor loggerAdvisor, // 日志记录器
                                 Advisor messageChatMemoryAdvisor,
                                 Advisor recordOptimizationAdvisor // 记录优化
                                 // CourseTools courseTools, // 课程工具
                                 // OrderTools orderTools // 预下单工具
    ) {
        return ChatClient.builder(dashScopeChatModel)
                .defaultAdvisors(loggerAdvisor, messageChatMemoryAdvisor, recordOptimizationAdvisor) //添加 Advisor 功能增强
                // .defaultTools(courseTools, orderTools)
                .build();
    }

    @Bean
    public ChatClient openAiChatClient(@Qualifier("openAiChatModel") ChatModel openAiChatModel,
                                       Advisor loggerAdvisor  // 日志记录器
    ) {
        return ChatClient.builder(openAiChatModel)
                .defaultAdvisors(loggerAdvisor)
                .build();
    }

注意:

  1. 配置文件决定"有哪些模型"

  配置文件(application.yml)提供了 API key、URL 等参数,Spring Boot 的自动配置据此创建 ChatModel Bean:

  spring:
    ai:
      dashscope:
        api-key: xxx        # → 创建名为 dashscopeChatModel 的 Bean
      openai:
        api-key: xxx        # → 创建名为 openAiChatModel 的 Bean

  2. 代码中的 @Qualifier 决定"用哪个模型"

  配置文件只负责创建两个 Bean。但你在代码里写 @Qualifier("dashscopeChatModel") 还是
  @Qualifier("openAiChatModel"),决定了注入给谁。

所以:
  - 配置文件 = 工厂,生产了两个模型实例
  - @Qualifier = 你指定拿哪个
  - 调用时 chatClient(...) 或 openAiChatClient(...) = 你选的这条路决定了最终用哪个模型

就是在注入时,注入那个bean对象,则调用哪个模型。

    //看bean名称
    private final ChatClient chatClient;//注入阿里云
    private final ChatClient openAiChatClient;//注入openAI

tts配置:

spring:
  ai:
    openai:
      audio:
        speech:
          base-url: https://api.chatanywhere.tech/
          api-key: ${tj.ai.openai.key}
          options:
            model: tts-1 #可用的 TTS 模型之一:tts-1 或 tts-1-hd
            voice: alloy #生成音频时使用的语音。支持的语音有:alloy、echo、fable、onyx、nova 和 shimmer。
            response-format: mp3 #默认为 mp3 音频的格式。支持的格式有:mp3、opus、aac 和 flac。
            speed: 1.0 #默认为 1 生成的音频速度。选择0.25到4.0之间的值。1.0是默认值。

Controller层:

@RestController
@RequestMapping("/audio")
@RequiredArgsConstructor
public class AudioController {

    private final AudioService audioService;
    

    @NoWrapper//返回结果不需要包装
    @PostMapping(value = "/tts-stream", produces = "audio/mp3")//value->路径值
    //响应头 Content-Type: audio/mp3,适合音频流
    //不加 produces	默认可能为 application/json(Spring 会尝试转换 ResponseBodyEmitter 导致异常)
    public ResponseBodyEmitter ttsStream(@RequestBody String text) {
        return this.audioService.ttsStream(text);
    }
}

ServiceImpl层:

@Service
@Slf4j
@RequiredArgsConstructor
public class AudioServiceImpl implements AudioService {
    private final OpenAiAudioSpeechModel openAiAudioSpeechModel;
    @Override
    public ResponseBodyEmitter ttsStream(String text) {
        ResponseBodyEmitter emitter = new ResponseBodyEmitter();
        log.info("开始语音合成, 文本内容:{}", text);
        SpeechPrompt speechPrompt = new SpeechPrompt(text);
        Flux<SpeechResponse> responseStream = openAiAudioSpeechModel.stream(speechPrompt);
        // 订阅响应流并发送数据
        responseStream.subscribe(
                speechResponse -> {
                    try {
                        // 获取响应输出的数据,并发送到响应体中
                        byte[] audioBytes = speechResponse.getResult().getOutput();
                        //发送给前端
                        emitter.send(audioBytes);
                    } catch (IOException e) {
                        emitter.completeWithError(e);
                    }
                },
                emitter::completeWithError,
                emitter::complete
        );
        return emitter;
    }
}

语音转文字:

引入依赖:

在输出的结果中,可能会存在繁体中文,所以需要将繁体转化为简体中文,用到了opencc4j组件,所以需要导入其依赖:

<dependency>
    <groupId>com.github.houbb</groupId>
    <artifactId>opencc4j</artifactId>
    <version>1.8.0</version>
</dependency>

stt配置:

基于接口来定的:

spring:
  ai:
    openai:
      audio:
        transcription:
          base-url: https://api.chatanywhere.tech/
          api-key: ${tj.ai.openai.key}
          options:
            model: whisper-1  #要使用的模型 ID。目前只有 whisper-1 是可用的。
            response-format: text #转录输出的格式,可选择:json、text、srt、verbose_json 或 vtt。
            temperature: 0 #默认为 0,采样温度,between 0 和 1。更高的值像 0.8 会使输出更随机,而更低的值像 0.2 会使其更集中和确定性。如果设置为 0,模型将使用对数概率自动增加温度直到达到特定阈值。
            language: zh #输入音频的语言。以 ISO-639-1 格式提供输入语言可以提高准确性和延迟。

Controller层:

    @PostMapping("/stt")
    public String stt(@RequestParam("audioFile") MultipartFile audioFile) {
        return this.audioService.stt(audioFile);
    }

ServiceImpl层:

    private final OpenAiAudioTranscriptionModel openAiAudioTranscriptionModel;
    @Override
    public String stt(MultipartFile multipartFile) {
        // 将MultipartFile转换为Resource
        Resource audioResource = multipartFile.getResource();
        AudioTranscriptionPrompt transcriptionRequest = new AudioTranscriptionPrompt(audioResource);
        // 调用OpenAiAudioTranscriptionModel进行语音识别
        AudioTranscriptionResponse response = openAiAudioTranscriptionModel.call(transcriptionRequest);
        // 获取识别结果
        String output = response.getResult().getOutput();
        // 将繁体转换为简体
        return ZhConverterUtil.toSimple(output);//转为中文
    }

Bug修复:

自定义了WrapperResponseMessageConverter消息转化器,他的作用是对输出的内容进行包装,而SpringAI底层用的发起http请求的组件是RetryTemplate,而RetryTemplate也会用到这个消息转化器,但是这个消息转化器是无法处理文件的,所以报消息转化出错:

RetryTemplate添加一个监听器,在发起Springai模块请求前设置标识,请求结束后删除标识,这样就可以解决问题了。如果是Springai请求,则跳过自定义WrapperResponseMessageConverter消息转化器,这样就不会出错了。

    /**
     * 创建并配置自定义重试监听器Bean
     * <p>
     * 实现说明:
     * 1. 创建匿名RetryListener实现,在重试操作期间管理Web属性
     * 2. 将监听器注册到提供的RetryTemplate实例
     *
     * @param retryTemplate Spring Retry模板对象,用于注册重试监听器
     * @return RetryListener 已注册到模板的重试监听器实例,将由Spring容器管理
     */
    @Bean
    public RetryListener customizeRetryTemplate(RetryTemplate retryTemplate) {
        // 创建自定义重试监听器,实现以下核心功能:
        // - 重试开始时设置上下文标识
        // - 重试结束后清理上下文标识
        RetryListener retryListener = new RetryListener() {
            @Override
            public <T, E extends Throwable> boolean open(RetryContext context, RetryCallback<T, E> callback) {
                WebUtils.setAttribute(Constant.SPRING_AI_ATTR, Constant.SPRING_AI_FLAG);
                return true;
            }

            @Override
            public <T, E extends Throwable> void close(RetryContext context, RetryCallback<T, E> callback, Throwable throwable) {
                WebUtils.removeAttribute(Constant.SPRING_AI_ATTR);
            }
        };

        // 将监听器注册到重试模板
        retryTemplate.registerListener(retryListener);
        return retryListener;
    }
Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐