Compare commits
merge into: majun:master
majun:chenchunxiao
majun:dev
majun:huangyongxing/feature-20260327161252-学习笔记
majun:libolin/feature-20260327153128-学习笔记
majun:master
majun:milestone-20260325-学习笔记
majun:milestone-20260401-学习笔记
majun:tianqingang/feature-20260402182447-学习笔记
majun:wuyifan
majun:wuyingzhao
majun:zanyuping
majun:zhouxinzhong/feature-20260327150147-学习笔记
pull from: majun:huangyongxing/feature-20260327161252-学习笔记
majun:chenchunxiao
majun:dev
majun:huangyongxing/feature-20260327161252-学习笔记
majun:libolin/feature-20260327153128-学习笔记
majun:master
majun:milestone-20260325-学习笔记
majun:milestone-20260401-学习笔记
majun:tianqingang/feature-20260402182447-学习笔记
majun:wuyifan
majun:wuyingzhao
majun:zanyuping
majun:zhouxinzhong/feature-20260327150147-学习笔记
13 Commits
master
...
huangyongx
| Author | SHA1 | Message | Date |
|---|---|---|---|
|
|
9c0a8e2d57 |
4.7学习笔记
|
7 days ago |
|
|
2f351aeb1d |
项目案例优化及笔记总结
|
1 week ago |
|
|
38e807600e |
智能客服项目案例
|
2 weeks ago |
|
|
357bdcc002 |
4.1项目案例
|
2 weeks ago |
|
|
d8a23e51b8 |
3.31学习笔记
|
2 weeks ago |
|
|
a4008af72a |
3.30学习笔记
|
2 weeks ago |
|
|
d04a111382 |
3.28学习笔记
|
2 weeks ago |
|
|
2c9ea554f3 |
3.27学习笔记
|
3 weeks ago |
|
|
5759ec7be7 |
3.27股票学习笔记
|
3 weeks ago |
|
|
174aa0bc84 |
3.27股票学习笔记
|
3 weeks ago |
|
|
d049131b30 |
3.27股票学习笔记
|
3 weeks ago |
|
|
4f70f59221 |
3.27学习笔记
|
3 weeks ago |
|
|
7404518e03 |
3.27学习笔记
|
3 weeks ago |
60 changed files with 2391 additions and 0 deletions
-
BIN黄永兴学习笔记/3.26黄永兴.docx
-
BIN黄永兴学习笔记/3.27黄永兴-股票学习笔记.docx
-
BIN黄永兴学习笔记/3.27黄永兴.docx
-
BIN黄永兴学习笔记/3.28黄永兴-股票学习笔记.docx
-
BIN黄永兴学习笔记/3.28黄永兴.docx
-
BIN黄永兴学习笔记/3.30黄永兴-软件功能总结.docx
-
BIN黄永兴学习笔记/3.30黄永兴.docx
-
BIN黄永兴学习笔记/3.31黄永兴-软件功能总结.docx
-
BIN黄永兴学习笔记/3.31黄永兴.docx
-
BIN黄永兴学习笔记/4.1黄永兴-项目案例笔记.docx
-
BIN黄永兴学习笔记/4.4黄永兴.docx
-
BIN黄永兴学习笔记/4.7CosyVoice 高并发调用实战笔记.docx
-
BIN黄永兴学习笔记/Spring AI Alibaba 智能客服+语音合成项目笔记.docx
-
BIN黄永兴学习笔记/SpringAIAlibaba.zip
-
33黄永兴学习笔记/StudySpringAI/SpringAIEmbedding/.gitignore
-
3黄永兴学习笔记/StudySpringAI/SpringAIEmbedding/.mvn/wrapper/maven-wrapper.properties
-
100黄永兴学习笔记/StudySpringAI/SpringAIEmbedding/pom.xml
-
13黄永兴学习笔记/StudySpringAI/SpringAIEmbedding/src/main/java/com/example/springaiembedding/SpringAiEmbeddingApplication.java
-
54黄永兴学习笔记/StudySpringAI/SpringAIEmbedding/src/main/java/com/example/springaiembedding/controller/EmbeddingController.java
-
22黄永兴学习笔记/StudySpringAI/SpringAIEmbedding/src/main/java/com/example/springaiembedding/controller/RagController.java
-
87黄永兴学习笔记/StudySpringAI/SpringAIEmbedding/src/main/java/com/example/springaiembedding/service/RagService.java
-
93黄永兴学习笔记/StudySpringAI/SpringAIEmbedding/src/main/java/com/example/springaiembedding/service/TextSimilarityService.java
-
28黄永兴学习笔记/StudySpringAI/SpringAIEmbedding/src/main/java/com/example/springaiembedding/tool/DocumentLoader.java
-
21黄永兴学习笔记/StudySpringAI/SpringAIEmbedding/src/main/resources/application.properties
-
8黄永兴学习笔记/StudySpringAI/SpringAIEmbedding/src/main/resources/knowledge.txt
-
13黄永兴学习笔记/StudySpringAI/SpringAIEmbedding/src/test/java/com/example/springaiembedding/SpringAiEmbeddingApplicationTests.java
-
33黄永兴学习笔记/StudySpringAI/SpringAIQuickStart/.gitignore
-
3黄永兴学习笔记/StudySpringAI/SpringAIQuickStart/.mvn/wrapper/maven-wrapper.properties
-
104黄永兴学习笔记/StudySpringAI/SpringAIQuickStart/pom.xml
-
13黄永兴学习笔记/StudySpringAI/SpringAIQuickStart/src/main/java/com/example/springaiquickstart/SpringAiQuickStartApplication.java
-
127黄永兴学习笔记/StudySpringAI/SpringAIQuickStart/src/main/java/com/example/springaiquickstart/controller/ChatController.java
-
13黄永兴学习笔记/StudySpringAI/SpringAIQuickStart/src/main/resources/application.properties
-
224黄永兴学习笔记/StudySpringAI/SpringAIQuickStart/src/main/resources/static/index.html
-
13黄永兴学习笔记/StudySpringAI/SpringAIQuickStart/src/test/java/com/example/springaiquickstart/SpringAiQuickStartApplicationTests.java
-
8黄永兴学习笔记/StudySpringAI/StudySpringAI.iml
-
3黄永兴学习笔记/智能客服案例/SpringAIAlibaba/.mvn/wrapper/maven-wrapper.properties
-
10黄永兴学习笔记/智能客服案例/SpringAIAlibaba/SpringAIAlibaba.iml
-
133黄永兴学习笔记/智能客服案例/SpringAIAlibaba/pom.xml
-
13黄永兴学习笔记/智能客服案例/SpringAIAlibaba/src/main/java/com/example/springaialibaba/SpringAiAlibabaApplication.java
-
230黄永兴学习笔记/智能客服案例/SpringAIAlibaba/src/main/java/com/example/springaialibaba/controller/CustomerServiceController.java
-
97黄永兴学习笔记/智能客服案例/SpringAIAlibaba/src/main/java/com/example/springaialibaba/controller/Main.java
-
118黄永兴学习笔记/智能客服案例/SpringAIAlibaba/src/main/java/com/example/springaialibaba/controller/SpeechSynthesisController.java
-
14黄永兴学习笔记/智能客服案例/SpringAIAlibaba/src/main/java/com/example/springaialibaba/pojo/StreamResponse.java
-
107黄永兴学习笔记/智能客服案例/SpringAIAlibaba/src/main/java/com/example/springaialibaba/service/AudioGenerationService.java
-
82黄永兴学习笔记/智能客服案例/SpringAIAlibaba/src/main/java/com/example/springaialibaba/service/SpeechSynthesisService.java
-
14黄永兴学习笔记/智能客服案例/SpringAIAlibaba/src/main/resources/application.yml
-
263黄永兴学习笔记/智能客服案例/SpringAIAlibaba/src/main/resources/static/index.html
-
13黄永兴学习笔记/智能客服案例/SpringAIAlibaba/src/test/java/com/example/springaialibaba/SpringAiAlibabaApplicationTests.java
-
14黄永兴学习笔记/智能客服案例/SpringAIAlibaba/target/classes/application.yml
-
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黄永兴学习笔记/智能客服案例/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
-
BIN黄永兴学习笔记/智能客服案例/SpringAIAlibaba/target/test-classes/com/example/springaialibaba/SpringAiAlibabaApplicationTests.class
@ -0,0 +1,33 @@ |
|||
HELP.md |
|||
target/ |
|||
.mvn/wrapper/maven-wrapper.jar |
|||
!**/src/main/**/target/ |
|||
!**/src/test/**/target/ |
|||
|
|||
### STS ### |
|||
.apt_generated |
|||
.classpath |
|||
.factorypath |
|||
.project |
|||
.settings |
|||
.springBeans |
|||
.sts4-cache |
|||
|
|||
### IntelliJ IDEA ### |
|||
.idea |
|||
*.iws |
|||
*.iml |
|||
*.ipr |
|||
|
|||
### NetBeans ### |
|||
/nbproject/private/ |
|||
/nbbuild/ |
|||
/dist/ |
|||
/nbdist/ |
|||
/.nb-gradle/ |
|||
build/ |
|||
!**/src/main/**/build/ |
|||
!**/src/test/**/build/ |
|||
|
|||
### VS Code ### |
|||
.vscode/ |
|||
@ -0,0 +1,3 @@ |
|||
wrapperVersion=3.3.4 |
|||
distributionType=only-script |
|||
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.14/apache-maven-3.9.14-bin.zip |
|||
@ -0,0 +1,100 @@ |
|||
<?xml version="1.0" encoding="UTF-8"?> |
|||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" |
|||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> |
|||
<modelVersion>4.0.0</modelVersion> |
|||
<parent> |
|||
<groupId>org.springframework.boot</groupId> |
|||
<artifactId>spring-boot-starter-parent</artifactId> |
|||
<version>3.5.13</version> |
|||
<relativePath/> <!-- lookup parent from repository --> |
|||
</parent> |
|||
<groupId>com.example</groupId> |
|||
<artifactId>SpringAIEmbedding</artifactId> |
|||
<version>0.0.1-SNAPSHOT</version> |
|||
<name>SpringAIEmbedding</name> |
|||
<description>SpringAIEmbedding</description> |
|||
<url/> |
|||
<licenses> |
|||
<license/> |
|||
</licenses> |
|||
<developers> |
|||
<developer/> |
|||
</developers> |
|||
<scm> |
|||
<connection/> |
|||
<developerConnection/> |
|||
<tag/> |
|||
<url/> |
|||
</scm> |
|||
<properties> |
|||
<java.version>17</java.version> |
|||
<spring-ai.version>1.1.3</spring-ai.version> |
|||
</properties> |
|||
|
|||
<dependencyManagement> |
|||
<dependencies> |
|||
<dependency> |
|||
<groupId>org.springframework.ai</groupId> |
|||
<artifactId>spring-ai-bom</artifactId> |
|||
<version>1.0.0-SNAPSHOT</version> |
|||
<type>pom</type> |
|||
<scope>import</scope> |
|||
</dependency> |
|||
</dependencies> |
|||
</dependencyManagement> |
|||
|
|||
<dependencies> |
|||
<dependency> |
|||
<groupId>org.springframework.boot</groupId> |
|||
<artifactId>spring-boot-starter-web</artifactId> |
|||
</dependency> |
|||
<!-- Spring AI Zhipuai 模型依赖 --> |
|||
<dependency> |
|||
<groupId>org.springframework.ai</groupId> |
|||
<artifactId>spring-ai-starter-model-zhipuai</artifactId> |
|||
</dependency> |
|||
|
|||
<!-- Spring AI Chat 客户端依赖 中包括TokenTextSplitter、TextReader、Document 等工具类 --> |
|||
<dependency> |
|||
<groupId>org.springframework.ai</groupId> |
|||
<artifactId>spring-ai-client-chat</artifactId> |
|||
<version>1.0.0</version> |
|||
</dependency> |
|||
</dependencies> |
|||
|
|||
|
|||
<!-- 声明仓库,用于获取 Spring AI 以及相关预发布版本 --> |
|||
<repositories> |
|||
<!-- Spring Milestones 仓库 --> |
|||
<repository> |
|||
<id>spring-milestones</id> |
|||
<name>Spring Milestones</name> |
|||
<url>https://repo.spring.io/milestone</url> |
|||
<snapshots> |
|||
<enabled>false</enabled> |
|||
</snapshots> |
|||
</repository> |
|||
<!-- Spring Snapshots 仓库 --> |
|||
<repository> |
|||
<id>spring-snapshots</id> |
|||
<name>Spring Snapshots</name> |
|||
<url>https://repo.spring.io/snapshot</url> |
|||
<releases> |
|||
<enabled>false</enabled> |
|||
</releases> |
|||
<snapshots> |
|||
<enabled>true</enabled> |
|||
</snapshots> |
|||
</repository> |
|||
</repositories> |
|||
|
|||
<build> |
|||
<plugins> |
|||
<plugin> |
|||
<groupId>org.springframework.boot</groupId> |
|||
<artifactId>spring-boot-maven-plugin</artifactId> |
|||
</plugin> |
|||
</plugins> |
|||
</build> |
|||
|
|||
</project> |
|||
@ -0,0 +1,13 @@ |
|||
package com.example.springaiembedding; |
|||
|
|||
import org.springframework.boot.SpringApplication; |
|||
import org.springframework.boot.autoconfigure.SpringBootApplication; |
|||
|
|||
@SpringBootApplication |
|||
public class SpringAiEmbeddingApplication { |
|||
|
|||
public static void main(String[] args) { |
|||
SpringApplication.run(SpringAiEmbeddingApplication.class, args); |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,54 @@ |
|||
package com.example.springaiembedding.controller; |
|||
|
|||
import com.example.springaiembedding.service.TextSimilarityService; |
|||
import jakarta.annotation.Resource; |
|||
import jakarta.servlet.http.HttpServlet; |
|||
import jakarta.servlet.http.HttpServletResponse; |
|||
import org.springframework.ai.embedding.EmbeddingModel; |
|||
import org.springframework.beans.factory.annotation.Autowired; |
|||
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 java.util.Arrays; |
|||
import java.util.Map; |
|||
|
|||
@RestController |
|||
@RequestMapping("/ai") |
|||
public class EmbeddingController { |
|||
|
|||
@Autowired |
|||
private EmbeddingModel embeddingModel; |
|||
|
|||
//对用户传入的文本进行向量化处理,测试embedding模型 |
|||
|
|||
@RequestMapping("/embedding") |
|||
public Map<String,Object> embedding( |
|||
@RequestParam(value = "message",defaultValue = "给我讲个笑话") String message |
|||
){ |
|||
|
|||
// 对用户传入的文本进行向量化处理 |
|||
float[] embedding = embeddingModel.embed(message); |
|||
|
|||
return Map.of("message",message, |
|||
"vector",embedding); |
|||
} |
|||
|
|||
|
|||
@Resource |
|||
private TextSimilarityService textSimilarityService; |
|||
|
|||
/** |
|||
* 查找相似文本接口 |
|||
* @param message 查询文本 |
|||
* @return 相似文本+相似度 |
|||
*/ |
|||
@GetMapping("/similarity") |
|||
public Map<String, Double> findSimilarText(@RequestParam String message) { |
|||
// 查询Top3相似文本 |
|||
return textSimilarityService.findSimilarTexts(message, 3); |
|||
} |
|||
|
|||
|
|||
} |
|||
@ -0,0 +1,22 @@ |
|||
package com.example.springaiembedding.controller; |
|||
|
|||
import com.example.springaiembedding.service.RagService; |
|||
import jakarta.annotation.Resource; |
|||
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; |
|||
|
|||
|
|||
@RestController |
|||
@RequestMapping("/ai/rag") |
|||
public class RagController { |
|||
@Resource |
|||
private RagService ragService; |
|||
|
|||
// 问答接口 |
|||
@GetMapping("/query") |
|||
public String query(@RequestParam String question) { |
|||
return ragService.generateAnswer(question); |
|||
} |
|||
} |
|||
@ -0,0 +1,87 @@ |
|||
package com.example.springaiembedding.service; |
|||
|
|||
import com.example.springaiembedding.tool.DocumentLoader; |
|||
import jakarta.annotation.PostConstruct; |
|||
import jakarta.annotation.Resource; |
|||
import org.springframework.ai.chat.client.ChatClient; |
|||
import org.springframework.ai.chat.model.ChatModel; |
|||
import org.springframework.ai.embedding.EmbeddingModel; |
|||
import org.springframework.ai.embedding.EmbeddingResponse; |
|||
import org.springframework.stereotype.Service; |
|||
|
|||
import java.io.IOException; |
|||
import java.util.*; |
|||
import java.util.stream.Collectors; |
|||
|
|||
@Service |
|||
public class RagService { |
|||
@Resource |
|||
private EmbeddingModel embeddingModel; |
|||
@Resource |
|||
private ChatModel chatModel; |
|||
|
|||
// 内存存储:知识库片段 → 对应向量 |
|||
private Map<String, float[]> knowledgeVectors = new HashMap<>(); |
|||
|
|||
// 项目启动时自动加载并向量化知识库 |
|||
@PostConstruct |
|||
public void initKnowledgeBase() throws IOException { |
|||
// 1. 加载并拆分文档 |
|||
List<String> chunks = DocumentLoader.loadAndSplit("knowledge.txt"); |
|||
// 2. 批量向量化(一次请求生成所有片段向量) |
|||
EmbeddingResponse embeddingResponse = embeddingModel.embedForResponse(chunks); |
|||
// 3. 存储片段与向量的映射 |
|||
for (int i = 0; i < chunks.size(); i++) { |
|||
knowledgeVectors.put(chunks.get(i), embeddingResponse.getResults().get(i).getOutput()); |
|||
} |
|||
} |
|||
|
|||
// 检索与用户问题最相似的 Top N 片段 |
|||
private List<String> retrieveSimilarChunks(String query, int topN) { |
|||
// 1. 用户问题向量化 |
|||
float[] queryVector = embeddingModel.embed(query); |
|||
// 2. 计算与所有知识库片段的相似度 |
|||
Map<String, Double> similarityMap = new HashMap<>(); |
|||
//Entry是什么?Map.Entry<String, float[]> entry 是一个 Map.Entry 对象,用于遍历 Map 中的键值对。 |
|||
for (Map.Entry<String, float[]> entry : knowledgeVectors.entrySet()) { |
|||
similarityMap.put(entry.getKey(), CosineSimilarityCalculator.calculate(queryVector, entry.getValue())); |
|||
} |
|||
// 3. 按相似度降序排序,取 Top N |
|||
return similarityMap.entrySet().stream() |
|||
.sorted(Collections.reverseOrder(Map.Entry.comparingByValue())) |
|||
.limit(topN) |
|||
.map(Map.Entry::getKey) |
|||
.collect(Collectors.toList()); |
|||
} |
|||
|
|||
// 生成最终回答 |
|||
public String generateAnswer(String query) { |
|||
// 1. 检索相似片段(取 Top 2) |
|||
List<String> similarChunks = retrieveSimilarChunks(query, 2); |
|||
// 2. 拼接 Prompt(约束模型基于知识库回答) |
|||
StringBuilder prompt = new StringBuilder(); |
|||
prompt.append("请基于以下知识库内容回答用户问题,禁止编造信息:\n"); |
|||
for (String chunk : similarChunks) { |
|||
prompt.append("- ").append(chunk).append("\n"); |
|||
} |
|||
prompt.append("\n用户问题:").append(query); |
|||
// 3. 调用 Chat 模型生成回答 |
|||
ChatClient chatClient = ChatClient.builder(chatModel).build(); |
|||
return chatClient.prompt().user(prompt.toString()).call().content(); |
|||
} |
|||
|
|||
public class CosineSimilarityCalculator { |
|||
// 计算两个 float[] 向量的余弦相似度 |
|||
public static double calculate(float[] vectorA, float[] vectorB) { |
|||
double dotProduct = 0.0; |
|||
double normA = 0.0; |
|||
double normB = 0.0; |
|||
for (int i = 0; i < vectorA.length; i++) { |
|||
dotProduct += vectorA[i] * vectorB[i]; |
|||
normA += Math.pow(vectorA[i], 2); |
|||
normB += Math.pow(vectorB[i], 2); |
|||
} |
|||
return dotProduct / (Math.sqrt(normA) * Math.sqrt(normB)); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,93 @@ |
|||
package com.example.springaiembedding.service; |
|||
|
|||
import org.springframework.ai.embedding.EmbeddingModel; |
|||
import org.springframework.ai.embedding.EmbeddingResponse; |
|||
import org.springframework.beans.factory.annotation.Autowired; |
|||
import org.springframework.stereotype.Service; |
|||
|
|||
import java.util.*; |
|||
import java.util.stream.Collectors; |
|||
|
|||
@Service |
|||
public class TextSimilarityService { |
|||
|
|||
@Autowired |
|||
private EmbeddingModel embeddingModel; |
|||
|
|||
// 模拟本地文本库(可替换为数据库/向量库) |
|||
private final List<String> TEXT_LIBRARY = Arrays.asList( |
|||
"我爱Java编程", |
|||
"SpringBoot是最流行的后端框架", |
|||
"人工智能改变世界", |
|||
"大模型应用开发", |
|||
"我喜欢学习编程", |
|||
"向量数据库用于存储Embedding", |
|||
"SpringAI简化大模型开发" |
|||
); |
|||
|
|||
/** |
|||
* 查找最相似的文本 |
|||
* @param queryText 查询文本 |
|||
* @param topN 返回前N条结果 |
|||
* @return 相似文本+相似度 |
|||
*/ |
|||
|
|||
public Map<String, Double> findSimilarTexts(String queryText, int topN) { |
|||
// 1. 将查询文本转为向量 |
|||
//float[] queryVector = embeddingModel.embed(queryText); |
|||
EmbeddingResponse queryEmbedding = embeddingModel.embedForResponse(List.of(queryText)); |
|||
float[] queryVector = queryEmbedding.getResults().get(0).getOutput(); |
|||
|
|||
// 2. 将文本库所有文本转为向量 |
|||
// 2. 将文本库所有文本转为向量 |
|||
Map<String, float[]> textVectorMap = new HashMap<>(); |
|||
for (String text : TEXT_LIBRARY) { |
|||
float[] textVector = embeddingModel.embed(text); |
|||
//put是将文本和向量存储到Map中 |
|||
textVectorMap.put(text, textVector); |
|||
} |
|||
|
|||
// 3. 计算相似度并排序 |
|||
Map<String, Double> similarityMap = new HashMap<>(); |
|||
for (Map.Entry<String, float[]> entry : textVectorMap.entrySet()) { |
|||
double similarity = CosineSimilarityCalculator.calculate(queryVector, entry.getValue()); |
|||
similarityMap.put(entry.getKey(), similarity); |
|||
} |
|||
|
|||
// 4. 按相似度降序排序,取TopN |
|||
return similarityMap.entrySet().stream() |
|||
.sorted(Collections.reverseOrder(Map.Entry.comparingByValue())) |
|||
.limit(topN) |
|||
.collect(Collectors.toMap( |
|||
Map.Entry::getKey, |
|||
Map.Entry::getValue, |
|||
(oldValue, newValue) -> oldValue, |
|||
LinkedHashMap::new |
|||
)); |
|||
} |
|||
|
|||
/** |
|||
* 余弦相似度工具类(计算文本向量相似度) |
|||
*/ |
|||
public class CosineSimilarityCalculator { |
|||
|
|||
/** |
|||
* 计算两个向量的余弦相似度 |
|||
* @param vectorA 向量A |
|||
* @param vectorB 向量B |
|||
* @return 相似度 0~1,值越大越相似 |
|||
*/ |
|||
public static double calculate(float[] vectorA, float[] vectorB) { |
|||
double dotProduct = 0.0; |
|||
double normA = 0.0; |
|||
double normB = 0.0; |
|||
|
|||
for (int i = 0; i < vectorA.length; i++) { |
|||
dotProduct += vectorA[i] * vectorB[i]; |
|||
normA += Math.pow(vectorA[i], 2); |
|||
normB += Math.pow(vectorB[i], 2); |
|||
} |
|||
return dotProduct / (Math.sqrt(normA) * Math.sqrt(normB)); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,28 @@ |
|||
package com.example.springaiembedding.tool; |
|||
|
|||
import org.springframework.core.io.ClassPathResource; |
|||
import java.io.BufferedReader; |
|||
import java.io.IOException; |
|||
import java.io.InputStreamReader; |
|||
import java.util.ArrayList; |
|||
import java.util.List; |
|||
import java.util.stream.Collectors; |
|||
|
|||
|
|||
// 文档加载工具类 |
|||
public class DocumentLoader { |
|||
// 加载并拆分知识库文档 |
|||
public static List<String> loadAndSplit(String resourcePath) throws IOException { |
|||
ClassPathResource resource = new ClassPathResource(resourcePath); // 从类路径加载资源 |
|||
BufferedReader reader = new BufferedReader(new InputStreamReader(resource.getInputStream())); // 按行读取 |
|||
String content = reader.lines().collect(Collectors.joining("\n")); // 读取文件内容,将所有行连接起来一个字符串 |
|||
|
|||
// 按 "---" 拆分片段,过滤空内容 |
|||
List<String> chunks = new ArrayList<>(); |
|||
for (String chunk : content.split("---")) { |
|||
String trimmed = chunk.trim(); |
|||
if (!trimmed.isEmpty()) chunks.add(trimmed); |
|||
} |
|||
return chunks; |
|||
} |
|||
} |
|||
@ -0,0 +1,21 @@ |
|||
spring.application.name=SpringAIEmbedding |
|||
|
|||
server.port=8080 |
|||
|
|||
## ????AI ???? |
|||
# Embedding ?? |
|||
spring.ai.zhipuai.api-key=c721340a438942d0942148b48a22e50e.5VCKagzmQdyHpwyc |
|||
spring.ai.zhipuai.base-url=https://open.bigmodel.cn/api/paas |
|||
|
|||
# Embedding ?? |
|||
spring.ai.zhipuai.embedding-model=embedding-2 |
|||
# Chat ?? |
|||
spring.ai.zhipuai.chat.options.model=GLM-4.7-Flash |
|||
|
|||
# ?????? |
|||
logging.pattern.console=%-5level %logger - %msg%n |
|||
|
|||
|
|||
|
|||
|
|||
|
|||
@ -0,0 +1,8 @@ |
|||
--- |
|||
Spring AI 是一个用于简化大模型应用开发的框架,支持智谱AI、OpenAI 等多种大模型厂商。 |
|||
--- |
|||
智谱AI Embedding 模型可将文本转换为向量,用于语义检索、相似匹配等场景。 |
|||
--- |
|||
RAG(检索增强生成)通过检索本地知识库内容,辅助大模型生成更准确的回答,避免幻觉。 |
|||
--- |
|||
Spring AI 提供 EmbeddingModel 和 ChatModel 接口,让开发者快速接入大模型能力。 |
|||
@ -0,0 +1,13 @@ |
|||
package com.example.springaiembedding; |
|||
|
|||
import org.junit.jupiter.api.Test; |
|||
import org.springframework.boot.test.context.SpringBootTest; |
|||
|
|||
@SpringBootTest |
|||
class SpringAiEmbeddingApplicationTests { |
|||
|
|||
@Test |
|||
void contextLoads() { |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,33 @@ |
|||
HELP.md |
|||
target/ |
|||
.mvn/wrapper/maven-wrapper.jar |
|||
!**/src/main/**/target/ |
|||
!**/src/test/**/target/ |
|||
|
|||
### STS ### |
|||
.apt_generated |
|||
.classpath |
|||
.factorypath |
|||
.project |
|||
.settings |
|||
.springBeans |
|||
.sts4-cache |
|||
|
|||
### IntelliJ IDEA ### |
|||
.idea |
|||
*.iws |
|||
*.iml |
|||
*.ipr |
|||
|
|||
### NetBeans ### |
|||
/nbproject/private/ |
|||
/nbbuild/ |
|||
/dist/ |
|||
/nbdist/ |
|||
/.nb-gradle/ |
|||
build/ |
|||
!**/src/main/**/build/ |
|||
!**/src/test/**/build/ |
|||
|
|||
### VS Code ### |
|||
.vscode/ |
|||
@ -0,0 +1,3 @@ |
|||
wrapperVersion=3.3.4 |
|||
distributionType=only-script |
|||
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.14/apache-maven-3.9.14-bin.zip |
|||
@ -0,0 +1,104 @@ |
|||
<?xml version="1.0" encoding="UTF-8"?> |
|||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" |
|||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> |
|||
<modelVersion>4.0.0</modelVersion> |
|||
<parent> |
|||
<groupId>org.springframework.boot</groupId> |
|||
<artifactId>spring-boot-starter-parent</artifactId> |
|||
<version>3.5.13</version> |
|||
<relativePath/> <!-- lookup parent from repository --> |
|||
</parent> |
|||
<groupId>com.example</groupId> |
|||
<artifactId>SpringAIQuickStart</artifactId> |
|||
<version>0.0.1-SNAPSHOT</version> |
|||
<name>SpringAIQuickStart</name> |
|||
<description>SpringAIQuickStart</description> |
|||
<url/> |
|||
<licenses> |
|||
<license/> |
|||
</licenses> |
|||
<developers> |
|||
<developer/> |
|||
</developers> |
|||
<scm> |
|||
<connection/> |
|||
<developerConnection/> |
|||
<tag/> |
|||
<url/> |
|||
</scm> |
|||
<properties> |
|||
<java.version>17</java.version> |
|||
</properties> |
|||
|
|||
<dependencyManagement> |
|||
<dependencies> |
|||
<dependency> |
|||
<groupId>org.springframework.ai</groupId> |
|||
<artifactId>spring-ai-bom</artifactId> |
|||
<version>1.0.0-SNAPSHOT</version> |
|||
<type>pom</type> |
|||
<scope>import</scope> |
|||
</dependency> |
|||
</dependencies> |
|||
</dependencyManagement> |
|||
|
|||
<dependencies> |
|||
<dependency> |
|||
<groupId>org.springframework.boot</groupId> |
|||
<artifactId>spring-boot-starter</artifactId> |
|||
</dependency> |
|||
|
|||
<dependency> |
|||
<groupId>org.springframework.boot</groupId> |
|||
<artifactId>spring-boot-starter-web</artifactId> |
|||
</dependency> |
|||
|
|||
<!-- Spring AI Deepseek 模型依赖 --> |
|||
<dependency> |
|||
<groupId>org.springframework.ai</groupId> |
|||
<artifactId>spring-ai-starter-model-deepseek</artifactId> |
|||
</dependency> |
|||
|
|||
|
|||
<dependency> |
|||
<groupId>org.springframework.boot</groupId> |
|||
<artifactId>spring-boot-starter-test</artifactId> |
|||
<scope>test</scope> |
|||
</dependency> |
|||
</dependencies> |
|||
|
|||
<!-- 声明仓库,用于获取 Spring AI 以及相关预发布版本 --> |
|||
<repositories> |
|||
<!-- Spring Milestones 仓库 --> |
|||
<repository> |
|||
<id>spring-milestones</id> |
|||
<name>Spring Milestones</name> |
|||
<url>https://repo.spring.io/milestone</url> |
|||
<snapshots> |
|||
<enabled>false</enabled> |
|||
</snapshots> |
|||
</repository> |
|||
<!-- Spring Snapshots 仓库 --> |
|||
<repository> |
|||
<id>spring-snapshots</id> |
|||
<name>Spring Snapshots</name> |
|||
<url>https://repo.spring.io/snapshot</url> |
|||
<releases> |
|||
<enabled>false</enabled> |
|||
</releases> |
|||
<snapshots> |
|||
<enabled>true</enabled> |
|||
</snapshots> |
|||
</repository> |
|||
</repositories> |
|||
|
|||
<build> |
|||
<plugins> |
|||
<plugin> |
|||
<groupId>org.springframework.boot</groupId> |
|||
<artifactId>spring-boot-maven-plugin</artifactId> |
|||
</plugin> |
|||
</plugins> |
|||
</build> |
|||
|
|||
</project> |
|||
@ -0,0 +1,13 @@ |
|||
package com.example.springaiquickstart; |
|||
|
|||
import org.springframework.boot.SpringApplication; |
|||
import org.springframework.boot.autoconfigure.SpringBootApplication; |
|||
|
|||
@SpringBootApplication |
|||
public class SpringAiQuickStartApplication { |
|||
|
|||
public static void main(String[] args) { |
|||
SpringApplication.run(SpringAiQuickStartApplication.class, args); |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,127 @@ |
|||
package com.example.springaiquickstart.controller; |
|||
|
|||
|
|||
import jakarta.servlet.http.HttpServlet; |
|||
import jakarta.servlet.http.HttpServletResponse; |
|||
import org.springframework.ai.chat.messages.AssistantMessage; |
|||
import org.springframework.ai.chat.messages.Message; |
|||
import org.springframework.ai.chat.messages.SystemMessage; |
|||
import org.springframework.ai.chat.messages.UserMessage; |
|||
import org.springframework.ai.chat.model.ChatResponse; |
|||
import org.springframework.ai.chat.model.Generation; |
|||
import org.springframework.ai.chat.prompt.ChatOptions; |
|||
import org.springframework.ai.chat.prompt.Prompt; |
|||
import org.springframework.ai.deepseek.DeepSeekChatModel; |
|||
import org.springframework.beans.factory.annotation.Autowired; |
|||
import org.springframework.http.HttpRequest; |
|||
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.core.publisher.Flux; |
|||
|
|||
import java.util.Arrays; |
|||
import java.util.List; |
|||
|
|||
@RestController |
|||
@RequestMapping("/ai") |
|||
public class ChatController { |
|||
|
|||
@Autowired |
|||
private DeepSeekChatModel chatModel; |
|||
|
|||
// 与模型直接对话,返回字符串响应 |
|||
@GetMapping("/generate") |
|||
public String generate(@RequestParam(value = "message",defaultValue = "你好,你是谁?") String message) { |
|||
System.out.println("message: " + message); |
|||
|
|||
// 与模型直接对话,调用chatModel的call方法生成响应 |
|||
String response = chatModel.call(message); |
|||
|
|||
System.out.println("response: " + response); |
|||
|
|||
return response; |
|||
} |
|||
|
|||
// 与模型对话,流式返回内容 |
|||
@GetMapping("/generateStream1") |
|||
public Flux<ChatResponse> generateStream1(@RequestParam(value = "message",defaultValue = "你好,你是谁?") String message) { |
|||
System.out.println("message: " + message); |
|||
|
|||
// 与模型对话,流式返回内容 |
|||
Prompt prompt = new Prompt(message); |
|||
Flux<ChatResponse> stream = chatModel.stream(prompt); |
|||
|
|||
System.out.println("stream: " + stream); |
|||
|
|||
return stream; |
|||
} |
|||
|
|||
// 与模型对话,流式返回内容,转换为字符串流 |
|||
// 解决中文乱码问题 |
|||
// 用lambda表达式简化 |
|||
@GetMapping("/generateStream2") |
|||
public Flux<String> generateStream2( |
|||
@RequestParam(value = "message",defaultValue = "你好,你是谁?") String message, |
|||
HttpServletResponse response |
|||
) { |
|||
//设置字符编码为UTF-8,解决中文乱码问题 |
|||
response.setCharacterEncoding("UTF-8"); |
|||
System.out.println("message: " + message); |
|||
|
|||
// 与模型对话,流式返回内容 |
|||
Prompt prompt = new Prompt(message); |
|||
Flux<ChatResponse> stream = chatModel.stream(prompt); |
|||
|
|||
// 转换为字符串流,用lambda表达式简化 |
|||
Flux<String> result = stream.map(ChatResponse -> |
|||
ChatResponse.getResult().getOutput().getText() |
|||
); |
|||
// 转换为字符串流,用方法引用简化 |
|||
// Flux<String> result = stream.map(ChatResponse::getResult) |
|||
// .map(Generation::getOutput) |
|||
// .map(AssistantMessage::getText); |
|||
|
|||
|
|||
System.out.println("result: " + result); |
|||
|
|||
return result; |
|||
} |
|||
|
|||
//运行时设置模型参数 |
|||
@GetMapping("/runtimeOptions") |
|||
public Flux<String> runtimeOptions( |
|||
@RequestParam(value = "message",defaultValue = "你好,你是谁?") String message, |
|||
@RequestParam(value = "temperature",required = false) Double temp, |
|||
HttpServletResponse response |
|||
) { |
|||
//设置字符编码为UTF-8,解决中文乱码问题 |
|||
response.setCharacterEncoding("UTF-8"); |
|||
// 构建系统提示 |
|||
SystemMessage systemMessage = new SystemMessage("你是一个资深Java开发工程师,回答要简洁专业"); |
|||
// 构建用户消息 |
|||
UserMessage userMessage = new UserMessage(message); |
|||
// 构建历史消息(多轮对话时加入) |
|||
List<Message> messages = Arrays.asList(systemMessage, userMessage); |
|||
|
|||
// 封装成 Prompt,还可以设置模型参数 |
|||
Prompt prompt = new Prompt(messages, |
|||
ChatOptions.builder() |
|||
.temperature(temp) |
|||
.maxTokens(1000) |
|||
.build() |
|||
); |
|||
|
|||
// 流式调用 |
|||
Flux<ChatResponse> stream = chatModel.stream(prompt); |
|||
// 转换为字符串流,用lambda表达式简化 |
|||
Flux<String> result = stream.map(ChatResponse -> |
|||
ChatResponse.getResult().getOutput().getText() |
|||
); |
|||
|
|||
System.out.println("result: " + result); |
|||
return result; |
|||
} |
|||
|
|||
|
|||
} |
|||
@ -0,0 +1,13 @@ |
|||
spring.application.name=SpringAIQuickStart |
|||
|
|||
server.port=8080 |
|||
|
|||
#?? Deepseek ???????url????????? |
|||
spring.ai.deepseek.base-url=https://api.deepseek.com |
|||
spring.ai.deepseek.api-key=sk-ccbfe09f433148129cd98df6150653e8 |
|||
spring.ai.deepseek.chat.options.model=deepseek-chat |
|||
|
|||
#??0-2???0????????2??????? |
|||
spring.ai.deepseek.chat.options.temperature=0.8 |
|||
|
|||
|
|||
@ -0,0 +1,224 @@ |
|||
<!DOCTYPE html> |
|||
<html lang="zh-CN"> |
|||
<head> |
|||
<meta charset="UTF-8"> |
|||
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|||
<title>Spring AI 聊天助手</title> |
|||
<script src="https://cdn.tailwindcss.com"></script> |
|||
<link href="https://cdn.jsdelivr.net/npm/font-awesome@4.7.0/css/font-awesome.min.css" rel="stylesheet"> |
|||
<script> |
|||
tailwind.config = { |
|||
theme: { |
|||
extend: { |
|||
colors: { |
|||
primary: '#3b82f6', |
|||
secondary: '#64748b', |
|||
neutral: '#f8fafc', |
|||
dark: '#1e293b' |
|||
}, |
|||
fontFamily: { |
|||
sans: ['Inter', 'system-ui', 'sans-serif'] |
|||
} |
|||
} |
|||
} |
|||
} |
|||
</script> |
|||
<style type="text/tailwindcss"> |
|||
@layer utilities { |
|||
.content-auto { |
|||
content-visibility: auto; |
|||
} |
|||
.chat-message { |
|||
@apply p-4 rounded-lg mb-4 max-w-[80%]; |
|||
} |
|||
.user-message { |
|||
@apply bg-primary text-white self-end; |
|||
} |
|||
.ai-message { |
|||
@apply bg-neutral border border-gray-200 text-dark self-start; |
|||
} |
|||
} |
|||
</style> |
|||
</head> |
|||
<body class="bg-gray-50 min-h-screen flex flex-col"> |
|||
<!-- 顶部导航栏 --> |
|||
<header class="bg-white shadow-sm"> |
|||
<div class="container mx-auto px-4 py-4 flex justify-between items-center"> |
|||
<div class="flex items-center space-x-2"> |
|||
<i class="fa fa-robot text-primary text-2xl"></i> |
|||
<h1 class="text-xl font-bold text-dark">Spring AI 聊天助手</h1> |
|||
</div> |
|||
<div class="text-sm text-secondary"> |
|||
<span id="status" class="flex items-center"> |
|||
<span class="w-2 h-2 bg-green-500 rounded-full mr-2"></span> |
|||
在线 |
|||
</span> |
|||
</div> |
|||
</div> |
|||
</header> |
|||
|
|||
<!-- 主内容区 --> |
|||
<main class="flex-1 container mx-auto px-4 py-8 max-w-4xl"> |
|||
<!-- 聊天区域 --> |
|||
<div class="bg-white rounded-xl shadow-md overflow-hidden"> |
|||
<!-- 聊天头部 --> |
|||
<div class="bg-primary text-white p-4"> |
|||
<h2 class="text-lg font-semibold">AI 助手</h2> |
|||
<p class="text-sm opacity-80">基于 DeepSeek 模型</p> |
|||
</div> |
|||
|
|||
<!-- 聊天消息区 --> |
|||
<div id="chat-messages" class="p-4 h-[500px] overflow-y-auto flex flex-col space-y-4"> |
|||
<!-- 欢迎消息 --> |
|||
<div class="chat-message ai-message"> |
|||
<div class="flex items-start"> |
|||
<div class="w-8 h-8 rounded-full bg-primary text-white flex items-center justify-center mr-3 flex-shrink-0"> |
|||
<i class="fa fa-robot"></i> |
|||
</div> |
|||
<div> |
|||
<p class="font-semibold mb-1">AI 助手</p> |
|||
<p>你好!我是基于 DeepSeek 模型的 AI 助手,有什么可以帮助你的吗?</p> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
|
|||
<!-- 输入区域 --> |
|||
<div class="p-4 border-t"> |
|||
<form id="chat-form" class="flex space-x-2"> |
|||
<input |
|||
type="text" |
|||
id="message-input" |
|||
placeholder="输入消息..." |
|||
class="flex-1 border border-gray-300 rounded-lg px-4 py-2 focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent" |
|||
> |
|||
<button |
|||
type="submit" |
|||
id="send-button" |
|||
class="bg-primary text-white rounded-lg px-6 py-2 hover:bg-primary/90 transition-colors flex items-center space-x-2" |
|||
> |
|||
<span>发送</span> |
|||
<i class="fa fa-paper-plane"></i> |
|||
</button> |
|||
</form> |
|||
</div> |
|||
</div> |
|||
|
|||
<!-- 功能说明 --> |
|||
<div class="mt-8 bg-white rounded-xl shadow-md p-6"> |
|||
<h3 class="text-lg font-semibold mb-4">功能说明</h3> |
|||
<ul class="space-y-2 text-secondary"> |
|||
<li class="flex items-center"> |
|||
<i class="fa fa-check-circle text-green-500 mr-2"></i> |
|||
支持自然语言对话 |
|||
</li> |
|||
<li class="flex items-center"> |
|||
<i class="fa fa-check-circle text-green-500 mr-2"></i> |
|||
基于 DeepSeek 大语言模型 |
|||
</li> |
|||
<li class="flex items-center"> |
|||
<i class="fa fa-check-circle text-green-500 mr-2"></i> |
|||
实时响应生成 |
|||
</li> |
|||
<li class="flex items-center"> |
|||
<i class="fa fa-check-circle text-green-500 mr-2"></i> |
|||
简洁美观的用户界面 |
|||
</li> |
|||
</ul> |
|||
</div> |
|||
</main> |
|||
|
|||
<!-- 页脚 --> |
|||
<footer class="bg-white shadow-sm mt-8"> |
|||
<div class="container mx-auto px-4 py-4 text-center text-secondary text-sm"> |
|||
<p>© 2026 Spring AI 聊天助手 | 基于 DeepSeek 模型</p> |
|||
</div> |
|||
</footer> |
|||
|
|||
<script> |
|||
document.addEventListener('DOMContentLoaded', function() { |
|||
const chatForm = document.getElementById('chat-form'); |
|||
const messageInput = document.getElementById('message-input'); |
|||
const chatMessages = document.getElementById('chat-messages'); |
|||
const sendButton = document.getElementById('send-button'); |
|||
const status = document.getElementById('status'); |
|||
|
|||
// 发送消息 |
|||
chatForm.addEventListener('submit', function(e) { |
|||
e.preventDefault(); |
|||
const message = messageInput.value.trim(); |
|||
if (message) { |
|||
// 添加用户消息到聊天区 |
|||
addMessage('user', message); |
|||
messageInput.value = ''; |
|||
|
|||
// 禁用发送按钮 |
|||
sendButton.disabled = true; |
|||
sendButton.classList.add('opacity-50'); |
|||
status.innerHTML = '<span class="w-2 h-2 bg-yellow-500 rounded-full mr-2"></span>处理中...'; |
|||
|
|||
// 调用后端 API |
|||
fetch(`/ai/generate?message=${encodeURIComponent(message)}`) |
|||
.then(response => response.text()) |
|||
.then(data => { |
|||
// 添加 AI 响应到聊天区 |
|||
addMessage('ai', data); |
|||
}) |
|||
.catch(error => { |
|||
console.error('Error:', error); |
|||
addMessage('ai', '抱歉,处理请求时出错,请稍后再试。'); |
|||
}) |
|||
.finally(() => { |
|||
// 恢复发送按钮状态 |
|||
sendButton.disabled = false; |
|||
sendButton.classList.remove('opacity-50'); |
|||
status.innerHTML = '<span class="w-2 h-2 bg-green-500 rounded-full mr-2"></span>在线'; |
|||
}); |
|||
} |
|||
}); |
|||
|
|||
// 添加消息到聊天区 |
|||
function addMessage(type, content) { |
|||
const messageDiv = document.createElement('div'); |
|||
messageDiv.className = `chat-message ${type === 'user' ? 'user-message' : 'ai-message'} self-${type === 'user' ? 'end' : 'start'}`; |
|||
|
|||
if (type === 'user') { |
|||
messageDiv.innerHTML = ` |
|||
<div class="flex items-start justify-end"> |
|||
<div> |
|||
<p class="font-semibold mb-1 text-right">你</p> |
|||
<p>${content}</p> |
|||
</div> |
|||
<div class="w-8 h-8 rounded-full bg-gray-300 text-dark flex items-center justify-center ml-3 flex-shrink-0"> |
|||
<i class="fa fa-user"></i> |
|||
</div> |
|||
</div> |
|||
`; |
|||
} else { |
|||
messageDiv.innerHTML = ` |
|||
<div class="flex items-start"> |
|||
<div class="w-8 h-8 rounded-full bg-primary text-white flex items-center justify-center mr-3 flex-shrink-0"> |
|||
<i class="fa fa-robot"></i> |
|||
</div> |
|||
<div> |
|||
<p class="font-semibold mb-1">AI 助手</p> |
|||
<p>${content}</p> |
|||
</div> |
|||
</div> |
|||
`; |
|||
} |
|||
|
|||
chatMessages.appendChild(messageDiv); |
|||
chatMessages.scrollTop = chatMessages.scrollHeight; |
|||
} |
|||
|
|||
// 回车键发送消息 |
|||
messageInput.addEventListener('keypress', function(e) { |
|||
if (e.key === 'Enter') { |
|||
chatForm.dispatchEvent(new Event('submit')); |
|||
} |
|||
}); |
|||
}); |
|||
</script> |
|||
</body> |
|||
</html> |
|||
@ -0,0 +1,13 @@ |
|||
package com.example.springaiquickstart; |
|||
|
|||
import org.junit.jupiter.api.Test; |
|||
import org.springframework.boot.test.context.SpringBootTest; |
|||
|
|||
@SpringBootTest |
|||
class SpringAiQuickStartApplicationTests { |
|||
|
|||
@Test |
|||
void contextLoads() { |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,8 @@ |
|||
<?xml version="1.0" encoding="UTF-8"?> |
|||
<module type="GENERAL_MODULE" version="4"> |
|||
<component name="NewModuleRootManager" inherit-compiler-output="true"> |
|||
<exclude-output /> |
|||
<content url="file://$MODULE_DIR$" /> |
|||
<orderEntry type="sourceFolder" forTests="false" /> |
|||
</component> |
|||
</module> |
|||
@ -0,0 +1,3 @@ |
|||
wrapperVersion=3.3.4 |
|||
distributionType=only-script |
|||
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.14/apache-maven-3.9.14-bin.zip |
|||
@ -0,0 +1,10 @@ |
|||
<?xml version="1.0" encoding="UTF-8"?> |
|||
<module version="4"> |
|||
<component name="AdditionalModuleElements"> |
|||
<content url="file://$MODULE_DIR$" dumb="true"> |
|||
<sourceFolder url="file://$MODULE_DIR$/src/main/java" isTestSource="false" /> |
|||
<sourceFolder url="file://$MODULE_DIR$/src/main/resources" type="java-resource" /> |
|||
<sourceFolder url="file://$MODULE_DIR$/src/test/java" isTestSource="true" /> |
|||
</content> |
|||
</component> |
|||
</module> |
|||
@ -0,0 +1,133 @@ |
|||
<?xml version="1.0" encoding="UTF-8"?> |
|||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" |
|||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> |
|||
<modelVersion>4.0.0</modelVersion> |
|||
<parent> |
|||
<groupId>org.springframework.boot</groupId> |
|||
<artifactId>spring-boot-starter-parent</artifactId> |
|||
<version>3.5.13</version> |
|||
<relativePath/> <!-- lookup parent from repository --> |
|||
</parent> |
|||
<groupId>com.example</groupId> |
|||
<artifactId>SpringAIAlibaba</artifactId> |
|||
<version>1.1.2.0</version> |
|||
<name>SpringAIAlibaba</name> |
|||
<description>SpringAIAlibaba</description> |
|||
|
|||
<properties> |
|||
<java.version>17</java.version> |
|||
<!-- 阿里云DashScope SDK版本(官方要求>=2.15.0) --> |
|||
<dashscope.version>2.22.13</dashscope.version> |
|||
<!-- RxJava版本(适配DashScope SDK) --> |
|||
<rxjava.version>2.2.21</rxjava.version> |
|||
</properties> |
|||
|
|||
<url/> |
|||
<licenses> |
|||
<license/> |
|||
</licenses> |
|||
<developers> |
|||
<developer/> |
|||
</developers> |
|||
<scm> |
|||
<connection/> |
|||
<developerConnection/> |
|||
<tag/> |
|||
<url/> |
|||
</scm> |
|||
|
|||
<dependencyManagement> |
|||
<dependencies> |
|||
<dependency> |
|||
<groupId>com.alibaba.cloud.ai</groupId> |
|||
<artifactId>spring-ai-alibaba-bom</artifactId> |
|||
<version>1.1.2.0</version> |
|||
<type>pom</type> |
|||
<scope>import</scope> |
|||
</dependency> |
|||
<dependency> |
|||
<groupId>org.springframework.ai</groupId> |
|||
<artifactId>spring-ai-bom</artifactId> |
|||
<version>1.1.2</version> |
|||
<type>pom</type> |
|||
<scope>import</scope> |
|||
</dependency> |
|||
<dependency> |
|||
<groupId>com.alibaba.cloud.ai</groupId> |
|||
<artifactId>spring-ai-alibaba-extensions-bom</artifactId> |
|||
<version>1.1.2.1</version> |
|||
<type>pom</type> |
|||
<scope>import</scope> |
|||
</dependency> |
|||
</dependencies> |
|||
</dependencyManagement> |
|||
|
|||
<dependencies> |
|||
|
|||
|
|||
<!-- Spring AI Alibaba Agent Framework --> |
|||
<dependency> |
|||
<groupId>com.alibaba.cloud.ai</groupId> |
|||
<artifactId>spring-ai-alibaba-agent-framework</artifactId> |
|||
<version>1.1.2.0</version> |
|||
</dependency> |
|||
|
|||
<!-- DashScope ChatModel 支持(如果使用其他模型,请跳转 Spring AI 文档选择对应的 starter) --> |
|||
<dependency> |
|||
<groupId>com.alibaba.cloud.ai</groupId> |
|||
<artifactId>spring-ai-alibaba-starter-dashscope</artifactId> |
|||
<version>1.1.2.0</version> |
|||
</dependency> |
|||
|
|||
<dependency> |
|||
<groupId>org.springframework.boot</groupId> |
|||
<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> |
|||
</dependency> |
|||
|
|||
<dependency> |
|||
<groupId>org.projectlombok</groupId> |
|||
<artifactId>lombok</artifactId> |
|||
<optional>true</optional> |
|||
</dependency> |
|||
|
|||
<!-- 1. 阿里云DashScope官方SDK(解决所有dashscope包报红) --> |
|||
<dependency> |
|||
<groupId>com.alibaba</groupId> |
|||
<artifactId>dashscope-sdk-java</artifactId> |
|||
<version>${dashscope.version}</version> |
|||
</dependency> |
|||
|
|||
<!-- 2. RxJava 2.x(解决io.reactivex.Flowable报红) --> |
|||
<dependency> |
|||
<groupId>io.reactivex.rxjava2</groupId> |
|||
<artifactId>rxjava</artifactId> |
|||
<version>${rxjava.version}</version> |
|||
</dependency> |
|||
|
|||
|
|||
|
|||
|
|||
|
|||
</dependencies> |
|||
|
|||
<build> |
|||
<plugins> |
|||
<plugin> |
|||
<groupId>org.springframework.boot</groupId> |
|||
<artifactId>spring-boot-maven-plugin</artifactId> |
|||
</plugin> |
|||
</plugins> |
|||
</build> |
|||
|
|||
</project> |
|||
@ -0,0 +1,13 @@ |
|||
package com.example.springaialibaba; |
|||
|
|||
import org.springframework.boot.SpringApplication; |
|||
import org.springframework.boot.autoconfigure.SpringBootApplication; |
|||
|
|||
@SpringBootApplication |
|||
public class SpringAiAlibabaApplication { |
|||
|
|||
public static void main(String[] args) { |
|||
SpringApplication.run(SpringAiAlibabaApplication.class, args); |
|||
} |
|||
|
|||
} |
|||
@ -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; |
|||
} |
|||
|
|||
|
|||
|
|||
|
|||
|
|||
|
|||
|
|||
|
|||
|
|||
} |
|||
@ -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 Key:https://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); |
|||
} |
|||
} |
|||
@ -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()); |
|||
}); |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
@ -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); |
|||
} |
|||
}); |
|||
} |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,14 @@ |
|||
server: |
|||
port: 8080 |
|||
|
|||
spring: |
|||
ai: |
|||
dashscope: |
|||
# 核心:工作流应用的AppId |
|||
agent: |
|||
app-id: df04e1abf24a416c8702260e88863ac4 |
|||
# 核心:你的API-Key |
|||
api-key: sk-f7d2253302b547c7a2fe257673cb854b |
|||
# 业务空间ID |
|||
workspace-id: llm-fuczq7vkh8vyz716 |
|||
|
|||
@ -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,0 +1,13 @@ |
|||
package com.example.springaialibaba; |
|||
|
|||
import org.junit.jupiter.api.Test; |
|||
import org.springframework.boot.test.context.SpringBootTest; |
|||
|
|||
@SpringBootTest |
|||
class SpringAiAlibabaApplicationTests { |
|||
|
|||
@Test |
|||
void contextLoads() { |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,14 @@ |
|||
server: |
|||
port: 8080 |
|||
|
|||
spring: |
|||
ai: |
|||
dashscope: |
|||
# 核心:工作流应用的AppId |
|||
agent: |
|||
app-id: df04e1abf24a416c8702260e88863ac4 |
|||
# 核心:你的API-Key |
|||
api-key: sk-f7d2253302b547c7a2fe257673cb854b |
|||
# 业务空间ID |
|||
workspace-id: llm-fuczq7vkh8vyz716 |
|||
|
|||
@ -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,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 |
|||
Write
Preview
Loading…
Cancel
Save
Reference in new issue