【智能协同云图库】智能协同云图库第八弹:基于阿里云百炼大模型—实现 AI 扩图功能
AI 扩图功能
需求分析
随着 AI 的高速发展,AI 几乎可以应用到任何传统业务中,增强应用的功能,带给用户更好的体验。
对于图库网站来说,AI 也有非常多的应用空间,比如可以利用 AI 绘图大模型来编辑图片,实现扩图、擦除补全、图配文、去水印等功能。
以 AI 扩图功能为例,用户可以选择一张已上传的图片,通过 AI 编辑得到新的图片,并根据情况自行选择是否保存。
该功能不限制仅在空间内使用,公共图库也可以支持。
方案设计
1. AI 绘图大模型选择
AI绘图大模型我们自己是搞不来的,可以选择一个市面上支持AI绘图的大模型。
选择 AI 大模型时,我们最关注的应该是生成效果、生成速度还有价格了吧?当然,对我们学习来说,最关注的还是价格,毕竟绘画大模型的费用不低。
国内的 AI 绘图大模型比较推荐阿里云百炼,它是一站式的大模型开发及应用构建平台,可以通过简单的界面操作,在5分钟内开发出一款大模型应用,并在线体验效果。
创建好应用后,利用官方提供的 API 或SDK,直接通过几行代码,就能在项目中使用大模型应用:
通过阅读 官方文档,发现它是支持AI图像编辑与生成功能的,包括 AI 扩图,支持 HTTP 调用,符合我们的需求。
在控制台也能看到对应的图像画面扩展模型:
百炼的大模型提供了新人免费额度,可以通过文档或者点进大模型了解,对于学习用来说足够了:
经过测试,图片生成效果、生成速度都是不错的,因此,本项目将选用阿里云百炼实现AI扩图功能。
建议:之前没接触过类似 AI 大模型平台的同学,先多利用网页控制台熟悉 AI 大模型的 Prompt,了解不同大模型的区别。
推荐学习网站:WaytoAGI-通往AGI之路,最好的 AI 知识库和工具站
2. 调用方式
通过阅读 AI 图像扩展的官方文档,我们发现,API 只支持异步方式调用。
这是因为 AI 绘画任务计算量大且耗时长,同步调用会导致服务器线程长时间被单个任务占用,限制了并发处理能力,增加了超时和系统崩溃的风险。
通过异步调用,服务器可以将任务放入队列中,合理调度资源,避免阻塞主线程,从而更高效地服务多个用户请求,提升整体系统的稳定性和可扩展性。
特点:客户端可以直接获取到结果,调用更方便。
异步调用流程如下,客户端需要在提交任务后,不断轮询请求,来检查任务是否执行完成。
由于 AI 接口已经选择了异步调用,所以我们作为要调用 AI 接口的客户端,要使用轮询的方式来检查任务状态是否为“已完成”,如果完成了,才可以获取到生成的图片。
那么是前端轮询还是后端轮询呢?
前端轮询
- 流程:前端调用后端提交任务后得到任务 ID,然后通过定时器轮询请求查询任务状态接口,直到任务完成或失败。
- 示例代码:
// 提交任务async function submitTask() { const response = await fetch(\'/api/createTask\', { method: \'POST\' }); const { taskId } = await response.json(); checkTaskStatus(taskId);}// 调用submitTask();// 检查任务状态async function checkTaskStatus(taskId) { const intervalId = setInterval(async () => { const response = await fetch(`/api/taskStatus?taskId=${taskId}`); const { status, result } = await response.json(); if (status === \'success\') { console.log(\'Task completed:\', result); clearInterval(intervalId); // 停止轮询 } else if (status === \'failed\') { console.error(\'Task failed\'); clearInterval(intervalId); // 停止轮询 } }, 2000); // 每隔 2 秒轮询}
后端轮询
- 流程:后端通过循环或定时任务检测任务状态,接口保持阻塞,直到任务完成或失败,直接返回结果给前端。
- 示例代码:
@RestControllerpublic class TaskController { @PostMapping(\"/createTask\") public String createTask() { String taskId = taskService.submitTask(); return taskId; } @GetMapping(\"/waitForTask\") public ResponseEntity<String> waitForTask(@RequestParam String taskId) { while (true) { String status = taskService.checkTaskStatus(taskId); if (\"success\".equals(status)) { return ResponseEntity.ok(\"Task completed\"); } else if (\"failed\".equals(status)) { return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(\"Task failed\"); } try { Thread.sleep(2000); // 等待 2 秒后重试 } catch (InterruptedException e) { return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(\"Error occurred\"); } } }}
- 后端轮询问题:后端轮询容易因为任务阻塞导致资源耗尽。
- 推荐方案:通常推荐前端轮询。除非有明确的需求要求时,才考虑后端轮询,比如任务结果需实时返回且对网络请求数敏感。(或者学习时不想写前端的同学哈哈)
- 选择:此处我们也选择前端轮询方案实现。
💡 小贴士:从这个方案设计中,我们也能感受到,如果你同时了解前端和后端,可以结合二者设计出更合理的方案,而不是把所有的“重担”都交给前端或者后端一方。所以企业中开需求评审会或者讨论方案时,前后端需要紧密协作。
后端开发
1. AI 扩图 API
(1) 创建 API Key
首先开发业务依赖的基础能力,也就是 AI 扩图 API。
1. 需要先进入阿里云百炼控制台开通服务:
2. 开通推理能力:
3. 开通之后,我们要在控制台获取API Key,可参考文档:
开通之后,在控制台获取 API Key,可参考文档。
能够在控制台查看到 API Key,注意,API Key 一定不要对外泄露!
接下来,我们需要根据下面的请求头使用 Java 来构造扩图请求实体类数据模型
:
通过阅读文档发现,百炼支持通过 SDK 或 HTTP 调用。
虽然官方写的支持 Java SDK,但 AI 扩图功能中对 SDK 的介绍非常少,此处考虑到兼容性,我们还是使用 HTTP 调用。
4. 由于使用异步的方式,需要开发创建任务和查询结果 2 个 API。
5. 填写配置文件:在配置文件中填写获取到的 apiKey
:
# 阿里云 AI 配置aliYunAi: apiKey: xxxx
(2) 创建请求参数接收类
在 api
包下新建 aliyunai
包,存放阿里云 AI 相关代码。
在 aliyunai.model
包下新建数据模型类,可以让 AI 根据官方文档中的请求响应信息,自动生成请求实体类,无需自己手动编写。
复制下面的请求体内容,交给 AI 生成:
code is cheap, show me the talk !
由于每个 AI 图片处理操作的请求响应都有一些区别,所以单独给 AI 扩图功能编写具体的请求响应类。创建扩图任务请求类:
@Datapublic class CreateOutPaintingTaskRequest implements Serializable { /** * 模型,例如 \"image-out-painting\" */ private String model = \"image-out-painting\"; /** * 输入图像信息 */ private Input input; /** * 图像处理参数 */ private Parameters parameters; @Data public static class Input { /** * 必选,图像 URL */ @Alias(\"image_url\") private String imageUrl; } @Data public static class Parameters implements Serializable { /** * 可选,逆时针旋转角度,默认值 0,取值范围 [0, 359] */ private Integer angle; /** * 可选,输出图像的宽高比,默认空字符串,不设置宽高比 * 可选值:[\"\", \"1:1\", \"3:4\", \"4:3\", \"9:16\", \"16:9\"] */ @Alias(\"output_ratio\") private String outputRatio; /** * 可选,图像居中,在水平方向上按比例扩展,默认值 1.0,范围 [1.0, 3.0] */ @Alias(\"x_scale\") @JsonProperty(\"xScale\") private Float xScale; /** * 可选,图像居中,在垂直方向上按比例扩展,默认值 1.0,范围 [1.0, 3.0] */ @Alias(\"y_scale\") @JsonProperty(\"yScale\") private Float yScale; /** * 可选,在图像上方添加像素,默认值 0 */ @Alias(\"top_offset\") private Integer topOffset; /** * 可选,在图像下方添加像素,默认值 0 */ @Alias(\"bottom_offset\") private Integer bottomOffset; /** * 可选,在图像左侧添加像素,默认值 0 */ @Alias(\"left_offset\") private Integer leftOffset; /** * 可选,在图像右侧添加像素,默认值 0 */ @Alias(\"right_offset\") private Integer rightOffset; /** * 可选,开启图像最佳质量模式,默认值 false * 若为 true,耗时会成倍增加 */ @Alias(\"best_quality\") private Boolean bestQuality; /** * 可选,限制模型生成的图像文件大小,默认值 true * - 单边长度 10000:输出图像文件大小限制为 10MB 以下 */ @Alias(\"limit_image_size\") private Boolean limitImageSize; /** * 可选,添加 \"Generated by AI\" 水印,默认值 true */ @Alias(\"add_watermark\") private Boolean addWatermark = false; }}
注意:上述代码中,某些字段打上了 Hutool 工具类的 @Alias
注解。这个注解仅对 Hutool 的 JSON 转换生效
,对 SpringMVC 的 JSON 转换没有任何影响。
这里有一个很坑的地方,经过测试发现,前端如果传递参数名
xScale
,是无法赋值给xScale
字段的;但是传递参数名xscale
,就可以赋值。这是因为 SpringMVC 对于第二个字母是大写的参数无法映射(和参数类别无关)。参考博客
解决方案:给这些字段增加 @JsonProperty
注解。
/** * 可选,图像居中,在水平方向上按比例扩展,默认值 1.0,范围 [1.0, 3.0] */@Alias(\"x_scale\")@JsonProperty(\"xScale\")private Float xScale;/** * 可选,图像居中,在垂直方向上按比例扩展,默认值 1.0,范围 [1.0, 3.0] */@Alias(\"y_scale\")@JsonProperty(\"yScale\")private Float yScale;
为什么 SpringMVC 要这样设计,通过查阅了解到,这是因为 Jackson 在处理字段名与 JSON 属性名映射时,会依赖 Java 的
标准命名规范
和反射 API
。
- 举个例子,根据 JavaBean 的规范,属性名称与其访问器方法(getter 和 setter)之间的映射规则是:如果属性名以小写字母开头,第二个字母是大写(如
geteMail()
和seteMail()
。- 但 Jackson 会尝试推断属性名为
记住结论即可:
SpringMVC 默认的序列化器 Jackson 在字段名的第二个字母为大写时,无法正确映射;需要使用 @JsonProperty(\"yScale\") 这样的注解正确映射
(3) 创建扩图任务响应类
这个类同理,不要自己写,直接使用 AI 生成:
@Data@NoArgsConstructor@AllArgsConstructorpublic class CreateOutPaintingTaskResponse { private Output output; /** * 表示任务的输出信息 */ @Data public static class Output { /** * 任务 ID */ private String taskId; /** * 任务状态 * * - PENDING:排队中
* - RUNNING:处理中
* - SUSPENDED:挂起
* - SUCCEEDED:执行成功
* - FAILED:执行失败
* - UNKNOWN:任务不存在或状态未知
*
*/ private String taskStatus; } /** * 接口错误码。 * 接口成功请求不会返回该参数。
*/ private String code; /** * 接口错误信息。 * 接口成功请求不会返回该参数。
*/ private String message; /** * 请求唯一标识。 * 可用于请求明细溯源和问题排查。
*/ private String requestId;}
(4) 查询任务响应类
根据官方文档响应的说明
,使用 AI 生成对应的查询任务响应类
:
@Data@NoArgsConstructor@AllArgsConstructorpublic class GetOutPaintingTaskResponse { /** * 请求唯一标识 */ private String requestId; /** * 输出信息 */ private Output output; /** * 表示任务的输出信息 */ @Data public static class Output { /** * 任务 ID */ private String taskId; /** * 任务状态 * * - PENDING:排队中
* - RUNNING:处理中
* - SUSPENDED:挂起
* - SUCCEEDED:执行成功
* - FAILED:执行失败
* - UNKNOWN:任务不存在或状态未知
*
*/ private String taskStatus; /** * 提交时间 * 格式:YYYY-MM-DD HH:mm:ss.SSS */ private String submitTime; /** * 调度时间 * 格式:YYYY-MM-DD HH:mm:ss.SSS */ private String scheduledTime; /** * 结束时间 * 格式:YYYY-MM-DD HH:mm:ss.SSS */ private String endTime; /** * 输出图像的 URL */ private String outputImageUrl; /** * 接口错误码 * 接口成功请求不会返回该参数
*/ private String code; /** * 接口错误信息 * 接口成功请求不会返回该参数
*/ private String message; /** * 任务指标信息 */ private TaskMetrics taskMetrics; } /** * 表示任务的统计信息 */ @Data public static class TaskMetrics { /** * 总任务数 */ private Integer total; /** * 成功任务数 */ private Integer succeeded; /** * 失败任务数 */ private Integer failed; }}
(5) 大模型调用API 开发
开发 API 调用类,通过 Hutool 的 HTTP 请求工具类来调用阿里云百炼的 API。
注解3:创建任务的请求地址,在官方文档中可以找到
注解4:查询任务状态的请求地址,在官方文档中同样可以找到
注解6:根据官方文档填写请求头
注解7:
注解 11:填写查询任务需要发送的请求头
@Slf4j@Component // 1. 这个类需要读取配置文件中的 APIKeypublic class AliYunAiApi { // 2. 使用 @Value 注解 (必须是 Spring 包), 读取需要的配置文件 @Value(\"${aliYunAi.apiKey}\") private String apiKey; // 3. 创建任务地址 public static final String CREATE_OUT_PAINTING_TASK_URL = \"https://dashscope.aliyuncs.com/api/v1/services/aigc/image2image/out-painting\"; // 4. 查询任务状态 %s 用于替换实际任务的 {task_id} public static final String GET_OUT_PAINTING_TASK_URL = \"https://dashscope.aliyuncs.com/api/v1/tasks/%s\"; // 5. 创建任务 public CreateOutPaintingTaskResponse createOutPaintingTask(CreateOutPaintingTaskRequest createOutPaintingTaskRequest){ if(createOutPaintingTaskRequest == null){ throw new BusinessException(ErrorCode.OPERATION_ERROR, \"扩图参数为空\"); }// curl --location --request POST \'https://dashscope.aliyuncs.com/api/v1/services/aigc/image2image/out-painting\' \\// --header \"Authorization: Bearer $DASHSCOPE_API_KEY\" \\// --header \'X-DashScope-Async: enable\' \\// --header \'Content-Type: application/json\' \\// --data \'{// \"model\": \"image-out-painting\",// \"input\": {// \"image_url\": \"http://xxx/image.jpg\"// },// \"parameters\":{// \"angle\": 45,// \"x_scale\":1.5,// \"y_scale\":1.5// }// }\' // 6. 复制上述请求, 然后构造 HTTP 请求, 可以用 AI 生成 HttpRequest httpRequest = HttpRequest.post(CREATE_OUT_PAINTING_TASK_URL) // 注解 3 的创建请求地址 .header(\"Authorization\", \"Bearer\" + apiKey) // 填充自定义 APIKey .header(\"X-DashScope-Async\", \"enable\") // 让用户必需显示开启异步, 也方便后续扩展 .header(\"Content-Type\", \"application/json\") .body(JSONUtil.toJsonStr(createOutPaintingTaskRequest));// 使用 Hutool 的 JSONUtil, 因为刚刚的请求使用了 @Alias // 7. 使用 try...with 方法释放 httpRequest 的资源, 自动释放资源的对象必须实现 AutoCloseable 接口 try(HttpResponse httpResponse = httpRequest.execute()){ // 8. 响应码异常 if(!httpResponse.isOk()){ log.error(\"请求异常: {}\", httpResponse.body()); throw new BusinessException(ErrorCode.OPERATION_ERROR, \"AI 扩图失败\"); } // 9. 将正常的响应体转为 JSON 格式的创建请求的响应对象 CreateOutPaintingTaskResponse createOutPaintingTaskResponse = JSONUtil.toBean(httpResponse.body(), CreateOutPaintingTaskResponse.class); // 10. 拿到响应对象后, 根据响应对象 code 是否有值, 进一步判断扩图是否成功 if (createOutPaintingTaskResponse.getCode() != null){ String errMessage = createOutPaintingTaskResponse.getMessage(); log.error(\"请求异常: {}\", errMessage); throw new BusinessException(ErrorCode.OPERATION_ERROR, \"AI 扩图失败\" + errMessage); } return createOutPaintingTaskResponse; } } /** * 查询创建的任务 * * @param taskId 任务 ID * @return 查询任务响应 */ public GetOutPaintingTaskResponse getOutPaintingTask(String taskId){ if(StrUtil.isBlank(taskId)){ throw new BusinessException(ErrorCode.PARAMS_ERROR, \"任务 ID 不能为空\"); } // 11. 填写请求头, 发送请求// --header \"Authorization: Bearer $DASHSCOPE_API_KEY\" \\// https://dashscope.aliyuncs.com/api/v1/tasks/86ecf553-d340-4e21-xxxxxxxxx String url = String.format(GET_OUT_PAINTING_TASK_URL , taskId); // 注解 4 查询请求的 URL , \"%s\" 替换为 taskId try(HttpResponse httpResponse = HttpRequest.get(url) .header(\"Authorization\", \"Bearer\" + apiKey) .execute()){ // 响应码异常 if(!httpResponse.isOk()){ log.error(\"请求异常: {}\", httpResponse.body()); throw new BusinessException(ErrorCode.OPERATION_ERROR, \"获取任务结果失败\"); } return JSONUtil.toBean(httpResponse.body(), GetOutPaintingTaskResponse.class); } }}
注意:要按照官方文档的要求给请求头增加鉴权信息,拼接配置中写好的 apiKey
。
2. 开发扩图 API 调用接口
(1) 数据模型开发
在 model.dto.picture
包下新建 AI 扩图请求类,用于接受前端传来的参数并传递给 Service 服务层。
字段包括图片 id 和扩图参数:
@Datapublic class CreatePictureOutPaintingTaskRequest implements Serializable { /** * 图片 id */ private Long pictureId; /** * 扩图参数 */ private CreateOutPaintingTaskRequest.Parameters parameters; private static final long serialVersionUID = 1L;}
我们只需要传一个已有的图片,即可实现扩图功能,具体流程:
- 前端构造
Parameters(内部类)
的各个参数,并与图片 ID 一起构造扩图请求; - 前端向后端发送
扩图请求
; - 后端从请求中解析
图片 ID
和图像处理参数 Parameters
; - 查询数据库,找到
图片 ID 对应的图片
,并进行关于图片与空间的鉴权
; - 将
图片的 URL
和Parameters
作为参数,构造API 扩图请求
,调用扩图 API; - 扩图 API 解析请求,校验参数,创建扩图任务;
- 创建的扩图任务,放入
大模型扩图任务队列
中,并生成对应的taskId
返回; - 可以通过调用
查看进度 API
,查看对应 taskId 对应的生成进度
; - 扩图成功后,
查看进度 API
会封装URL
到响应中,返回给前端;
(2) 扩图服务开发
在图片服务中编写创建扩图任务方法,从数据库中获取图片信息和 URL 地址,构造请求参数后调用 API 创建扩图任务
。
注意,如果图片有空间 id,则需要校验权限,直接复用以前的权限校验方法。
@Overridepublic void checkPictureAuth(User loginUser, Picture picture) { Long spaceId = picture.getSpaceId(); Long loginUserId = loginUser.getId(); if (spaceId == null) { // 公共图库, 仅本人和管理员可操作 if (!picture.getUserId().equals(loginUserId) && !userService.isAdmin(loginUser)) { throw new BusinessException(ErrorCode.NO_AUTH_ERROR); } } else { // 私有空间, 仅空间管理员可操作 if (!picture.getUserId().equals(loginUserId)) { throw new BusinessException(ErrorCode.NO_AUTH_ERROR); } }}
在调用大模型接口前,先调用该方法,对图片进行鉴权,只有空间管理员,可以对图片调用扩图 API
接下来,我们对调用大模型 API 进行服务开发:
/** * 创建扩图任务 * @param createPictureOutPaintingTaskRequest 扩图请求 * @param loginUser * @return */CreateOutPaintingTaskResponse createPictureOutPaintingTask(CreatePictureOutPaintingTaskRequest createPictureOutPaintingTaskRequest, User loginUser);
@Resourceprivate AliYunAiApi aliYunAiApi;@Overridepublic CreateOutPaintingTaskResponse createPictureOutPaintingTask(CreatePictureOutPaintingTaskRequest createPictureOutPaintingTaskRequest, User loginUser) { // 1. 根据请求获取图片 ID Long pictureId = createPictureOutPaintingTaskRequest.getPictureId(); // 2. 查询数据库, 获取图片, 如果数据库没有该图片, 抛异常// Picture picture = this.getById(pictureId);// ThrowUtils.throwIf(picture == null, ErrorCode.NOT_FOUND_ERROR, \"图片不存在\"); Picture picture = Optional.ofNullable(this.getById(pictureId)) .orElseThrow(() -> new BusinessException(ErrorCode.NOT_FOUND_ERROR, \"图片不存在\")); // Optional.ofNullable(...): 安全地包装可能为 null 的查询结果; // orElseThrow(...):如果结果确实为 null, 就立即抛出指定的异常; // 3. 校验权限 checkPictureAuth(loginUser, picture); // 4. 创建扩图任务请求 CreateOutPaintingTaskRequest createOutPaintingTaskRequest = new CreateOutPaintingTaskRequest(); CreateOutPaintingTaskRequest.Input input = new CreateOutPaintingTaskRequest.Input(); // 内部类 Input, 也作为参数 input.setImageUrl(picture.getUrl()); createOutPaintingTaskRequest.setInput(input); createOutPaintingTaskRequest.setParameters(createPictureOutPaintingTaskRequest.getParameters()); // 5. 调用 API 创建任务 return aliYunAiApi.createOutPaintingTask(createOutPaintingTaskRequest);}
(3) 扩图接口开发
在 PictureController
添加 AI 扩图接口,包括创建任务和查询任务状态接口:
/** * 创建 AI 扩图任务 */@PostMapping(\"/out_painting/create_task\")public BaseResponse<CreateOutPaintingTaskResponse> createPictureOutPaintingTask( @RequestBody CreatePictureOutPaintingTaskRequest createPictureOutPaintingTaskRequest, HttpServletRequest request) { if (createPictureOutPaintingTaskRequest == null || createPictureOutPaintingTaskRequest.getPictureId() == null) { throw new BusinessException(ErrorCode.PARAMS_ERROR); } User loginUser = userService.getLoginUser(request); CreateOutPaintingTaskResponse response = pictureService.createPictureOutPaintingTask(createPictureOutPaintingTaskRequest, loginUser); return ResultUtils.success(response);}
@Resourceprivate AliYunAiApi aliYunAiApi;/** * 查询 AI 扩图任务 */@GetMapping(\"/out_painting/get_task\")public BaseResponse<GetOutPaintingTaskResponse> getPictureOutPaintingTask(String taskId) { ThrowUtils.throwIf(StrUtil.isBlank(taskId), ErrorCode.PARAMS_ERROR); GetOutPaintingTaskResponse task = aliYunAiApi.getOutPaintingTask(taskId); return ResultUtils.success(task);}
(4) 接口测试
测试图片:
发送扩图任务请求:
响应:
复制 taskId,调用查看任务接口:
打开 outputImageUrl
效果对比:
至此,我们的 AI 图片编辑后端开发完成啦~~~~