2 Commits

Author SHA1 Message Date
huangyongxing 2f351aeb1d 项目案例优化及笔记总结 1 week ago
huangyongxing 38e807600e 智能客服项目案例 2 weeks ago
  1. BIN
      黄永兴学习笔记/4.4黄永兴.docx
  2. BIN
      黄永兴学习笔记/Spring AI Alibaba 智能客服+语音合成项目笔记.docx
  3. BIN
      黄永兴学习笔记/SpringAIAlibaba.zip
  4. 177
      黄永兴学习笔记/智能客服demo/SpringAIAlibaba/src/main/java/com/example/springaialibaba/controller/CustomerServiceController.java
  5. BIN
      黄永兴学习笔记/智能客服demo/SpringAIAlibaba/target/classes/com/example/springaialibaba/SpringAiAlibabaApplication.class
  6. BIN
      黄永兴学习笔记/智能客服demo/SpringAIAlibaba/target/classes/com/example/springaialibaba/controller/CustomerServiceController.class
  7. 0
      黄永兴学习笔记/智能客服案例/SpringAIAlibaba/.mvn/wrapper/maven-wrapper.properties
  8. 0
      黄永兴学习笔记/智能客服案例/SpringAIAlibaba/SpringAIAlibaba.iml
  9. 8
      黄永兴学习笔记/智能客服案例/SpringAIAlibaba/pom.xml
  10. 0
      黄永兴学习笔记/智能客服案例/SpringAIAlibaba/src/main/java/com/example/springaialibaba/SpringAiAlibabaApplication.java
  11. 230
      黄永兴学习笔记/智能客服案例/SpringAIAlibaba/src/main/java/com/example/springaialibaba/controller/CustomerServiceController.java
  12. 97
      黄永兴学习笔记/智能客服案例/SpringAIAlibaba/src/main/java/com/example/springaialibaba/controller/Main.java
  13. 118
      黄永兴学习笔记/智能客服案例/SpringAIAlibaba/src/main/java/com/example/springaialibaba/controller/SpeechSynthesisController.java
  14. 14
      黄永兴学习笔记/智能客服案例/SpringAIAlibaba/src/main/java/com/example/springaialibaba/pojo/StreamResponse.java
  15. 107
      黄永兴学习笔记/智能客服案例/SpringAIAlibaba/src/main/java/com/example/springaialibaba/service/AudioGenerationService.java
  16. 82
      黄永兴学习笔记/智能客服案例/SpringAIAlibaba/src/main/java/com/example/springaialibaba/service/SpeechSynthesisService.java
  17. 1
      黄永兴学习笔记/智能客服案例/SpringAIAlibaba/src/main/resources/application.yml
  18. 263
      黄永兴学习笔记/智能客服案例/SpringAIAlibaba/src/main/resources/static/index.html
  19. 0
      黄永兴学习笔记/智能客服案例/SpringAIAlibaba/src/test/java/com/example/springaialibaba/SpringAiAlibabaApplicationTests.java
  20. 1
      黄永兴学习笔记/智能客服案例/SpringAIAlibaba/target/classes/application.yml
  21. BIN
      黄永兴学习笔记/智能客服案例/SpringAIAlibaba/target/classes/com/example/springaialibaba/SpringAiAlibabaApplication.class
  22. BIN
      黄永兴学习笔记/智能客服案例/SpringAIAlibaba/target/classes/com/example/springaialibaba/controller/CustomerServiceController.class
  23. BIN
      黄永兴学习笔记/智能客服案例/SpringAIAlibaba/target/classes/com/example/springaialibaba/controller/Main$1.class
  24. BIN
      黄永兴学习笔记/智能客服案例/SpringAIAlibaba/target/classes/com/example/springaialibaba/controller/Main.class
  25. BIN
      黄永兴学习笔记/智能客服案例/SpringAIAlibaba/target/classes/com/example/springaialibaba/controller/TimeUtils.class
  26. BIN
      黄永兴学习笔记/智能客服案例/SpringAIAlibaba/target/classes/com/example/springaialibaba/pojo/StreamResponse.class
  27. BIN
      黄永兴学习笔记/智能客服案例/SpringAIAlibaba/target/classes/com/example/springaialibaba/service/SpeechSynthesisService.class
  28. 263
      黄永兴学习笔记/智能客服案例/SpringAIAlibaba/target/classes/static/index.html
  29. 0
      黄永兴学习笔记/智能客服案例/SpringAIAlibaba/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst
  30. 4
      黄永兴学习笔记/智能客服案例/SpringAIAlibaba/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst
  31. 0
      黄永兴学习笔记/智能客服案例/SpringAIAlibaba/target/test-classes/com/example/springaialibaba/SpringAiAlibabaApplicationTests.class

BIN
黄永兴学习笔记/4.4黄永兴.docx

BIN
黄永兴学习笔记/Spring AI Alibaba 智能客服+语音合成项目笔记.docx

BIN
黄永兴学习笔记/SpringAIAlibaba.zip

177
黄永兴学习笔记/智能客服demo/SpringAIAlibaba/src/main/java/com/example/springaialibaba/controller/CustomerServiceController.java

