Commit 9cd97a77e06a2956b20d47ccce97761265c4d75f
Merge remote-tracking branch 'origin/master-jdk21-ai' into master-jdk21-ai
Showing
7 changed files
with
120 additions
and
22 deletions
yudao-module-ai/yudao-spring-boot-starter-ai/pom.xml
| @@ -114,6 +114,12 @@ | @@ -114,6 +114,12 @@ | ||
| 114 | <artifactId>broadscope-bailian-sdk-java</artifactId> | 114 | <artifactId>broadscope-bailian-sdk-java</artifactId> |
| 115 | <version>1.3.0</version> | 115 | <version>1.3.0</version> |
| 116 | </dependency> | 116 | </dependency> |
| 117 | + <dependency> | ||
| 118 | + <groupId>org.apache.httpcomponents</groupId> | ||
| 119 | + <artifactId>httpclient</artifactId> | ||
| 120 | + <version>4.5.14</version> | ||
| 121 | + <scope>compile</scope> | ||
| 122 | + </dependency> | ||
| 117 | </dependencies> | 123 | </dependencies> |
| 118 | 124 | ||
| 119 | </project> | 125 | </project> |
| 120 | \ No newline at end of file | 126 | \ No newline at end of file |
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/image/ImageClient.java
| @@ -19,7 +19,6 @@ package cn.iocoder.yudao.framework.ai.image; | @@ -19,7 +19,6 @@ package cn.iocoder.yudao.framework.ai.image; | ||
| 19 | 19 | ||
| 20 | import cn.iocoder.yudao.framework.ai.model.ModelClient; | 20 | import cn.iocoder.yudao.framework.ai.model.ModelClient; |
| 21 | 21 | ||
| 22 | -@FunctionalInterface | ||
| 23 | public interface ImageClient extends ModelClient<ImagePrompt, ImageResponse> { | 22 | public interface ImageClient extends ModelClient<ImagePrompt, ImageResponse> { |
| 24 | 23 | ||
| 25 | /** | 24 | /** |
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/imageopenai/OpenAiImageApi.java
| 1 | package cn.iocoder.yudao.framework.ai.imageopenai; | 1 | package cn.iocoder.yudao.framework.ai.imageopenai; |
| 2 | 2 | ||
| 3 | +import cn.hutool.json.JSONUtil; | ||
| 3 | import cn.iocoder.yudao.framework.ai.imageopenai.api.OpenAiImageRequest; | 4 | import cn.iocoder.yudao.framework.ai.imageopenai.api.OpenAiImageRequest; |
| 4 | import cn.iocoder.yudao.framework.ai.imageopenai.api.OpenAiImageResponse; | 5 | import cn.iocoder.yudao.framework.ai.imageopenai.api.OpenAiImageResponse; |
| 5 | import cn.iocoder.yudao.framework.ai.util.JacksonUtil; | 6 | import cn.iocoder.yudao.framework.ai.util.JacksonUtil; |
| 6 | import io.netty.channel.ChannelOption; | 7 | import io.netty.channel.ChannelOption; |
| 8 | +import lombok.extern.slf4j.Slf4j; | ||
| 9 | +import org.apache.http.HttpEntity; | ||
| 10 | +import org.apache.http.client.methods.CloseableHttpResponse; | ||
| 11 | +import org.apache.http.client.methods.HttpPost; | ||
| 12 | +import org.apache.http.entity.StringEntity; | ||
| 13 | +import org.apache.http.impl.client.CloseableHttpClient; | ||
| 14 | +import org.apache.http.impl.client.HttpClients; | ||
| 15 | +import org.apache.http.util.EntityUtils; | ||
| 7 | import org.springframework.http.client.reactive.ReactorClientHttpConnector; | 16 | import org.springframework.http.client.reactive.ReactorClientHttpConnector; |
| 8 | import org.springframework.web.reactive.function.BodyInserters; | 17 | import org.springframework.web.reactive.function.BodyInserters; |
| 9 | import org.springframework.web.reactive.function.client.WebClient; | 18 | import org.springframework.web.reactive.function.client.WebClient; |
| 10 | import reactor.netty.http.client.HttpClient; | 19 | import reactor.netty.http.client.HttpClient; |
| 11 | 20 | ||
| 21 | +import java.io.IOException; | ||
| 22 | +import java.net.URI; | ||
| 12 | import java.time.Duration; | 23 | import java.time.Duration; |
| 13 | 24 | ||
| 14 | /** | 25 | /** |
| @@ -17,6 +28,7 @@ import java.time.Duration; | @@ -17,6 +28,7 @@ import java.time.Duration; | ||
| 17 | * author: fansili | 28 | * author: fansili |
| 18 | * time: 2024/3/17 09:53 | 29 | * time: 2024/3/17 09:53 |
| 19 | */ | 30 | */ |
| 31 | +@Slf4j | ||
| 20 | public class OpenAiImageApi { | 32 | public class OpenAiImageApi { |
| 21 | 33 | ||
| 22 | private static final String DEFAULT_BASE_URL = "https://api.openai.com"; | 34 | private static final String DEFAULT_BASE_URL = "https://api.openai.com"; |
| @@ -24,6 +36,8 @@ public class OpenAiImageApi { | @@ -24,6 +36,8 @@ public class OpenAiImageApi { | ||
| 24 | // 发送请求 webClient | 36 | // 发送请求 webClient |
| 25 | private final WebClient webClient; | 37 | private final WebClient webClient; |
| 26 | 38 | ||
| 39 | + private CloseableHttpClient httpclient = HttpClients.createDefault(); | ||
| 40 | + | ||
| 27 | public OpenAiImageApi(String apiKey) { | 41 | public OpenAiImageApi(String apiKey) { |
| 28 | this.apiKey = apiKey; | 42 | this.apiKey = apiKey; |
| 29 | // 创建一个HttpClient实例并设置超时 | 43 | // 创建一个HttpClient实例并设置超时 |
| @@ -37,18 +51,40 @@ public class OpenAiImageApi { | @@ -37,18 +51,40 @@ public class OpenAiImageApi { | ||
| 37 | } | 51 | } |
| 38 | 52 | ||
| 39 | public OpenAiImageResponse createImage(OpenAiImageRequest request) { | 53 | public OpenAiImageResponse createImage(OpenAiImageRequest request) { |
| 40 | - String res = webClient.post() | ||
| 41 | - .uri(uriBuilder -> uriBuilder.path("/v1/images/generations").build()) | ||
| 42 | - .header("Content-Type", "application/json") | ||
| 43 | - .header("Authorization", "Bearer " + apiKey) | ||
| 44 | - // 设置请求体(这里假设jsonStr是一个JSON格式的字符串) | ||
| 45 | - .body(BodyInserters.fromValue(JacksonUtil.toJson(request))) | ||
| 46 | - // 发送请求并获取响应体 | ||
| 47 | - .retrieve() | ||
| 48 | - // 转换响应体为String类型 | ||
| 49 | - .bodyToMono(String.class) | ||
| 50 | - .block(); | ||
| 51 | - // TODO: 2024/3/17 这里发送请求会失败! | ||
| 52 | - return null; | 54 | + HttpPost httpPost = new HttpPost(); |
| 55 | + httpPost.setURI(URI.create(DEFAULT_BASE_URL.concat("/v1/images/generations"))); | ||
| 56 | + httpPost.setHeader("Content-Type", "application/json"); | ||
| 57 | + httpPost.setHeader("Authorization", "Bearer " + apiKey); | ||
| 58 | + httpPost.setEntity(new StringEntity(JacksonUtil.toJson(request), "UTF-8")); | ||
| 59 | + | ||
| 60 | + CloseableHttpResponse response= null; | ||
| 61 | + try { | ||
| 62 | + response = httpclient.execute(httpPost); | ||
| 63 | + HttpEntity entity = response.getEntity(); | ||
| 64 | + String resultJson = EntityUtils.toString(entity); | ||
| 65 | + log.info("openai 图片生成结果: {}", resultJson); | ||
| 66 | + return JSONUtil.toBean(resultJson, OpenAiImageResponse.class); | ||
| 67 | + } catch (IOException e) { | ||
| 68 | + throw new RuntimeException(e); | ||
| 69 | + } finally { | ||
| 70 | + if (response != null) { | ||
| 71 | + try { | ||
| 72 | + response.close(); | ||
| 73 | + } catch (IOException e) { | ||
| 74 | + throw new RuntimeException(e); | ||
| 75 | + } | ||
| 76 | + } | ||
| 77 | + } | ||
| 78 | +// String res = webClient.post() | ||
| 79 | +// .uri(uriBuilder -> uriBuilder.path("/v1/images/generations").build()) | ||
| 80 | +// .header("Content-Type", "application/json") | ||
| 81 | +// .header("Authorization", "Bearer " + apiKey) | ||
| 82 | +// // 设置请求体(这里假设jsonStr是一个JSON格式的字符串) | ||
| 83 | +// .body(BodyInserters.fromValue(JacksonUtil.toJson(request))) | ||
| 84 | +// // 发送请求并获取响应体 | ||
| 85 | +// .retrieve() | ||
| 86 | +// // 转换响应体为String类型 | ||
| 87 | +// .bodyToMono(String.class) | ||
| 88 | +// .block(); | ||
| 53 | } | 89 | } |
| 54 | } | 90 | } |
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/imageopenai/OpenAiImageClient.java
| 1 | package cn.iocoder.yudao.framework.ai.imageopenai; | 1 | package cn.iocoder.yudao.framework.ai.imageopenai; |
| 2 | 2 | ||
| 3 | import cn.hutool.core.bean.BeanUtil; | 3 | import cn.hutool.core.bean.BeanUtil; |
| 4 | +import cn.hutool.core.codec.Base64; | ||
| 5 | +import cn.hutool.http.HttpUtil; | ||
| 4 | import cn.iocoder.yudao.framework.ai.chat.ChatException; | 6 | import cn.iocoder.yudao.framework.ai.chat.ChatException; |
| 5 | import cn.iocoder.yudao.framework.ai.chatyiyan.exception.YiYanApiException; | 7 | import cn.iocoder.yudao.framework.ai.chatyiyan.exception.YiYanApiException; |
| 6 | -import cn.iocoder.yudao.framework.ai.image.ImageClient; | ||
| 7 | -import cn.iocoder.yudao.framework.ai.image.ImageOptions; | ||
| 8 | -import cn.iocoder.yudao.framework.ai.image.ImagePrompt; | ||
| 9 | -import cn.iocoder.yudao.framework.ai.image.ImageResponse; | 8 | +import cn.iocoder.yudao.framework.ai.image.*; |
| 10 | import cn.iocoder.yudao.framework.ai.imageopenai.api.OpenAiImageRequest; | 9 | import cn.iocoder.yudao.framework.ai.imageopenai.api.OpenAiImageRequest; |
| 11 | import cn.iocoder.yudao.framework.ai.imageopenai.api.OpenAiImageResponse; | 10 | import cn.iocoder.yudao.framework.ai.imageopenai.api.OpenAiImageResponse; |
| 12 | -import jdk.jfr.Frequency; | ||
| 13 | import lombok.extern.slf4j.Slf4j; | 11 | import lombok.extern.slf4j.Slf4j; |
| 14 | import org.springframework.retry.RetryCallback; | 12 | import org.springframework.retry.RetryCallback; |
| 15 | import org.springframework.retry.RetryContext; | 13 | import org.springframework.retry.RetryContext; |
| @@ -74,9 +72,15 @@ public class OpenAiImageClient implements ImageClient { | @@ -74,9 +72,15 @@ public class OpenAiImageClient implements ImageClient { | ||
| 74 | // 创建请求 | 72 | // 创建请求 |
| 75 | OpenAiImageRequest request = new OpenAiImageRequest(); | 73 | OpenAiImageRequest request = new OpenAiImageRequest(); |
| 76 | BeanUtil.copyProperties(openAiImageOptions, request); | 74 | BeanUtil.copyProperties(openAiImageOptions, request); |
| 75 | + request.setPrompt(imagePrompt.getInstructions().get(0).getText()); | ||
| 77 | // 发送请求 | 76 | // 发送请求 |
| 78 | OpenAiImageResponse response = openAiImageApi.createImage(request); | 77 | OpenAiImageResponse response = openAiImageApi.createImage(request); |
| 79 | - return null; | 78 | + return new ImageResponse(response.getData().stream().map(res -> { |
| 79 | + byte[] bytes = HttpUtil.downloadBytes(res.getUrl()); | ||
| 80 | + String base64 = Base64.encode(bytes); | ||
| 81 | + return new ImageGeneration(new Image(res.getUrl(), base64)); | ||
| 82 | + }).toList()); | ||
| 80 | }); | 83 | }); |
| 81 | } | 84 | } |
| 85 | + | ||
| 82 | } | 86 | } |
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/imageopenai/OpenAiImageOptions.java
| @@ -2,6 +2,7 @@ package cn.iocoder.yudao.framework.ai.imageopenai; | @@ -2,6 +2,7 @@ package cn.iocoder.yudao.framework.ai.imageopenai; | ||
| 2 | 2 | ||
| 3 | import cn.iocoder.yudao.framework.ai.image.ImageOptions; | 3 | import cn.iocoder.yudao.framework.ai.image.ImageOptions; |
| 4 | import lombok.Data; | 4 | import lombok.Data; |
| 5 | +import lombok.Getter; | ||
| 5 | import lombok.experimental.Accessors; | 6 | import lombok.experimental.Accessors; |
| 6 | 7 | ||
| 7 | /** | 8 | /** |
| @@ -47,6 +48,21 @@ public class OpenAiImageOptions implements ImageOptions { | @@ -47,6 +48,21 @@ public class OpenAiImageOptions implements ImageOptions { | ||
| 47 | // 代表您的终端用户的唯一标识符,有助于OpenAI监控并检测滥用行为。了解更多信息请参考官方文档。 | 48 | // 代表您的终端用户的唯一标识符,有助于OpenAI监控并检测滥用行为。了解更多信息请参考官方文档。 |
| 48 | private String endUserId; | 49 | private String endUserId; |
| 49 | 50 | ||
| 51 | + @Getter | ||
| 52 | + public enum ResponseFormatEnum { | ||
| 53 | + | ||
| 54 | + URL("url"), | ||
| 55 | + BASE64("b64_json"), | ||
| 56 | + | ||
| 57 | + ; | ||
| 58 | + | ||
| 59 | + ResponseFormatEnum(String value) { | ||
| 60 | + this.value = value; | ||
| 61 | + } | ||
| 62 | + | ||
| 63 | + private String value; | ||
| 64 | + } | ||
| 65 | + | ||
| 50 | // | 66 | // |
| 51 | // 适配 spring ai | 67 | // 适配 spring ai |
| 52 | 68 |
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/imageopenai/api/OpenAiImageResponse.java
yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/image/OpenAiImageClientTests.java
| @@ -6,6 +6,14 @@ import cn.iocoder.yudao.framework.ai.imageopenai.OpenAiImageOptions; | @@ -6,6 +6,14 @@ import cn.iocoder.yudao.framework.ai.imageopenai.OpenAiImageOptions; | ||
| 6 | import org.junit.Before; | 6 | import org.junit.Before; |
| 7 | import org.junit.Test; | 7 | import org.junit.Test; |
| 8 | 8 | ||
| 9 | +import javax.imageio.ImageIO; | ||
| 10 | +import javax.swing.*; | ||
| 11 | +import java.awt.image.BufferedImage; | ||
| 12 | +import java.io.ByteArrayInputStream; | ||
| 13 | +import java.io.IOException; | ||
| 14 | +import java.util.Base64; | ||
| 15 | +import java.util.Scanner; | ||
| 16 | + | ||
| 9 | /** | 17 | /** |
| 10 | * author: fansili | 18 | * author: fansili |
| 11 | * time: 2024/3/17 10:40 | 19 | * time: 2024/3/17 10:40 |
| @@ -20,12 +28,40 @@ public class OpenAiImageClientTests { | @@ -20,12 +28,40 @@ public class OpenAiImageClientTests { | ||
| 20 | // 初始化 openAiImageClient | 28 | // 初始化 openAiImageClient |
| 21 | this.openAiImageClient = new OpenAiImageClient( | 29 | this.openAiImageClient = new OpenAiImageClient( |
| 22 | new OpenAiImageApi(""), | 30 | new OpenAiImageApi(""), |
| 23 | - new OpenAiImageOptions() | 31 | + new OpenAiImageOptions().setResponseFormat(OpenAiImageOptions.ResponseFormatEnum.URL.getValue()) |
| 24 | ); | 32 | ); |
| 25 | } | 33 | } |
| 26 | 34 | ||
| 27 | @Test | 35 | @Test |
| 28 | public void callTest() { | 36 | public void callTest() { |
| 29 | - openAiImageClient.call(new ImagePrompt("我和我的小狗,一起在北极和企鹅玩排球。")); | 37 | + ImageResponse call = openAiImageClient.call(new ImagePrompt("我和我的小狗,一起在北极和企鹅玩排球。")); |
| 38 | + System.err.println("url: " + call.getResult().getOutput().getUrl()); | ||
| 39 | + System.err.println("base64: " + call.getResult().getOutput().getB64Json()); | ||
| 40 | + | ||
| 41 | + String base64String = call.getResult().getOutput().getB64Json(); | ||
| 42 | + ImageIcon imageIcon = new ImageIcon(decodeBase64ToImage(base64String)); | ||
| 43 | + JLabel label = new JLabel(imageIcon); | ||
| 44 | + | ||
| 45 | + JFrame frame = new JFrame("Base64 Image Display"); | ||
| 46 | + frame.getContentPane().add(label); | ||
| 47 | + frame.pack(); | ||
| 48 | + frame.setVisible(true); | ||
| 49 | + | ||
| 50 | + // 阻止退出 | ||
| 51 | + Scanner scanner = new Scanner(System.in); | ||
| 52 | + scanner.nextLine(); | ||
| 53 | + } | ||
| 54 | + | ||
| 55 | + | ||
| 56 | + // 将Base64解码为BufferedImage | ||
| 57 | + private static BufferedImage decodeBase64ToImage(String base64String) { | ||
| 58 | + try { | ||
| 59 | + byte[] decodedBytes = Base64.getDecoder().decode(base64String); | ||
| 60 | + ByteArrayInputStream bis = new ByteArrayInputStream(decodedBytes); | ||
| 61 | + return ImageIO.read(bis); | ||
| 62 | + } catch (IOException e) { | ||
| 63 | + System.out.println("Error decoding the base64 image: " + e.getMessage()); | ||
| 64 | + return null; | ||
| 65 | + } | ||
| 30 | } | 66 | } |
| 31 | } | 67 | } |