!1352 新增Hutool-AI服务

Merge pull request !1352 from eli_chow/v5-dev
This commit is contained in:
eli_chow 2025-05-30 06:50:37 +00:00 committed by Gitee
commit 04f9784d08
No known key found for this signature in database
GPG Key ID: 173E9B9CA92EEF8F
13 changed files with 1014 additions and 0 deletions

View File

@ -22,6 +22,7 @@ import cn.hutool.ai.core.Message;
import cn.hutool.ai.model.deepseek.DeepSeekService;
import cn.hutool.ai.model.doubao.DoubaoService;
import cn.hutool.ai.model.grok.GrokService;
import cn.hutool.ai.model.hutool.HutoolService;
import cn.hutool.ai.model.openai.OpenaiService;
import java.util.List;
@ -58,6 +59,17 @@ public class AIUtil {
return getAIService(config, AIService.class);
}
/**
* 获取Hutool-AI服务
*
* @param config 创建的AI服务模型的配置
* @return HutoolService
* @since 5.8.39
*/
public static HutoolService getHutoolService(final AIConfig config) {
return getAIService(config, HutoolService.class);
}
/**
* 获取DeepSeek模型服务
*

View File

@ -23,6 +23,11 @@ package cn.hutool.ai;
* @since 5.8.38
*/
public enum ModelName {
/**
* hutool
*/
HUTOOL("hutool"),
/**
* deepSeek
*/

View File

@ -24,6 +24,22 @@ package cn.hutool.ai;
*/
public class Models {
// Hutool的模型
public enum Hutool {
HUTOOL("hutool");
private final String model;
Hutool(String model) {
this.model = model;
}
public String getModel() {
return model;
}
}
// DeepSeek的模型
public enum DeepSeek {
DEEPSEEK_CHAT("deepseek-chat"),

View File

@ -0,0 +1,119 @@
/*
* Copyright (c) 2025 Hutool Team and hutool.cn
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.hutool.ai.model.hutool;
/**
* hutool公共类
*
* @author elichow
* @since 5.8.39
*/
public class HutoolCommon {
//hutool视觉参数
public enum HutoolVision {
AUTO("auto"),
LOW("low"),
HIGH("high");
private final String detail;
HutoolVision(String detail) {
this.detail = detail;
}
public String getDetail() {
return detail;
}
}
//hutool音频参数
public enum HutoolSpeech {
ALLOY("alloy"),
ASH("ash"),
CORAL("coral"),
ECHO("echo"),
FABLE("fable"),
ONYX("onyx"),
NOVA("nova"),
SAGE("sage"),
SHIMMER("shimmer");
private final String voice;
HutoolSpeech(String voice) {
this.voice = voice;
}
public String getVoice() {
return voice;
}
}
//hutool视频生成参数
public enum HutoolVideo {
//宽高比例
RATIO_16_9("--rt", "16:9"),//[1280, 720]
RATIO_4_3("--rt", "4:3"),//[960, 720]
RATIO_1_1("--rt", "1:1"),//[720, 720]
RATIO_3_4("--rt", "3:4"),//[720, 960]
RATIO_9_16("--rt", "9:16"),//[720, 1280]
RATIO_21_9("--rt", "21:9"),//[1280, 544]
//生成视频时长
DURATION_5("--dur", 5),//文生视频图生视频
DURATION_10("--dur", 10),//文生视频
//帧率即一秒时间内视频画面数量
FPS_5("--fps", 24),
//视频分辨率
RESOLUTION_5("--rs", "720p"),
//生成视频是否包含水印
WATERMARK_TRUE("--wm", true),
WATERMARK_FALSE("--wm", false);
private final String type;
private final Object value;
HutoolVideo(String type, Object value) {
this.type = type;
this.value = value;
}
public String getType() {
return type;
}
public Object getValue() {
if (value instanceof String) {
return (String) value;
} else if (value instanceof Integer) {
return (Integer) value;
} else if (value instanceof Boolean) {
return (Boolean) value;
}
return value;
}
}
}

View File

@ -0,0 +1,49 @@
/*
* Copyright (c) 2025 Hutool Team and hutool.cn
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.hutool.ai.model.hutool;
import cn.hutool.ai.Models;
import cn.hutool.ai.core.BaseConfig;
/**
* Hutool配置类初始化API接口地址设置默认的模型
*
* @author elichow
* @since 5.8.39
*/
public class HutoolConfig extends BaseConfig {
private final String API_URL = "https://api.hutool.cn/blade-ai/api";
private final String DEFAULT_MODEL = Models.Hutool.HUTOOL.getModel();
public HutoolConfig() {
setApiUrl(API_URL);
setModel(DEFAULT_MODEL);
}
public HutoolConfig(String apiKey) {
this();
setApiKey(apiKey);
}
@Override
public String getModelName() {
return "hutool";
}
}

View File

@ -0,0 +1,39 @@
/*
* Copyright (c) 2025 Hutool Team and hutool.cn
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.hutool.ai.model.hutool;
import cn.hutool.ai.core.AIConfig;
import cn.hutool.ai.core.AIServiceProvider;
/**r
* 创建Hutool服务实现类
*
* @author elichow
* @since 5.8.39
*/
public class HutoolProvider implements AIServiceProvider {
@Override
public String getServiceName() {
return "hutool";
}
@Override
public HutoolService create(final AIConfig config) {
return new HutoolServiceImpl(config);
}
}

View File

@ -0,0 +1,170 @@
/*
* Copyright (c) 2025 Hutool Team and hutool.cn
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.hutool.ai.model.hutool;
import cn.hutool.ai.core.AIService;
import java.io.File;
import java.io.InputStream;
import java.util.List;
import java.util.function.Consumer;
/**
* hutool支持的扩展接口
*
* @author elichow
* @since 5.8.39
*/
public interface HutoolService extends AIService {
/**
* 图像理解模型会依据传入的图片信息以及问题给出回复
*
* @param prompt 题词
* @param images 图片列表/或者图片Base64编码图片列表(URI形式)
* @param detail 手动设置图片的质量取值范围highlowauto,默认为auto
* @return AI回答
* @since 5.8.39
*/
String chatVision(String prompt, final List<String> images, String detail);
/**
* 图像理解-SSE流式输出
*
* @param prompt 题词
* @param images 图片列表/或者图片Base64编码图片列表(URI形式)
* @param detail 手动设置图片的质量取值范围highlowauto,默认为auto
* @param callback 流式数据回调函数
* @since 5.8.39
*/
void chatVision(String prompt, final List<String> images, String detail,final Consumer<String> callback);
/**
* 图像理解模型会依据传入的图片信息以及问题给出回复
*
* @param prompt 题词
* @param images 传入的图片列表地址/或者图片Base64编码图片列表(URI形式)
* @return AI回答
* @since 5.8.39
*/
default String chatVision(String prompt, final List<String> images) {
return chatVision(prompt, images, HutoolCommon.HutoolVision.AUTO.getDetail());
}
/**
* 图像理解模型会依据传入的图片信息以及问题给出回复
*
* @param prompt 题词
* @param images 传入的图片列表地址/或者图片Base64编码图片列表(URI形式)
* @param callback 流式数据回调函数
* @since 5.8.39
*/
default void chatVision(String prompt, final List<String> images, final Consumer<String> callback){
chatVision(prompt, images, HutoolCommon.HutoolVision.AUTO.getDetail(), callback);
}
/**
* 分词可以将文本转换为模型可理解的 token 信息
*
* @param text 需要分词的内容
* @return 分词结果
* @since 5.8.39
*/
String tokenizeText(String text);
/**
* 文生图
*
* @param prompt 题词
* @return 包含生成图片的url
* @since 5.8.39
*/
String imagesGenerations(String prompt);
/**
* 图文向量化仅支持单一文本单张图片或文本与图片的组合输入即一段文本 + 一张图片暂不支持批量文本 / 图片的同时处理
*
* @param text 需要向量化的内容
* @param image 需要向量化的图片地址/或者图片Base64编码图片(URI形式)
* @return 处理后的向量信息
* @since 5.8.39
*/
String embeddingVision(String text, String image);
/**
* TTS文本转语音
*
* @param input 需要转成语音的文本
* @param voice AI的音色
* @return 返回的音频mp3文件流
* @since 5.8.39
*/
InputStream tts(String input, final HutoolCommon.HutoolSpeech voice);
/**
* TTS文本转语音
*
* @param input 需要转成语音的文本
* @return 返回的音频mp3文件流
* @since 5.8.39
*/
default InputStream tts(String input) {
return tts(input, HutoolCommon.HutoolSpeech.ALLOY);
}
/**
* STT音频转文本
*
* @param file 需要转成文本的音频文件
* @return 返回的文本内容
* @since 5.8.39
*/
String stt(final File file);
/**
* 创建视频生成任务
*
* @param text 文本提示词
* @param image 图片/或者图片Base64编码图片(URI形式)
* @param videoParams 视频参数列表
* @return 生成任务id
* @since 5.8.39
*/
String videoTasks(String text, String image, final List<HutoolCommon.HutoolVideo> videoParams);
/**
* 创建视频生成任务
*
* @param text 文本提示词
* @param image 图片/或者图片Base64编码图片(URI形式)
* @return 生成任务id
* @since 5.8.39
*/
default String videoTasks(String text, String image) {
return videoTasks(text, image, null);
}
/**
* 查询视频生成任务信息
*
* @param taskId 通过创建生成视频任务返回的生成任务id
* @return 生成任务信息
* @since 5.8.39
*/
String getVideoTasksInfo(String taskId);
}

View File

@ -0,0 +1,380 @@
/*
* Copyright (c) 2025 Hutool Team and hutool.cn
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.hutool.ai.model.hutool;
import cn.hutool.ai.AIException;
import cn.hutool.ai.core.AIConfig;
import cn.hutool.ai.core.BaseAIService;
import cn.hutool.ai.core.Message;
import cn.hutool.core.thread.ThreadUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.HttpResponse;
import cn.hutool.json.JSONUtil;
import java.io.File;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
/**
* Hutool服务AI具体功能的实现
*
* @author elichow
* @since 5.8.39
*/
public class HutoolServiceImpl extends BaseAIService implements HutoolService {
//对话补全
private final String CHAT_ENDPOINT = "/chat/completions";
//分词
private final String TOKENIZE_TEXT = "/tokenize/text";
//文生图
private final String IMAGES_GENERATIONS = "/images/generations";
//图文向量化
private final String EMBEDDING_VISION = "/embeddings/multimodal";
//文本转语音
private final String TTS = "/audio/tts";
//语音转文本
private final String STT = "/audio/stt";
//创建视频生成任务
private final String CREATE_VIDEO = "/video/generations";
public HutoolServiceImpl(final AIConfig config) {
//初始化hutool客户端
super(config);
}
@Override
public String chat(final List<Message> messages) {
String paramJson = buildChatRequestBody(messages);
final HttpResponse response = sendPost(CHAT_ENDPOINT, paramJson);
return response.body();
}
@Override
public void chat(List<Message> messages,Consumer<String> callback) {
Map<String, Object> paramMap = buildChatStreamRequestBody(messages);
ThreadUtil.newThread(() -> sendPostStream(CHAT_ENDPOINT, paramMap, callback::accept), "hutool-chat-sse").start();
}
@Override
public String chatVision(String prompt, final List<String> images, String detail) {
String paramJson = buildChatVisionRequestBody(prompt, images, detail);
final HttpResponse response = sendPost(CHAT_ENDPOINT, paramJson);
return response.body();
}
@Override
public void chatVision(String prompt, List<String> images, String detail, Consumer<String> callback) {
Map<String, Object> paramMap = buildChatVisionStreamRequestBody(prompt, images, detail);
System.out.println(JSONUtil.toJsonStr(paramMap));
ThreadUtil.newThread(() -> sendPostStream(CHAT_ENDPOINT, paramMap, callback::accept), "hutool-chatVision-sse").start();
}
@Override
public String tokenizeText(String text) {
String paramJson = buildTokenizeRequestBody(text);
final HttpResponse response = sendPost(TOKENIZE_TEXT, paramJson);
return response.body();
}
@Override
public String imagesGenerations(String prompt) {
String paramJson = buildImagesGenerationsRequestBody(prompt);
final HttpResponse response = sendPost(IMAGES_GENERATIONS, paramJson);
return response.body();
}
@Override
public String embeddingVision(String text, String image) {
String paramJson = buildEmbeddingVisionRequestBody(text, image);
final HttpResponse response = sendPost(EMBEDDING_VISION, paramJson);
return response.body();
}
@Override
public InputStream tts(String input, final HutoolCommon.HutoolSpeech voice) {
try {
String paramJson = buildTTSRequestBody(input, voice.getVoice());
final HttpResponse response = sendPost(TTS, paramJson);
// 检查响应内容类型
String contentType = response.header("Content-Type");
if (contentType != null && contentType.startsWith("application/json")) {
// 如果是JSON响应说明有错误
String errorBody = response.body();
throw new AIException("TTS请求失败: " + errorBody);
}
// 默认返回音频流
return response.bodyStream();
} catch (Exception e) {
throw new AIException("TTS处理失败: " + e.getMessage(), e);
}
}
@Override
public String stt(final File file) {
final Map<String, Object> paramMap = buildSTTRequestBody(file);
final HttpResponse response = sendFormData(STT, paramMap);
return response.body();
}
@Override
public String videoTasks(String text, String image, final List<HutoolCommon.HutoolVideo> videoParams) {
String paramJson = buildGenerationsTasksRequestBody(text, image, videoParams);
final HttpResponse response = sendPost(CREATE_VIDEO, paramJson);
return response.body();
}
@Override
public String getVideoTasksInfo(String taskId) {
final HttpResponse response = sendGet(CREATE_VIDEO + "/" + taskId);
return response.body();
}
// 构建chat请求体
private String buildChatRequestBody(final List<Message> messages) {
//使用JSON工具
final Map<String, Object> paramMap = new HashMap<>();
paramMap.put("model", config.getModel());
paramMap.put("messages", messages);
//合并其他参数
paramMap.putAll(config.getAdditionalConfigMap());
return JSONUtil.toJsonStr(paramMap);
}
private Map<String, Object> buildChatStreamRequestBody(final List<Message> messages) {
//使用JSON工具
final Map<String, Object> paramMap = new HashMap<>();
paramMap.put("stream", true);
paramMap.put("model", config.getModel());
paramMap.put("messages", messages);
//合并其他参数
paramMap.putAll(config.getAdditionalConfigMap());
return paramMap;
}
//构建chatVision请求体
private String buildChatVisionRequestBody(String prompt, final List<String> images, String detail) {
// 定义消息结构
final List<Message> messages = new ArrayList<>();
final List<Object> content = new ArrayList<>();
final Map<String, String> contentMap = new HashMap<>();
contentMap.put("type", "text");
contentMap.put("text", prompt);
content.add(contentMap);
for (String img : images) {
HashMap<String, Object> imgUrlMap = new HashMap<>();
imgUrlMap.put("type", "image_url");
HashMap<String, String> urlMap = new HashMap<>();
urlMap.put("url", img);
urlMap.put("detail", detail);
imgUrlMap.put("image_url", urlMap);
content.add(imgUrlMap);
}
messages.add(new Message("user", content));
//使用JSON工具
final Map<String, Object> paramMap = new HashMap<>();
paramMap.put("model", config.getModel());
paramMap.put("messages", messages);
//合并其他参数
paramMap.putAll(config.getAdditionalConfigMap());
return JSONUtil.toJsonStr(paramMap);
}
private Map<String, Object> buildChatVisionStreamRequestBody(String prompt, final List<String> images, String detail) {
// 定义消息结构
final List<Message> messages = new ArrayList<>();
final List<Object> content = new ArrayList<>();
final Map<String, String> contentMap = new HashMap<>();
contentMap.put("type", "text");
contentMap.put("text", prompt);
content.add(contentMap);
for (String img : images) {
HashMap<String, Object> imgUrlMap = new HashMap<>();
imgUrlMap.put("type", "image_url");
HashMap<String, String> urlMap = new HashMap<>();
urlMap.put("url", img);
urlMap.put("detail", detail);
imgUrlMap.put("image_url", urlMap);
content.add(imgUrlMap);
}
messages.add(new Message("user", content));
//使用JSON工具
final Map<String, Object> paramMap = new HashMap<>();
paramMap.put("stream", true);
paramMap.put("model", config.getModel());
paramMap.put("messages", messages);
//合并其他参数
paramMap.putAll(config.getAdditionalConfigMap());
return paramMap;
}
//构建分词请求体
private String buildTokenizeRequestBody(String text) {
//使用JSON工具
final Map<String, Object> paramMap = new HashMap<>();
paramMap.put("model", config.getModel());
paramMap.put("text", text);
//合并其他参数
paramMap.putAll(config.getAdditionalConfigMap());
return JSONUtil.toJsonStr(paramMap);
}
//构建文生图请求体
private String buildImagesGenerationsRequestBody(String prompt) {
final Map<String, Object> paramMap = new HashMap<>();
paramMap.put("model", config.getModel());
paramMap.put("prompt", prompt);
//合并其他参数
paramMap.putAll(config.getAdditionalConfigMap());
return JSONUtil.toJsonStr(paramMap);
}
//构建图文向量化请求体
private String buildEmbeddingVisionRequestBody(String text, String image) {
//使用JSON工具
final Map<String, Object> paramMap = new HashMap<>();
paramMap.put("model", config.getModel());
final List<Object> input = new ArrayList<>();
//添加文本参数
if (!StrUtil.isBlank(text)) {
final Map<String, String> textMap = new HashMap<>();
textMap.put("type", "text");
textMap.put("text", text);
input.add(textMap);
}
//添加图片参数
if (!StrUtil.isBlank(image)) {
final Map<String, Object> imgUrlMap = new HashMap<>();
imgUrlMap.put("type", "image_url");
final Map<String, String> urlMap = new HashMap<>();
urlMap.put("url", image);
imgUrlMap.put("image_url", urlMap);
input.add(imgUrlMap);
}
paramMap.put("input", input);
//合并其他参数
paramMap.putAll(config.getAdditionalConfigMap());
System.out.println(JSONUtil.toJsonStr(paramMap));
return JSONUtil.toJsonStr(paramMap);
}
//构建TTS请求体
private String buildTTSRequestBody(String input, String voice) {
final Map<String, Object> paramMap = new HashMap<>();
paramMap.put("model", config.getModel());
paramMap.put("input", input);
paramMap.put("voice", voice);
//合并其他参数
paramMap.putAll(config.getAdditionalConfigMap());
return JSONUtil.toJsonStr(paramMap);
}
//构建STT请求体
private Map<String, Object> buildSTTRequestBody(final File file) {
final Map<String, Object> paramMap = new HashMap<>();
paramMap.put("model", config.getModel());
paramMap.put("file", file);
//合并其他参数
paramMap.putAll(config.getAdditionalConfigMap());
return paramMap;
}
//构建创建视频任务请求体
private String buildGenerationsTasksRequestBody(String text, String image, final List<HutoolCommon.HutoolVideo> videoParams) {
//使用JSON工具
final Map<String, Object> paramMap = new HashMap<>();
paramMap.put("model", config.getModel());
final List<Object> content = new ArrayList<>();
//添加文本参数
final Map<String, String> textMap = new HashMap<>();
if (!StrUtil.isBlank(text)) {
textMap.put("type", "text");
textMap.put("text", text);
content.add(textMap);
}
//添加图片参数
if (!StrUtil.isBlank(image)) {
final Map<String, Object> imgUrlMap = new HashMap<>();
imgUrlMap.put("type", "image_url");
final Map<String, String> urlMap = new HashMap<>();
urlMap.put("url", image);
imgUrlMap.put("image_url", urlMap);
content.add(imgUrlMap);
}
//添加视频参数
if (videoParams != null && !videoParams.isEmpty()) {
//如果有文本参数就加在后面
if (textMap != null && !textMap.isEmpty()) {
int textIndex = content.indexOf(textMap);
StringBuilder textBuilder = new StringBuilder(text);
for (HutoolCommon.HutoolVideo videoParam : videoParams) {
textBuilder.append(" ").append(videoParam.getType()).append(" ").append(videoParam.getValue());
}
textMap.put("type", "text");
textMap.put("text", textBuilder.toString());
if (textIndex != -1) {
content.set(textIndex, textMap);
} else {
content.add(textMap);
}
} else {
//如果没有文本参数就重新增加
StringBuilder textBuilder = new StringBuilder();
for (HutoolCommon.HutoolVideo videoParam : videoParams) {
textBuilder.append(videoParam.getType()).append(videoParam.getValue()).append(" ");
}
textMap.put("type", "text");
textMap.put("text", textBuilder.toString());
content.add(textMap);
}
}
paramMap.put("content", content);
//合并其他参数
paramMap.putAll(config.getAdditionalConfigMap());
System.out.println(JSONUtil.toJsonStr(paramMap));
return JSONUtil.toJsonStr(paramMap);
}
}

View File

@ -0,0 +1,24 @@
/*
* Copyright (c) 2025 Hutool Team and hutool.cn
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* 对hutool的封装实现
*
* @author elichow
* @since 5.8.39
*/
package cn.hutool.ai.model.hutool;

View File

@ -1,3 +1,4 @@
cn.hutool.ai.model.hutool.HutoolConfig
cn.hutool.ai.model.deepseek.DeepSeekConfig
cn.hutool.ai.model.openai.OpenaiConfig
cn.hutool.ai.model.doubao.DoubaoConfig

View File

@ -1,3 +1,4 @@
cn.hutool.ai.model.hutool.HutoolProvider
cn.hutool.ai.model.deepseek.DeepSeekProvider
cn.hutool.ai.model.openai.OpenaiProvider
cn.hutool.ai.model.doubao.DoubaoProvider

View File

@ -22,6 +22,7 @@ import cn.hutool.ai.core.Message;
import cn.hutool.ai.model.deepseek.DeepSeekService;
import cn.hutool.ai.model.doubao.DoubaoService;
import cn.hutool.ai.model.grok.GrokService;
import cn.hutool.ai.model.hutool.HutoolService;
import cn.hutool.ai.model.openai.OpenaiService;
import org.junit.jupiter.api.Test;
@ -46,6 +47,12 @@ class AIUtilTest {
assertNotNull(aiService);
}
@Test
void getHutoolService() {
final HutoolService hutoolService = AIUtil.getHutoolService(new AIConfigBuilder(ModelName.HUTOOL.getValue()).setApiKey(key).build());
assertNotNull(hutoolService);
}
@Test
void getDeepSeekService() {
final DeepSeekService deepSeekService = AIUtil.getDeepSeekService(new AIConfigBuilder(ModelName.DEEPSEEK.getValue()).setApiKey(key).build());

View File

@ -0,0 +1,191 @@
/*
* Copyright (c) 2025 Hutool Team and hutool.cn
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.hutool.ai.model.hutool;
import cn.hutool.ai.AIException;
import cn.hutool.ai.AIServiceFactory;
import cn.hutool.ai.ModelName;
import cn.hutool.ai.core.AIConfigBuilder;
import cn.hutool.ai.core.Message;
import cn.hutool.core.img.ImgUtil;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.thread.ThreadUtil;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import java.awt.*;
import java.io.File;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import static org.junit.jupiter.api.Assertions.*;
class HutoolServiceTest {
String key = "请前往Hutool-AI官网https://ai.hutool.cn 获取";
HutoolService hutoolService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.HUTOOL.getValue()).setApiKey(key).build(), HutoolService.class);
@Test
@Disabled
void chat(){
final String chat = hutoolService.chat("写一个疯狂星期四广告词");
assertNotNull(chat);
}
@Test
@Disabled
void chatStream() {
String prompt = "写一个疯狂星期四广告词";
// 使用AtomicBoolean作为结束标志
AtomicBoolean isDone = new AtomicBoolean(false);
hutoolService.chat(prompt, data -> {
assertNotNull(data);
if (data.equals("data: [DONE]")) {
// 设置结束标志
isDone.set(true);
} else if (data.contains("\"error\"")) {
isDone.set(true);
}
});
// 轮询检查结束标志
while (!isDone.get()) {
ThreadUtil.sleep(100);
}
}
@Test
@Disabled
void testChat(){
final List<Message> messages = new ArrayList<>();
messages.add(new Message("system","你是个抽象大师,会说很抽象的话,最擅长说抽象的笑话"));
messages.add(new Message("user","给我说一个笑话"));
final String chat = hutoolService.chat(messages);
assertNotNull(chat);
}
@Test
@Disabled
void chatVision() {
final String base64 = ImgUtil.toBase64DataUri(Toolkit.getDefaultToolkit().createImage("your imageUrl"), "png");
final String chatVision = hutoolService.chatVision("图片上有些什么?", Arrays.asList(base64));
assertNotNull(chatVision);
}
@Test
@Disabled
void testChatVisionStream() {
String prompt = "图片上有些什么?";
List<String> images = Arrays.asList("https://img2.baidu.com/it/u=862000265,4064861820&fm=253&fmt=auto&app=138&f=JPEG?w=800&h=1544");
// 使用AtomicBoolean作为结束标志
AtomicBoolean isDone = new AtomicBoolean(false);
hutoolService.chatVision(prompt,images, data -> {
assertNotNull(data);
if (data.equals("data:[DONE]")) {
// 设置结束标志
isDone.set(true);
} else if (data.contains("\"error\"")) {
isDone.set(true);
}
});
// 轮询检查结束标志
while (!isDone.get()) {
ThreadUtil.sleep(100);
}
}
@Test
@Disabled
void testChatVision() {
final String chatVision = hutoolService.chatVision("图片上有些什么?", Arrays.asList("https://img2.baidu.com/it/u=862000265,4064861820&fm=253&fmt=auto&app=138&f=JPEG?w=800&h=1544"));
assertNotNull(chatVision);
}
@Test
@Disabled
void tokenizeText() {
final String tokenizeText = hutoolService.tokenizeText(key);
assertNotNull(tokenizeText);
}
@Test
@Disabled
void imagesGenerations() {
final String imagesGenerations = hutoolService.imagesGenerations("一位年轻的宇航员站在未来感十足的太空站内,透过巨大的弧形落地窗凝望浩瀚宇宙。窗外,璀璨的星河与五彩斑斓的星云交织,远处隐约可见未知星球的轮廓,仿佛在召唤着探索的脚步。宇航服上的呼吸灯与透明显示屏上的星图交相辉映,象征着人类科技与宇宙奥秘的碰撞。画面深邃而神秘,充满对未知的渴望与无限可能的想象。");
assertNotNull(imagesGenerations);
}
@Test
@Disabled
void embeddingVision() {
final String embeddingVision = hutoolService.embeddingVision("天空好难", "https://img2.baidu.com/it/u=862000265,4064861820&fm=253&fmt=auto&app=138&f=JPEG?w=800&h=1544");
assertNotNull(embeddingVision);
}
@Test
@Disabled
void textToSpeech() {
try {
// 测试正常音频流返回
final InputStream inputStream = hutoolService.tts("万里山河一夜白,\n" +
"千峰尽染玉龙哀。\n" +
"长风卷起琼花碎,\n" +
"直上九霄揽月来。", HutoolCommon.HutoolSpeech.NOVA);
assertNotNull(inputStream);
// 保存音频文件
final String filePath = "your filePath";
FileUtil.writeFromStream(inputStream, new File(filePath));
} catch (Exception e) {
throw new AIException("TTS测试失败: " + e.getMessage());
}
}
@Test
@Disabled
void speechToText() {
final File file = FileUtil.file("your filePath");
final String speechToText = hutoolService.stt(file);
assertNotNull(speechToText);
}
@Test
@Disabled
void videoTasks() {
final String videoTasks = hutoolService.videoTasks("生成一段动画视频,主角是大耳朵图图,一个活泼可爱的小男孩。视频中图图在公园里玩耍," +
"画面采用明亮温暖的卡通风格,色彩鲜艳,动作流畅。背景音乐轻快活泼,带有冒险感,音效包括鸟叫声、欢笑声和山洞回声。", "https://img2.baidu.com/it/u=862000265,4064861820&fm=253&fmt=auto&app=138&f=JPEG?w=800&h=1544");
assertNotNull(videoTasks);//cgt-20250529154621-d7dq9
}
@Test
@Disabled
void getVideoTasksInfo() {
final String videoTasksInfo = hutoolService.getVideoTasksInfo("cgt-20250529154621-d7dq9");
assertNotNull(videoTasksInfo);
}
}