@ -1,177 +0,0 @@
package com.example.springaialibaba.controller;
import com.alibaba.cloud.ai.dashscope.agent.DashScopeAgent;
import com.alibaba.cloud.ai.dashscope.agent.DashScopeAgentOptions;
import com.alibaba.dashscope.app.Application;
import com.alibaba.dashscope.app.ApplicationParam;
import com.alibaba.dashscope.app.ApplicationResult;
import com.alibaba.dashscope.app.FlowStreamMode;
import com.alibaba.dashscope.exception.InputRequiredException;
import com.alibaba.dashscope.exception.NoApiKeyException;
import io.reactivex.Flowable;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import reactor.adapter.rxjava.RxJava2Adapter;
import reactor.core.publisher.Flux;
import java.nio.charset.StandardCharsets;
@Slf4j
@RestController
@RequiredArgsConstructor //使用RequiredArgsConstructor注解自动注入final类型的字段
@RequestMapping("/customer")
public class CustomerServiceController {
//注入Spring AI Alibaba Agent
//@Autowired
private final DashScopeAgent dashScopeAgent;
/**
* 智能客服接口非流式同步获取结果
* @param question 用户问题
* @return 知识库标准答案
*/
@GetMapping("/service")
public String customerService(@RequestParam("question") String question) {
try {
/*
DashScopeAgentOptions 是工作流 / 智能体的调用参数它只负责触发工作流不负责修改大模型的推理参数
因为 Spring AI Alibaba 的自动配置在工作流Agent模式下不生效
*/
//
/* ChatResponse response = dashScopeAgent.call(
new Prompt(question, DashScopeAgentOptions.builder()
.appId("df04e1abf24a416c8702260e88863ac4")
.build()
)
);
return response.getResult().getOutput().getText();*/
ApplicationParam param = ApplicationParam.builder()
// 若没有配置环境变量可用百炼API Key将下行替换为.apiKey("sk-xxx")但不建议在生产环境中直接将API Key硬编码到代码中以减少API Key泄露风险
.apiKey(System.getenv("sk-f7d2253302b547c7a2fe257673cb854b"))
.appId("df04e1abf24a416c8702260e88863ac4")
.prompt(question)
.build();
Application application = new Application();
ApplicationResult result = application.call(param);
return result.getOutput().getText();
} catch (Exception e) {
log.error("客服调用失败", e);
return "抱歉,智能客服暂时无法响应,请联系人工客服。";
}
}
// 官方原生流式调用1:1对照文档
@GetMapping(value = "/service2", produces = MediaType.TEXT_HTML_VALUE + ";charset=UTF-8")
public Flux<String> customerService2(
@RequestParam("question") String question,
HttpServletResponse response
) {
//设置字符编码为UTF-8,解决中文乱码问题
response.setCharacterEncoding("UTF-8");
try {
// ==============================================
// 官方原版写法
// ==============================================
ApplicationParam param = ApplicationParam.builder()
.apiKey("sk-f7d2253302b547c7a2fe257673cb854b") // 你的API Key
.appId("df04e1abf24a416c8702260e88863ac4")
.prompt(question)
.incrementalOutput(true) // 官方流式必须开
.flowStreamMode(FlowStreamMode.MESSAGE_FORMAT) // 设置为流模式
.build();
Application application = new Application();
// 官方流式调用
Flowable<ApplicationResult> resultFlowable = application.streamCall(param);
// RxJava Flowable Spring Flux用于SSE输出
// 正确的流式处理逻辑
System.out.println(resultFlowable);
/* return Flux.create(sink -> {
resultFlowable.subscribe(
res -> {
try {
if (res != null && res.getOutput() != null) {
String content = null;
if ("stop".equals(res.getOutput().getFinishReason())) {
// 流结束
content = res.getOutput().getText();
if (content != null && !content.trim().isEmpty()) {
sink.next("data: " + content + "\n\n");
}
} else {
// 流式输出
if (res.getOutput().getWorkflowMessage() != null &&
res.getOutput().getWorkflowMessage().getMessage() != null) {
content = res.getOutput().getWorkflowMessage()
.getMessage().getContent();
if (content != null && !content.trim().isEmpty()) {
sink.next("data: " + content + "\n\n");
}
}
}
}
} catch (Exception e) {
log.error("处理响应异常", e);
}
},
error -> {
log.error("流式调用异常", error);
sink.next("data: 服务调用异常\n\n");
sink.complete();
},
() -> {
log.info("流式调用完成");
sink.complete();
}
);
});*/
// RxJava适配器 flowable 转换为 flux
return RxJava2Adapter.flowableToFlux(resultFlowable)
.map(this::extractContent)
.filter(content -> content != null && !content.trim().isEmpty())
.map(content -> "data: " + content + "\n\n");
} catch (Exception e) {
log.error("客服调用失败", e);
return Flux.error(e);
}
}
// ApplicationResult 中提取内容
private String extractContent(ApplicationResult res) {
if (res == null || res.getOutput() == null) return null;
if ("stop".equals(res.getOutput().getFinishReason())) {
return res.getOutput().getText();
} else if (res.getOutput().getWorkflowMessage() != null &&
res.getOutput().getWorkflowMessage().getMessage() != null) {
return res.getOutput().getWorkflowMessage().getMessage().getContent();
}
return null;
}
}

BIN
黄永兴学习笔记/智能客服demo/SpringAIAlibaba/target/classes/com/example/springaialibaba/SpringAiAlibabaApplication.class

BIN
黄永兴学习笔记/智能客服demo/SpringAIAlibaba/target/classes/com/example/springaialibaba/controller/CustomerServiceController.class

0
黄永兴学习笔记/智能客服demo/SpringAIAlibaba/.mvn/wrapper/maven-wrapper.properties → 黄永兴学习笔记/智能客服案例/SpringAIAlibaba/.mvn/wrapper/maven-wrapper.properties

0
黄永兴学习笔记/智能客服demo/SpringAIAlibaba/SpringAIAlibaba.iml → 黄永兴学习笔记/智能客服案例/SpringAIAlibaba/SpringAIAlibaba.iml

8
黄永兴学习笔记/智能客服demo/SpringAIAlibaba/pom.xml → 黄永兴学习笔记/智能客服案例/SpringAIAlibaba/pom.xml

@ -84,10 +84,15 @@
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Spring WebFlux 流式核心 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>io.projectreactor.addons</groupId>
<artifactId>reactor-adapter</artifactId>
<version>3.5.0</version>
</dependency>
<dependency>
@ -113,6 +118,7 @@
</dependencies>
<build>

0
黄永兴学习笔记/智能客服demo/SpringAIAlibaba/src/main/java/com/example/springaialibaba/SpringAiAlibabaApplication.java → 黄永兴学习笔记/智能客服案例/SpringAIAlibaba/src/main/java/com/example/springaialibaba/SpringAiAlibabaApplication.java

230
黄永兴学习笔记/智能客服案例/SpringAIAlibaba/src/main/java/com/example/springaialibaba/controller/CustomerServiceController.java

