From 322c079abf2ef33e280887339d39e6ab2a0c805b Mon Sep 17 00:00:00 2001 From: choweli <1030848819@qq.com> Date: Fri, 30 May 2025 14:36:48 +0800 Subject: [PATCH 1/2] =?UTF-8?q?=E6=96=B0=E5=A2=9EHutool-AI=E6=9C=8D?= =?UTF-8?q?=E5=8A=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/java/cn/hutool/ai/AIUtil.java | 12 + .../src/main/java/cn/hutool/ai/ModelName.java | 5 + .../src/main/java/cn/hutool/ai/Models.java | 16 + .../hutool/ai/model/hutool/HutoolCommon.java | 119 ++++++ .../hutool/ai/model/hutool/HutoolConfig.java | 49 +++ .../ai/model/hutool/HutoolProvider.java | 39 ++ .../hutool/ai/model/hutool/HutoolService.java | 170 ++++++++ .../ai/model/hutool/HutoolServiceImpl.java | 380 ++++++++++++++++++ .../hutool/ai/model/hutool/package-info.java | 24 ++ .../services/cn.hutool.ai.core.AIConfig | 1 + .../cn.hutool.ai.core.AIServiceProvider | 1 + hutool-ai/src/test/java/AIUtilTest.java | 7 + .../ai/model/hutool/HutoolServiceTest.java | 193 +++++++++ 13 files changed, 1016 insertions(+) create mode 100644 hutool-ai/src/main/java/cn/hutool/ai/model/hutool/HutoolCommon.java create mode 100644 hutool-ai/src/main/java/cn/hutool/ai/model/hutool/HutoolConfig.java create mode 100644 hutool-ai/src/main/java/cn/hutool/ai/model/hutool/HutoolProvider.java create mode 100644 hutool-ai/src/main/java/cn/hutool/ai/model/hutool/HutoolService.java create mode 100644 hutool-ai/src/main/java/cn/hutool/ai/model/hutool/HutoolServiceImpl.java create mode 100644 hutool-ai/src/main/java/cn/hutool/ai/model/hutool/package-info.java create mode 100644 hutool-ai/src/test/java/cn/hutool/ai/model/hutool/HutoolServiceTest.java diff --git a/hutool-ai/src/main/java/cn/hutool/ai/AIUtil.java b/hutool-ai/src/main/java/cn/hutool/ai/AIUtil.java index 5da188bff..7f4659d62 100644 --- a/hutool-ai/src/main/java/cn/hutool/ai/AIUtil.java +++ b/hutool-ai/src/main/java/cn/hutool/ai/AIUtil.java @@ -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模型服务 * diff --git a/hutool-ai/src/main/java/cn/hutool/ai/ModelName.java b/hutool-ai/src/main/java/cn/hutool/ai/ModelName.java index aa828a609..ceaca7bb8 100644 --- a/hutool-ai/src/main/java/cn/hutool/ai/ModelName.java +++ b/hutool-ai/src/main/java/cn/hutool/ai/ModelName.java @@ -23,6 +23,11 @@ package cn.hutool.ai; * @since 5.8.38 */ public enum ModelName { + + /** + * hutool + */ + HUTOOL("hutool"), /** * deepSeek */ diff --git a/hutool-ai/src/main/java/cn/hutool/ai/Models.java b/hutool-ai/src/main/java/cn/hutool/ai/Models.java index e11a7f2a7..04856d367 100644 --- a/hutool-ai/src/main/java/cn/hutool/ai/Models.java +++ b/hutool-ai/src/main/java/cn/hutool/ai/Models.java @@ -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"), diff --git a/hutool-ai/src/main/java/cn/hutool/ai/model/hutool/HutoolCommon.java b/hutool-ai/src/main/java/cn/hutool/ai/model/hutool/HutoolCommon.java new file mode 100644 index 000000000..4db506a64 --- /dev/null +++ b/hutool-ai/src/main/java/cn/hutool/ai/model/hutool/HutoolCommon.java @@ -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; + } + + } + +} diff --git a/hutool-ai/src/main/java/cn/hutool/ai/model/hutool/HutoolConfig.java b/hutool-ai/src/main/java/cn/hutool/ai/model/hutool/HutoolConfig.java new file mode 100644 index 000000000..7e0fe7356 --- /dev/null +++ b/hutool-ai/src/main/java/cn/hutool/ai/model/hutool/HutoolConfig.java @@ -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"; + } + +} diff --git a/hutool-ai/src/main/java/cn/hutool/ai/model/hutool/HutoolProvider.java b/hutool-ai/src/main/java/cn/hutool/ai/model/hutool/HutoolProvider.java new file mode 100644 index 000000000..c983ced9f --- /dev/null +++ b/hutool-ai/src/main/java/cn/hutool/ai/model/hutool/HutoolProvider.java @@ -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); + } +} diff --git a/hutool-ai/src/main/java/cn/hutool/ai/model/hutool/HutoolService.java b/hutool-ai/src/main/java/cn/hutool/ai/model/hutool/HutoolService.java new file mode 100644 index 000000000..7293d5d0c --- /dev/null +++ b/hutool-ai/src/main/java/cn/hutool/ai/model/hutool/HutoolService.java @@ -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 手动设置图片的质量,取值范围high、low、auto,默认为auto + * @return AI回答 + * @since 5.8.39 + */ + String chatVision(String prompt, final List images, String detail); + + /** + * 图像理解-SSE流式输出 + * + * @param prompt 题词 + * @param images 图片列表/或者图片Base64编码图片列表(URI形式) + * @param detail 手动设置图片的质量,取值范围high、low、auto,默认为auto + * @param callback 流式数据回调函数 + * @since 5.8.39 + */ + void chatVision(String prompt, final List images, String detail,final Consumer callback); + + /** + * 图像理解:模型会依据传入的图片信息以及问题,给出回复。 + * + * @param prompt 题词 + * @param images 传入的图片列表地址/或者图片Base64编码图片列表(URI形式) + * @return AI回答 + * @since 5.8.39 + */ + default String chatVision(String prompt, final List 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 images, final Consumer 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 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); + +} diff --git a/hutool-ai/src/main/java/cn/hutool/ai/model/hutool/HutoolServiceImpl.java b/hutool-ai/src/main/java/cn/hutool/ai/model/hutool/HutoolServiceImpl.java new file mode 100644 index 000000000..a29b49da5 --- /dev/null +++ b/hutool-ai/src/main/java/cn/hutool/ai/model/hutool/HutoolServiceImpl.java @@ -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) { + //初始化grok客户端 + super(config); + } + + @Override + public String chat(final List messages) { + String paramJson = buildChatRequestBody(messages); + final HttpResponse response = sendPost(CHAT_ENDPOINT, paramJson); + return response.body(); + } + + @Override + public void chat(List messages,Consumer callback) { + Map paramMap = buildChatStreamRequestBody(messages); + ThreadUtil.newThread(() -> sendPostStream(CHAT_ENDPOINT, paramMap, callback::accept), "hutool-chat-sse").start(); + } + + @Override + public String chatVision(String prompt, final List 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 images, String detail, Consumer callback) { + Map 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 paramMap = buildSTTRequestBody(file); + final HttpResponse response = sendFormData(STT, paramMap); + return response.body(); + } + + + @Override + public String videoTasks(String text, String image, final List 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 messages) { + //使用JSON工具 + final Map paramMap = new HashMap<>(); + paramMap.put("model", config.getModel()); + paramMap.put("messages", messages); + //合并其他参数 + paramMap.putAll(config.getAdditionalConfigMap()); + + return JSONUtil.toJsonStr(paramMap); + } + + private Map buildChatStreamRequestBody(final List messages) { + //使用JSON工具 + final Map 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 images, String detail) { + // 定义消息结构 + final List messages = new ArrayList<>(); + final List content = new ArrayList<>(); + + final Map contentMap = new HashMap<>(); + contentMap.put("type", "text"); + contentMap.put("text", prompt); + content.add(contentMap); + for (String img : images) { + HashMap imgUrlMap = new HashMap<>(); + imgUrlMap.put("type", "image_url"); + HashMap 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 paramMap = new HashMap<>(); + paramMap.put("model", config.getModel()); + paramMap.put("messages", messages); + //合并其他参数 + paramMap.putAll(config.getAdditionalConfigMap()); + return JSONUtil.toJsonStr(paramMap); + } + + private Map buildChatVisionStreamRequestBody(String prompt, final List images, String detail) { + // 定义消息结构 + final List messages = new ArrayList<>(); + final List content = new ArrayList<>(); + + final Map contentMap = new HashMap<>(); + contentMap.put("type", "text"); + contentMap.put("text", prompt); + content.add(contentMap); + for (String img : images) { + HashMap imgUrlMap = new HashMap<>(); + imgUrlMap.put("type", "image_url"); + HashMap 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 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 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 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 paramMap = new HashMap<>(); + paramMap.put("model", config.getModel()); + + final List input = new ArrayList<>(); + //添加文本参数 + if (!StrUtil.isBlank(text)) { + final Map textMap = new HashMap<>(); + textMap.put("type", "text"); + textMap.put("text", text); + input.add(textMap); + } + //添加图片参数 + if (!StrUtil.isBlank(image)) { + final Map imgUrlMap = new HashMap<>(); + imgUrlMap.put("type", "image_url"); + final Map 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 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 buildSTTRequestBody(final File file) { + final Map 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 videoParams) { + //使用JSON工具 + final Map paramMap = new HashMap<>(); + paramMap.put("model", config.getModel()); + + final List content = new ArrayList<>(); + //添加文本参数 + final Map textMap = new HashMap<>(); + if (!StrUtil.isBlank(text)) { + textMap.put("type", "text"); + textMap.put("text", text); + content.add(textMap); + } + //添加图片参数 + if (!StrUtil.isBlank(image)) { + final Map imgUrlMap = new HashMap<>(); + imgUrlMap.put("type", "image_url"); + final Map 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); + } + +} diff --git a/hutool-ai/src/main/java/cn/hutool/ai/model/hutool/package-info.java b/hutool-ai/src/main/java/cn/hutool/ai/model/hutool/package-info.java new file mode 100644 index 000000000..f89797d27 --- /dev/null +++ b/hutool-ai/src/main/java/cn/hutool/ai/model/hutool/package-info.java @@ -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; diff --git a/hutool-ai/src/main/resources/META-INF/services/cn.hutool.ai.core.AIConfig b/hutool-ai/src/main/resources/META-INF/services/cn.hutool.ai.core.AIConfig index c31ffb46d..f26782976 100644 --- a/hutool-ai/src/main/resources/META-INF/services/cn.hutool.ai.core.AIConfig +++ b/hutool-ai/src/main/resources/META-INF/services/cn.hutool.ai.core.AIConfig @@ -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 diff --git a/hutool-ai/src/main/resources/META-INF/services/cn.hutool.ai.core.AIServiceProvider b/hutool-ai/src/main/resources/META-INF/services/cn.hutool.ai.core.AIServiceProvider index 40e506c9d..d796fd60c 100644 --- a/hutool-ai/src/main/resources/META-INF/services/cn.hutool.ai.core.AIServiceProvider +++ b/hutool-ai/src/main/resources/META-INF/services/cn.hutool.ai.core.AIServiceProvider @@ -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 diff --git a/hutool-ai/src/test/java/AIUtilTest.java b/hutool-ai/src/test/java/AIUtilTest.java index 5a0333f15..2ae9d89be 100644 --- a/hutool-ai/src/test/java/AIUtilTest.java +++ b/hutool-ai/src/test/java/AIUtilTest.java @@ -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()); diff --git a/hutool-ai/src/test/java/cn/hutool/ai/model/hutool/HutoolServiceTest.java b/hutool-ai/src/test/java/cn/hutool/ai/model/hutool/HutoolServiceTest.java new file mode 100644 index 000000000..f54aa4a50 --- /dev/null +++ b/hutool-ai/src/test/java/cn/hutool/ai/model/hutool/HutoolServiceTest.java @@ -0,0 +1,193 @@ +/* + * 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 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 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"); + System.out.println(embeddingVision); + 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"); + System.out.println(videoTasksInfo); + assertNotNull(videoTasksInfo); + } + +} From 008b9fd662f898796a68db44f0405e198c27d746 Mon Sep 17 00:00:00 2001 From: choweli <1030848819@qq.com> Date: Fri, 30 May 2025 14:42:24 +0800 Subject: [PATCH 2/2] =?UTF-8?q?=E6=96=B0=E5=A2=9EHutool-AI=E6=9C=8D?= =?UTF-8?q?=E5=8A=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/java/cn/hutool/ai/model/hutool/HutoolServiceImpl.java | 2 +- .../test/java/cn/hutool/ai/model/hutool/HutoolServiceTest.java | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/hutool-ai/src/main/java/cn/hutool/ai/model/hutool/HutoolServiceImpl.java b/hutool-ai/src/main/java/cn/hutool/ai/model/hutool/HutoolServiceImpl.java index a29b49da5..27b90e75c 100644 --- a/hutool-ai/src/main/java/cn/hutool/ai/model/hutool/HutoolServiceImpl.java +++ b/hutool-ai/src/main/java/cn/hutool/ai/model/hutool/HutoolServiceImpl.java @@ -57,7 +57,7 @@ public class HutoolServiceImpl extends BaseAIService implements HutoolService { private final String CREATE_VIDEO = "/video/generations"; public HutoolServiceImpl(final AIConfig config) { - //初始化grok客户端 + //初始化hutool客户端 super(config); } diff --git a/hutool-ai/src/test/java/cn/hutool/ai/model/hutool/HutoolServiceTest.java b/hutool-ai/src/test/java/cn/hutool/ai/model/hutool/HutoolServiceTest.java index f54aa4a50..7ef56d820 100644 --- a/hutool-ai/src/test/java/cn/hutool/ai/model/hutool/HutoolServiceTest.java +++ b/hutool-ai/src/test/java/cn/hutool/ai/model/hutool/HutoolServiceTest.java @@ -141,7 +141,6 @@ class HutoolServiceTest { @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"); - System.out.println(embeddingVision); assertNotNull(embeddingVision); } @@ -186,7 +185,6 @@ class HutoolServiceTest { @Disabled void getVideoTasksInfo() { final String videoTasksInfo = hutoolService.getVideoTasksInfo("cgt-20250529154621-d7dq9"); - System.out.println(videoTasksInfo); assertNotNull(videoTasksInfo); }