@ -0,0 +1,230 @@
package com.example.springaialibaba.controller;
import com.alibaba.cloud.ai.dashscope.agent.DashScopeAgent;
import com.alibaba.dashscope.app.*;
import com.example.springaialibaba.pojo.StreamResponse;
import com.example.springaialibaba.service.SpeechSynthesisService;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.reactivex.Flowable;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import java.nio.ByteBuffer;
import java.util.Base64;
import java.util.UUID;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicInteger;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
import reactor.adapter.rxjava.RxJava2Adapter;
import reactor.adapter.rxjava.RxJava3Adapter;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@Slf4j
@RestController
@RequiredArgsConstructor //使用RequiredArgsConstructor注解自动注入final类型的字段
@RequestMapping("/customer")
public class CustomerServiceController {
//注入Spring AI Alibaba Agent
//@Autowired
private final DashScopeAgent dashScopeAgent;
private final SpeechSynthesisService speechSynthesisService;
/**
* 智能客服接口非流式同步获取结果
* @param question 用户问题
* @return 知识库标准答案
*/
@GetMapping("/service")
public String customerServiceController(@RequestParam("question") String question) {
try {
/*
DashScopeAgentOptions 是工作流 / 智能体的调用参数它只负责触发工作流不负责修改大模型的推理参数
因为 Spring AI Alibaba 的自动配置在工作流Agent模式下不生效
*/
//
/* ChatResponse response = dashScopeAgent.call(
new Prompt(question, DashScopeAgentOptions.builder()
.appId("df04e1abf24a416c8702260e88863ac4")
.build()
)
);
return response.getResult().getOutput().getText();*/
ApplicationParam param = ApplicationParam.builder()
// 若没有配置环境变量可用百炼API Key将下行替换为.apiKey("sk-xxx")但不建议在生产环境中直接将API Key硬编码到代码中以减少API Key泄露风险
.apiKey(System.getenv("sk-f7d2253302b547c7a2fe257673cb854b"))
.appId("df04e1abf24a416c8702260e88863ac4")
.prompt(question)
.build();
Application application = new Application();
ApplicationResult result = application.call(param);
return result.getOutput().getText();
} catch (Exception e) {
log.error("客服调用失败", e);
return "抱歉,智能客服暂时无法响应,请联系人工客服。";
}
}
// 官方原生流式调用1:1对照文档
@GetMapping(value = "/service2", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> customerService2Controller(
@RequestParam("question") String question,
HttpServletResponse response
) {
//设置字符编码为UTF-8,解决中文乱码问题
response.setCharacterEncoding("UTF-8");
ObjectMapper objectMapper = new ObjectMapper();
AtomicInteger counter = new AtomicInteger(0);
try {
// 官方原版写法
ApplicationParam param = ApplicationParam.builder()
.apiKey("sk-f7d2253302b547c7a2fe257673cb854b")
.appId("df04e1abf24a416c8702260e88863ac4")
.prompt(question)
.flowStreamMode(FlowStreamMode.MESSAGE_FORMAT)
.build();
Application application = new Application();
// 官方流式调用
Flowable<ApplicationResult> resultFlowable = application.streamCall(param);
// RxJava适配器 flowable 转换为 flux Mono是单元素的Flux
return RxJava2Adapter.flowableToFlux(resultFlowable)
.flatMap(res -> {
String content = extractContent(res);
if (content != null && !content.trim().isEmpty()) {
try {
String id = UUID.randomUUID().toString().substring(0, 8);
int seq = counter.incrementAndGet();
String jsonData = objectMapper.writeValueAsString(new StreamResponse(id, seq, content));
return Mono.just(jsonData);
} catch (Exception e) {
log.error("JSON序列化失败", e);
return Mono.empty();
}
}
return Mono.empty();
});
} catch (Exception e) {
log.error("客服调用失败", e);
return Flux.error(e);
}
}
// 语音合成接口 - 单向流式返回音频流数据
@GetMapping(value = "/synthesize", produces = "audio/wav")
public StreamingResponseBody synthesizeSpeech(@RequestParam("text") String text) {
log.info("开始语音合成,文本: {}", text);
return outputStream -> {
CountDownLatch latch = new CountDownLatch(1);
try {
Flux<byte[]> audioFlux = speechSynthesisService.streamSynthesize(text);
audioFlux.subscribe(
bytes -> {
try {
log.info("Controller收到音频数据,大小: {} 字节,写入输出流", bytes.length);
outputStream.write(bytes);
outputStream.flush();
log.info("已刷新输出流");
} catch (Exception e) {
log.error("写入音频数据失败", e);
}
},
error -> {
log.error("音频流处理错误", error);
latch.countDown();
},
() -> {
log.info("音频数据传输完成");
latch.countDown();
}
);
log.info("等待音频流完成...");
latch.await();
log.info("音频流完成,结束请求");
} catch (Exception e) {
log.error("语音合成失败", e);
}
};
}
// SSE推送音频数据接口
@GetMapping(value = "/synthesize-sse", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> synthesizeSpeechSSE(@RequestParam("text") String text) {
log.info("开始SSE语音合成,文本: {}", text);
Flux<byte[]> audioFlux = speechSynthesisService.streamSynthesize(text);
AtomicInteger seq = new AtomicInteger(0);
return audioFlux
.map(bytes -> {
String base64Data = Base64.getEncoder().encodeToString(bytes);
int currentSeq = seq.incrementAndGet();
String jsonData = String.format("{\"seq\":%d,\"data\":\"%s\",\"size\":%d}",
currentSeq, base64Data, bytes.length);
log.info("SSE推送音频数据块 [{}], 大小: {} 字节", currentSeq, bytes.length);
return "data: " + jsonData + "\n\n";
})
.doOnComplete(() -> {
log.info("SSE音频推送完成");
})
.doOnError(error -> {
log.error("SSE音频推送错误", error);
});
}
// ApplicationResult 中提取内容
private String extractContent(ApplicationResult res) {
if (res == null || res.getOutput() == null) return null;
if (res.getOutput().getWorkflowMessage() != null) {
WorkflowMessage workflowMessage = res.getOutput().getWorkflowMessage();
if (workflowMessage != null && workflowMessage.getMessage() != null) {
String nodeId = workflowMessage.getNodeId();
String content = workflowMessage.getMessage().getContent();
// 区分不同的nodeId
if ("Output_emZG".equals(nodeId)) {
// 流程输出节点返回内容
return content;
} else if ("End_mOtD".equals(nodeId)) {
// 结束节点不返回内容
log.debug("流式传输结束,收到完整结果,不返回");
return null;
} else {
// 其他节点返回内容
return content;
}
}
}
return null;
}
}

97
黄永兴学习笔记/智能客服案例/SpringAIAlibaba/src/main/java/com/example/springaialibaba/controller/Main.java

@ -0,0 +1,97 @@
package com.example.springaialibaba.controller;
import com.alibaba.dashscope.audio.tts.SpeechSynthesisResult;
import com.alibaba.dashscope.audio.ttsv2.SpeechSynthesisAudioFormat;
import com.alibaba.dashscope.audio.ttsv2.SpeechSynthesisParam;
import com.alibaba.dashscope.audio.ttsv2.SpeechSynthesizer;
import com.alibaba.dashscope.common.ResultCallback;
import com.alibaba.dashscope.utils.Constants;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
class TimeUtils {
private static final DateTimeFormatter formatter =
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS");
public static String getTimestamp() {
return LocalDateTime.now().format(formatter);
}
}
public class Main {
private static String[] textArray = {"流式文本语音合成SDK,",
"可以将输入的文本", "合成为语音二进制数据,", "相比于非流式语音合成,",
"流式合成的优势在于实时性", "更强。用户在输入文本的同时",
"可以听到接近同步的语音输出,", "极大地提升了交互体验,",
"减少了用户等待时间。", "适用于调用大规模", "语言模型(LLM),以",
"流式输入文本的方式", "进行语音合成的场景。"};
private static String model = "cosyvoice-v3-flash"; // 模型
private static String voice = "longanyang"; // 音色
public static void streamAudioDataToSpeaker() {
// 配置回调函数
ResultCallback<SpeechSynthesisResult> callback = new ResultCallback<SpeechSynthesisResult>() {
@Override
public void onEvent(SpeechSynthesisResult result) {
// System.out.println("收到消息: " + result);
if (result.getAudioFrame() != null) {
// 此处实现处理音频数据的逻辑
System.out.println(TimeUtils.getTimestamp() + " 收到音频");
}
}
@Override
public void onComplete() {
System.out.println(TimeUtils.getTimestamp() + " 收到Complete,语音合成结束");
}
@Override
public void onError(Exception e) {
System.out.println("出现异常:" + e.toString());
}
};
// 请求参数
SpeechSynthesisParam param =
SpeechSynthesisParam.builder()
// 新加坡和北京地域的API Key不同获取API Keyhttps://help.aliyun.com/zh/model-studio/get-api-key
// 若没有配置环境变量请用百炼API Key将下行替换为.apiKey("sk-xxx")
.apiKey("sk-f7d2253302b547c7a2fe257673cb854b")
.model(model)
.voice(voice)
.format(SpeechSynthesisAudioFormat
.PCM_22050HZ_MONO_16BIT) // 流式合成使用PCM或者MP3
.build();
SpeechSynthesizer synthesizer = new SpeechSynthesizer(param, callback);
// 带Callback的call方法将不会阻塞当前线程
try {
for (String text : textArray) {
// 发送文本片段在回调接口的onEvent方法中实时获取二进制音频
synthesizer.streamingCall(text);
}
// 等待结束流式语音合成
synthesizer.streamingComplete();
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
// 任务结束关闭websocket连接
synthesizer.getDuplexApi().close(1000, "bye");
}
// 首次发送文本时需建立 WebSocket 连接因此首包延迟会包含连接建立的耗时
System.out.println(
"[Metric] requestId为:"
+ synthesizer.getLastRequestId()
+ ",首包延迟(毫秒)为:"
+ synthesizer.getFirstPackageDelay());
}
public static void main(String[] args) {
// 以下为北京地域url若使用新加坡地域的模型需将url替换为wss://dashscope-intl.aliyuncs.com/api-ws/v1/inference
Constants.baseWebsocketApiUrl = "wss://dashscope.aliyuncs.com/api-ws/v1/inference";
streamAudioDataToSpeaker();
System.exit(0);
}
}

118
黄永兴学习笔记/智能客服案例/SpringAIAlibaba/src/main/java/com/example/springaialibaba/controller/SpeechSynthesisController.java

@ -0,0 +1,118 @@
package com.example.springaialibaba.controller;
import com.alibaba.dashscope.audio.tts.SpeechSynthesisParam;
import com.alibaba.dashscope.audio.tts.SpeechSynthesizer;
import com.alibaba.dashscope.audio.tts.SpeechSynthesisResult;
import com.alibaba.dashscope.exception.NoApiKeyException;
import com.alibaba.dashscope.utils.Constants;
import io.reactivex.Flowable;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import reactor.adapter.rxjava.RxJava2Adapter;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Base64;
@Slf4j
@RestController
@RequestMapping("/speech")
public class SpeechSynthesisController {
private static final String MODEL = "cosyvoice-v3-flash";
private static final String VOICE = "longanyang";
static {
// 北京地域url若使用新加坡地域的模型需将url替换为wss://dashscope-intl.aliyuncs.com/api-ws/v1/inference
Constants.baseWebsocketApiUrl = "wss://dashscope.aliyuncs.com/api-ws/v1/inference";
}
@GetMapping(value = "/synthesize", produces = MediaType.APPLICATION_OCTET_STREAM_VALUE)
public void synthesizeAudio(
@RequestParam("text") String text,
HttpServletResponse response
) throws NoApiKeyException, IOException {
// 设置响应头
response.setContentType("audio/wav");
response.setHeader("Content-Disposition", "attachment; filename=output.wav");
// 请求参数
SpeechSynthesisParam param = SpeechSynthesisParam.builder()
.apiKey(System.getenv("DASHSCOPE_API_KEY"))
.model(MODEL)
.voice(VOICE)
.build();
SpeechSynthesizer synthesizer = new SpeechSynthesizer(param, null);
// 流式获取音频数据
Flowable<SpeechSynthesisResult> resultFlowable = synthesizer.callAsFlowable(text);
// 处理音频数据
OutputStream outputStream = response.getOutputStream();
resultFlowable.blockingForEach(result -> {
if (result.getAudioFrame() != null) {
try {
// 音频数据是Base64编码的需要解码
byte[] audioData = Base64.getDecoder().decode(result.getAudioFrame().getAudio());
outputStream.write(audioData);
outputStream.flush();
log.debug("写入音频数据: {} bytes", audioData.length);
} catch (IOException e) {
log.error("写入音频数据失败", e);
}
}
});
// 关闭连接和输出流
synthesizer.getDuplexApi().close(1000, "bye");
outputStream.close();
// 打印首包延迟
log.info("[Metric] requestId为:{}", synthesizer.getLastRequestId());
log.info("首包延迟(毫秒)为:{}", synthesizer.getFirstPackageDelay());
}
@GetMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> streamAudio(
@RequestParam("text") String text
) throws NoApiKeyException {
// 请求参数
SpeechSynthesisParam param = SpeechSynthesisParam.builder()
.apiKey(System.getenv("DASHSCOPE_API_KEY"))
.model(MODEL)
.voice(VOICE)
.build();
SpeechSynthesizer synthesizer = new SpeechSynthesizer(param, null);
// 流式获取音频数据
Flowable<SpeechSynthesisResult> resultFlowable = synthesizer.callAsFlowable(text);
// 转换为Flux并返回
return RxJava2Adapter.flowableToFlux(resultFlowable)
.flatMap(result -> {
if (result.getAudioFrame() != null) {
String audioBase64 = result.getAudioFrame().getAudio();
return Mono.just("data: " + audioBase64 + "\n\n");
}
return Mono.empty();
})
.doOnTerminate(() -> {
// 关闭连接
synthesizer.getDuplexApi().close(1000, "bye");
log.info("[Metric] requestId为:{}", synthesizer.getLastRequestId());
log.info("首包延迟(毫秒)为:{}", synthesizer.getFirstPackageDelay());
});
}
}

14
黄永兴学习笔记/智能客服案例/SpringAIAlibaba/src/main/java/com/example/springaialibaba/pojo/StreamResponse.java

@ -0,0 +1,14 @@
package com.example.springaialibaba.pojo;
// 流式响应数据结构
public class StreamResponse {
public String id;
public int seq;
public String text;
public StreamResponse(String id, int seq, String text) {
this.id = id;
this.seq = seq;
this.text = text;
}
}

107
黄永兴学习笔记/智能客服案例/SpringAIAlibaba/src/main/java/com/example/springaialibaba/service/AudioGenerationService.java

@ -0,0 +1,107 @@
package com.example.springaialibaba.service;
import com.alibaba.dashscope.audio.tts.SpeechSynthesisResult;
import com.alibaba.dashscope.audio.ttsv2.SpeechSynthesisParam;
import com.alibaba.dashscope.audio.ttsv2.SpeechSynthesizer;
import com.alibaba.dashscope.common.ResultCallback;
import com.alibaba.dashscope.exception.NoApiKeyException;
import com.alibaba.dashscope.utils.Constants;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
@Slf4j
@Service
public class AudioGenerationService {
private static final String MODEL = "cosyvoice-v3-flash";
private static final String VOICE = "longanyang";
static {
// 设置WebSocket API地址北京地域
Constants.baseWebsocketApiUrl = "wss://dashscope.aliyuncs.com/api-ws/v1/inference";
}
/**
* 生成文本的音频数据
* @param text 要转换为音频的文本
* @return 音频数据的Flux流
*/
public Flux<ByteBuffer> generateAudio(String text) {
return Mono.fromCallable(() -> {
try {
SpeechSynthesisParam param = SpeechSynthesisParam.builder()
.apiKey("sk-f7d2253302b547c7a2fe257673cb854b")
.model(MODEL)
.voice(VOICE)
.build();
SpeechSynthesizer synthesizer = new SpeechSynthesizer(param, null);
List<ByteBuffer> audioChunks = new ArrayList<>();
synthesizer.callAsFlowable(text).blockingForEach(result -> {
if (result.getAudioFrame() != null) {
ByteBuffer audioData = ByteBuffer.wrap(result.getAudioFrame());
audioChunks.add(audioData);
log.debug("收到音频数据块,大小: {} bytes", result.getAudioFrame().length);
}
});
synthesizer.getDuplexApi().close(1000, "bye");
log.info("音频生成完成,首包延迟: {} ms", synthesizer.getFirstPackageDelay());
return Flux.fromIterable(audioChunks);
} catch (NoApiKeyException e) {
log.error("API Key错误", e);
return Flux.error(e);
} catch (Exception e) {
log.error("音频生成失败", e);
return Flux.error(e);
}
}).flatMapMany(flux -> flux);
}
/**
* 生成单个音频数据块用于实时流式传输
* @param text 要转换为音频的文本
* @return 音频数据的Mono
*/
public Mono<ByteBuffer> generateAudioChunk(String text) {
return Mono.fromCallable(() -> {
try {
SpeechSynthesisParam param = SpeechSynthesisParam.builder()
.apiKey("sk-f7d2253302b547c7a2fe257673cb854b")
.model(MODEL)
.voice(VOICE)
.build();
SpeechSynthesizer synthesizer = new SpeechSynthesizer(param, null);
ByteBuffer audioData = null;
synthesizer.callAsFlowable(text).blockingForEach(result -> {
if (result.getAudioFrame() != null) {
audioData = ByteBuffer.wrap(result.getAudioFrame());
log.debug("生成音频数据块,大小: {} bytes", result.getAudioFrame().length);
}
});
synthesizer.getDuplexApi().close(1000, "bye");
return audioData;
} catch (Exception e) {
log.error("音频生成失败", e);
throw new RuntimeException("音频生成失败", e);
}
});
}
}

82
黄永兴学习笔记/智能客服案例/SpringAIAlibaba/src/main/java/com/example/springaialibaba/service/SpeechSynthesisService.java

@ -0,0 +1,82 @@
package com.example.springaialibaba.service;
import com.alibaba.dashscope.audio.tts.SpeechSynthesisResult;
import com.alibaba.dashscope.audio.ttsv2.SpeechSynthesisParam;
import com.alibaba.dashscope.audio.ttsv2.SpeechSynthesizer;
import com.alibaba.dashscope.utils.Constants;
import io.reactivex.Flowable;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import reactor.adapter.rxjava.RxJava2Adapter;
import reactor.core.publisher.Flux;
import java.nio.ByteBuffer;
@Slf4j
@Service
public class SpeechSynthesisService {
private static final String MODEL = "cosyvoice-v3-flash";
private static final String VOICE = "longanyang";
public Flux<byte[]> streamSynthesize(String text) {
try {
Constants.baseWebsocketApiUrl = "wss://dashscope.aliyuncs.com/api-ws/v1/inference";
SpeechSynthesisParam param = SpeechSynthesisParam.builder()
.apiKey("sk-f7d2253302b547c7a2fe257673cb854b")
.model(MODEL)
.voice(VOICE)
.build();
SpeechSynthesizer synthesizer = new SpeechSynthesizer(param, null);
Flowable<SpeechSynthesisResult> resultFlowable = synthesizer.callAsFlowable(text);
Flux<byte[]> audioFlux = RxJava2Adapter.flowableToFlux(resultFlowable)
.doOnNext(result -> {
log.info("收到SpeechSynthesisResult, audioFrame: {}, frameSize: {}",
result.getAudioFrame() != null,
result.getAudioFrame() != null ? result.getAudioFrame().remaining() : 0);
})
.filter(result -> {
boolean hasAudio = result.getAudioFrame() != null;
if (!hasAudio) {
log.info("跳过无音频帧的结果");
}
return hasAudio;
})
.map(result -> {
ByteBuffer buffer = result.getAudioFrame();
byte[] bytes = new byte[buffer.remaining()];
buffer.get(bytes);
log.info("处理音频数据块,大小: {} 字节", bytes.length);
return bytes;
})
.doOnComplete(() -> {
log.info("语音合成完成,requestId: {}, 首包延迟: {}ms",
synthesizer.getLastRequestId(),
synthesizer.getFirstPackageDelay());
try {
synthesizer.getDuplexApi().close(1000, "bye");
} catch (Exception e) {
log.error("关闭连接失败", e);
}
})
.doOnError(error -> {
log.error("语音合成错误", error);
try {
synthesizer.getDuplexApi().close(1000, "bye");
} catch (Exception e) {
log.error("关闭连接失败", e);
}
});
return audioFlux;
} catch (Exception e) {
log.error("语音合成失败", e);
return Flux.error(e);
}
}
}

1
黄永兴学习笔记/智能客服demo/SpringAIAlibaba/src/main/resources/application.yml → 黄永兴学习笔记/智能客服案例/SpringAIAlibaba/src/main/resources/application.yml

@ -11,3 +11,4 @@ spring:
api-key: sk-f7d2253302b547c7a2fe257673cb854b
# 业务空间ID
workspace-id: llm-fuczq7vkh8vyz716

263
黄永兴学习笔记/智能客服案例/SpringAIAlibaba/src/main/resources/static/index.html

@ -0,0 +1,263 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>智能客服 - 文本 + 语音</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 1000px;
margin: 30px auto;
padding: 20px;
}
.container {
border: 1px solid #ddd;
border-radius: 8px;
padding: 30px;
}
h1 {
color: #333;
margin-bottom: 30px;
}
.input-group {
margin-bottom: 20px;
}
textarea {
width: 100%;
height: 100px;
padding: 12px;
font-size: 16px;
border: 1px solid #ccc;
border-radius: 4px;
resize: vertical;
box-sizing: border-box;
}
button {
padding: 12px 30px;
font-size: 16px;
background-color: #007bff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
button:hover {
background-color: #0056b3;
}
button:disabled {
background-color: #ccc;
cursor: not-allowed;
}
.output-section {
margin-top: 30px;
border-top: 1px solid #eee;
padding-top: 20px;
}
.output-title {
font-size: 18px;
font-weight: bold;
margin-bottom: 15px;
color: #333;
}
.output-text {
padding: 15px;
background-color: #f8f9fa;
border-radius: 4px;
border: 1px solid #e9ecef;
min-height: 100px;
font-size: 16px;
line-height: 1.8;
color: #333;
white-space: pre-wrap;
}
.status {
margin-top: 20px;
padding: 15px;
background-color: #d1ecf1;
border-radius: 4px;
color: #0c5460;
font-size: 16px;
}
audio {
margin-top: 20px;
width: 100%;
}
.example-btn {
margin-left: 10px;
background-color: #28a745;
}
.example-btn:hover {
background-color: #218838;
}
</style>
</head>
<body>
<div class="container">
<h1>🤖 智能客服 - 文本 + 语音</h1>
<div class="input-group">
<label style="display: block; margin-bottom: 10px; font-size: 16px;">请输入您的问题:</label>
<textarea id="questionInput" placeholder="请输入您的问题,例如:什么是半仓?"></textarea>
</div>
<div class="input-group">
<button id="generateBtn" onclick="askQuestion()">提问</button>
<button class="example-btn" onclick="fillExample()">示例问题</button>
</div>
<div class="status" id="status">
状态:等待提问
</div>
<div class="output-section">
<div class="output-title">📝 回答内容</div>
<div class="output-text" id="outputText">
(回答将在这里显示)
</div>
</div>
<audio id="audioPlayer" controls></audio>
</div>
<script>
const exampleQuestion = "什么是半仓?";
function fillExample() {
document.getElementById('questionInput').value = exampleQuestion;
}
async function askQuestion() {
const question = document.getElementById('questionInput').value.trim();
const statusDiv = document.getElementById('status');
const outputText = document.getElementById('outputText');
const audioPlayer = document.getElementById('audioPlayer');
const generateBtn = document.getElementById('generateBtn');
if (!question) {
alert('请输入您的问题!');
return;
}
generateBtn.disabled = true;
outputText.textContent = '';
statusDiv.textContent = '状态:正在获取回答...';
console.log('开始提问:', question);
let fullText = '';
let eventSource = null;
try {
eventSource = new EventSource(`/customer/service2?question=${encodeURIComponent(question)}`);
eventSource.onmessage = (event) => {
console.log('收到原始数据:', event.data);
const dataStr = event.data;
if (!dataStr || dataStr.trim() === '') {
console.log('跳过空数据');
return;
}
try {
const data = JSON.parse(dataStr);
const text = data.text;
if (text) {
fullText += text;
outputText.textContent += text;
outputText.scrollTop = outputText.scrollHeight;
statusDiv.textContent = `状态:正在获取回答... (已收到 ${fullText.length} 字)`;
console.log('解析到文本:', text);
}
} catch (parseError) {
console.error('JSON解析失败:', parseError, '原始数据:', dataStr);
}
};
eventSource.onerror = (error) => {
console.log('EventSource 连接关闭:', error);
eventSource.close();
if (fullText.length > 0) {
statusDiv.textContent = '状态:回答获取完成,正在生成语音...';
console.log('完整回答:', fullText);
generateAndPlayAudio(fullText);
} else {
statusDiv.textContent = '状态:未获取到回答';
generateBtn.disabled = false;
}
};
} catch (error) {
console.error('获取回答错误:', error);
statusDiv.textContent = `状态:获取回答失败 - ${error.message}`;
generateBtn.disabled = false;
}
}
async function generateAndPlayAudio(text) {
const statusDiv = document.getElementById('status');
const audioPlayer = document.getElementById('audioPlayer');
const generateBtn = document.getElementById('generateBtn');
statusDiv.textContent = '状态:正在生成语音...';
console.log('开始语音合成,文本长度:', text.length);
try {
const url = `/customer/synthesize?text=${encodeURIComponent(text)}`;
audioPlayer.src = url;
audioPlayer.onloadstart = () => {
statusDiv.textContent = '状态:开始接收音频流...';
console.log('开始接收音频流');
};
audioPlayer.onloadeddata = () => {
statusDiv.textContent = '状态:数据加载中,准备播放...';
console.log('音频数据加载完成,可以开始播放');
};
audioPlayer.onplaying = () => {
statusDiv.textContent = '状态:正在播放(流式)';
console.log('开始播放音频');
};
audioPlayer.onwaiting = () => {
statusDiv.textContent = '状态:缓冲中...';
};
audioPlayer.oncanplay = () => {
statusDiv.textContent = '状态:正在播放(流畅)';
};
audioPlayer.onended = () => {
statusDiv.textContent = '状态:播放完成';
generateBtn.disabled = false;
console.log('播放完成');
};
audioPlayer.onerror = (e) => {
console.error('音频播放错误:', e);
statusDiv.textContent = `状态:音频播放失败 - ${audioPlayer.error ? audioPlayer.error.message : '未知错误'}`;
generateBtn.disabled = false;
};
audioPlayer.play();
console.log('已调用 audio.play()');
} catch (error) {
console.error('语音合成错误:', error);
statusDiv.textContent = `状态:出错了 - ${error.message}`;
generateBtn.disabled = false;
}
}
window.onload = () => {
console.log('页面加载完成');
};
</script>
</body>
</html>

0
黄永兴学习笔记/智能客服demo/SpringAIAlibaba/src/test/java/com/example/springaialibaba/SpringAiAlibabaApplicationTests.java → 黄永兴学习笔记/智能客服案例/SpringAIAlibaba/src/test/java/com/example/springaialibaba/SpringAiAlibabaApplicationTests.java

1
黄永兴学习笔记/智能客服demo/SpringAIAlibaba/target/classes/application.yml → 黄永兴学习笔记/智能客服案例/SpringAIAlibaba/target/classes/application.yml

@ -11,3 +11,4 @@ spring:
api-key: sk-f7d2253302b547c7a2fe257673cb854b
# 业务空间ID
workspace-id: llm-fuczq7vkh8vyz716

BIN
黄永兴学习笔记/智能客服案例/SpringAIAlibaba/target/classes/com/example/springaialibaba/SpringAiAlibabaApplication.class

BIN
黄永兴学习笔记/智能客服案例/SpringAIAlibaba/target/classes/com/example/springaialibaba/controller/CustomerServiceController.class

BIN
黄永兴学习笔记/智能客服案例/SpringAIAlibaba/target/classes/com/example/springaialibaba/controller/Main$1.class

BIN
黄永兴学习笔记/智能客服案例/SpringAIAlibaba/target/classes/com/example/springaialibaba/controller/Main.class

BIN
黄永兴学习笔记/智能客服案例/SpringAIAlibaba/target/classes/com/example/springaialibaba/controller/TimeUtils.class

BIN
黄永兴学习笔记/智能客服案例/SpringAIAlibaba/target/classes/com/example/springaialibaba/pojo/StreamResponse.class

BIN
黄永兴学习笔记/智能客服案例/SpringAIAlibaba/target/classes/com/example/springaialibaba/service/SpeechSynthesisService.class

263
黄永兴学习笔记/智能客服案例/SpringAIAlibaba/target/classes/static/index.html

@ -0,0 +1,263 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>智能客服 - 文本 + 语音</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 1000px;
margin: 30px auto;
padding: 20px;
}
.container {
border: 1px solid #ddd;
border-radius: 8px;
padding: 30px;
}
h1 {
color: #333;
margin-bottom: 30px;
}
.input-group {
margin-bottom: 20px;
}
textarea {
width: 100%;
height: 100px;
padding: 12px;
font-size: 16px;
border: 1px solid #ccc;
border-radius: 4px;
resize: vertical;
box-sizing: border-box;
}
button {
padding: 12px 30px;
font-size: 16px;
background-color: #007bff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
button:hover {
background-color: #0056b3;
}
button:disabled {
background-color: #ccc;
cursor: not-allowed;
}
.output-section {
margin-top: 30px;
border-top: 1px solid #eee;
padding-top: 20px;
}
.output-title {
font-size: 18px;
font-weight: bold;
margin-bottom: 15px;
color: #333;
}
.output-text {
padding: 15px;
background-color: #f8f9fa;
border-radius: 4px;
border: 1px solid #e9ecef;
min-height: 100px;
font-size: 16px;
line-height: 1.8;
color: #333;
white-space: pre-wrap;
}
.status {
margin-top: 20px;
padding: 15px;
background-color: #d1ecf1;
border-radius: 4px;
color: #0c5460;
font-size: 16px;
}
audio {
margin-top: 20px;
width: 100%;
}
.example-btn {
margin-left: 10px;
background-color: #28a745;
}
.example-btn:hover {
background-color: #218838;
}
</style>
</head>
<body>
<div class="container">
<h1>🤖 智能客服 - 文本 + 语音</h1>
<div class="input-group">
<label style="display: block; margin-bottom: 10px; font-size: 16px;">请输入您的问题:</label>
<textarea id="questionInput" placeholder="请输入您的问题,例如:什么是半仓?"></textarea>
</div>
<div class="input-group">
<button id="generateBtn" onclick="askQuestion()">提问</button>
<button class="example-btn" onclick="fillExample()">示例问题</button>
</div>
<div class="status" id="status">
状态:等待提问
</div>
<div class="output-section">
<div class="output-title">📝 回答内容</div>
<div class="output-text" id="outputText">
(回答将在这里显示)
</div>
</div>
<audio id="audioPlayer" controls></audio>
</div>
<script>
const exampleQuestion = "什么是半仓?";
function fillExample() {
document.getElementById('questionInput').value = exampleQuestion;
}
async function askQuestion() {
const question = document.getElementById('questionInput').value.trim();
const statusDiv = document.getElementById('status');
const outputText = document.getElementById('outputText');
const audioPlayer = document.getElementById('audioPlayer');
const generateBtn = document.getElementById('generateBtn');
if (!question) {
alert('请输入您的问题!');
return;
}
generateBtn.disabled = true;
outputText.textContent = '';
statusDiv.textContent = '状态:正在获取回答...';
console.log('开始提问:', question);
let fullText = '';
let eventSource = null;
try {
eventSource = new EventSource(`/customer/service2?question=${encodeURIComponent(question)}`);
eventSource.onmessage = (event) => {
console.log('收到原始数据:', event.data);
const dataStr = event.data;
if (!dataStr || dataStr.trim() === '') {
console.log('跳过空数据');
return;
}
try {
const data = JSON.parse(dataStr);
const text = data.text;
if (text) {
fullText += text;
outputText.textContent += text;
outputText.scrollTop = outputText.scrollHeight;
statusDiv.textContent = `状态:正在获取回答... (已收到 ${fullText.length} 字)`;
console.log('解析到文本:', text);
}
} catch (parseError) {
console.error('JSON解析失败:', parseError, '原始数据:', dataStr);
}
};
eventSource.onerror = (error) => {
console.log('EventSource 连接关闭:', error);
eventSource.close();
if (fullText.length > 0) {
statusDiv.textContent = '状态:回答获取完成,正在生成语音...';
console.log('完整回答:', fullText);
generateAndPlayAudio(fullText);
} else {
statusDiv.textContent = '状态:未获取到回答';
generateBtn.disabled = false;
}
};
} catch (error) {
console.error('获取回答错误:', error);
statusDiv.textContent = `状态:获取回答失败 - ${error.message}`;
generateBtn.disabled = false;
}
}
async function generateAndPlayAudio(text) {
const statusDiv = document.getElementById('status');
const audioPlayer = document.getElementById('audioPlayer');
const generateBtn = document.getElementById('generateBtn');
statusDiv.textContent = '状态:正在生成语音...';
console.log('开始语音合成,文本长度:', text.length);
try {
const url = `/customer/synthesize?text=${encodeURIComponent(text)}`;
audioPlayer.src = url;
audioPlayer.onloadstart = () => {
statusDiv.textContent = '状态:开始接收音频流...';
console.log('开始接收音频流');
};
audioPlayer.onloadeddata = () => {
statusDiv.textContent = '状态:数据加载中,准备播放...';
console.log('音频数据加载完成,可以开始播放');
};
audioPlayer.onplaying = () => {
statusDiv.textContent = '状态:正在播放(流式)';
console.log('开始播放音频');
};
audioPlayer.onwaiting = () => {
statusDiv.textContent = '状态:缓冲中...';
};
audioPlayer.oncanplay = () => {
statusDiv.textContent = '状态:正在播放(流畅)';
};
audioPlayer.onended = () => {
statusDiv.textContent = '状态:播放完成';
generateBtn.disabled = false;
console.log('播放完成');
};
audioPlayer.onerror = (e) => {
console.error('音频播放错误:', e);
statusDiv.textContent = `状态:音频播放失败 - ${audioPlayer.error ? audioPlayer.error.message : '未知错误'}`;
generateBtn.disabled = false;
};
audioPlayer.play();
console.log('已调用 audio.play()');
} catch (error) {
console.error('语音合成错误:', error);
statusDiv.textContent = `状态:出错了 - ${error.message}`;
generateBtn.disabled = false;
}
}
window.onload = () => {
console.log('页面加载完成');
};
</script>
</body>
</html>

0
黄永兴学习笔记/智能客服案例/SpringAIAlibaba/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst

4
黄永兴学习笔记/智能客服案例/SpringAIAlibaba/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst

@ -0,0 +1,4 @@
C:\Users\Mayn\IdeaProjects\StudySpringAI\StudySpringAI\SpringAIAlibaba\src\main\java\com\example\springaialibaba\controller\CustomerServiceController.java
C:\Users\Mayn\IdeaProjects\StudySpringAI\StudySpringAI\SpringAIAlibaba\src\main\java\com\example\springaialibaba\controller\Main.java
C:\Users\Mayn\IdeaProjects\StudySpringAI\StudySpringAI\SpringAIAlibaba\src\main\java\com\example\springaialibaba\service\SpeechSynthesisService.java
C:\Users\Mayn\IdeaProjects\StudySpringAI\StudySpringAI\SpringAIAlibaba\src\main\java\com\example\springaialibaba\SpringAiAlibabaApplication.java

0
黄永兴学习笔记/智能客服demo/SpringAIAlibaba/target/test-classes/com/example/springaialibaba/SpringAiAlibabaApplicationTests.class → 黄永兴学习笔记/智能客服案例/SpringAIAlibaba/target/test-classes/com/example/springaialibaba/SpringAiAlibabaApplicationTests.class

Loading…
Cancel
Save