16 changed files with 927 additions and 1 deletions
@ -0,0 +1,64 @@ |
|||
package com.youlai.boot.mini.controller; |
|||
|
|||
import com.youlai.boot.common.annotation.Log; |
|||
import com.youlai.boot.common.annotation.RepeatSubmit; |
|||
import com.youlai.boot.common.enums.ActionTypeEnum; |
|||
import com.youlai.boot.common.enums.LogModuleEnum; |
|||
import com.youlai.boot.common.result.Result; |
|||
import com.youlai.boot.framework.security.util.SecurityUtils; |
|||
import com.youlai.boot.mini.model.dto.PhotoToComicRequest; |
|||
import com.youlai.boot.mini.model.vo.AiTaskCallbackVO; |
|||
import com.youlai.boot.mini.service.AiGenerationService; |
|||
import io.swagger.v3.oas.annotations.Operation; |
|||
import io.swagger.v3.oas.annotations.Parameter; |
|||
import io.swagger.v3.oas.annotations.enums.ParameterIn; |
|||
import io.swagger.v3.oas.annotations.media.Schema; |
|||
import io.swagger.v3.oas.annotations.tags.Tag; |
|||
import jakarta.validation.Valid; |
|||
import lombok.RequiredArgsConstructor; |
|||
import org.springframework.http.MediaType; |
|||
import org.springframework.web.bind.annotation.*; |
|||
import org.springframework.web.multipart.MultipartFile; |
|||
|
|||
import java.util.Collections; |
|||
import java.util.List; |
|||
|
|||
@Tag(name = "AI生成图片视频相关接口") |
|||
@RestController |
|||
@RequestMapping("/api/v1/mini/ai/generation") |
|||
@RequiredArgsConstructor |
|||
@Valid |
|||
public class AiGenerationController { |
|||
|
|||
private final AiGenerationService aiGenerationService; |
|||
|
|||
@Operation(summary = "上传AI生成参考文件", operationId = "AiReferenceSaveFile") |
|||
@PostMapping(value = "/upload-reference", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) |
|||
@RepeatSubmit |
|||
@Log(module = LogModuleEnum.AI_TASK_MEDIA, value = ActionTypeEnum.INSERT) |
|||
public Result<List<String>> uploadReferenceFile( |
|||
@RequestPart(name = "images", required = false) List<MultipartFile> images, |
|||
@RequestPart(name = "videos", required = false) List<MultipartFile> videos |
|||
) { |
|||
Long userId = SecurityUtils.getUserId(); |
|||
List<String> urlList = aiGenerationService.uploadReferenceFile(images, videos, userId); |
|||
return Result.success(urlList); |
|||
} |
|||
|
|||
@Operation(summary = "提交单图漫画生成任务") |
|||
@PostMapping("/generate-single-image") |
|||
@Log(module = LogModuleEnum.AI_GENERATION_TASK, value = ActionTypeEnum.INSERT) |
|||
public Result<String> generateSingleImage(@Valid @RequestBody PhotoToComicRequest request) { |
|||
Long userId = SecurityUtils.getUserId(); |
|||
String taskUuid = aiGenerationService.createAndGenerateImage(request, userId); |
|||
return Result.success(taskUuid); |
|||
} |
|||
|
|||
@Operation(summary = "AI生成任务回调接口") |
|||
@PostMapping("/task/callback") |
|||
@Log(module = LogModuleEnum.AI_GENERATION_TASK, value = ActionTypeEnum.UPDATE) |
|||
public Result<Boolean> taskCallback(@Valid @RequestBody AiTaskCallbackVO vo) { |
|||
boolean success = aiGenerationService.handleTaskCallback(vo.getUuid(), vo.getStatus(), vo.getResultUrl()); |
|||
return Result.success(success); |
|||
} |
|||
} |
|||
@ -0,0 +1,14 @@ |
|||
package com.youlai.boot.mini.mapper; |
|||
|
|||
import com.youlai.boot.mini.model.entity.MiniAiGenerationTask; |
|||
import com.baomidou.mybatisplus.core.mapper.BaseMapper; |
|||
|
|||
/** |
|||
* AI生成任务统一表 Mapper 接口 |
|||
* |
|||
* @author jwy |
|||
* @since |
|||
*/ |
|||
public interface MiniAiGenerationTaskMapper extends BaseMapper<MiniAiGenerationTask> { |
|||
|
|||
} |
|||
@ -0,0 +1,14 @@ |
|||
package com.youlai.boot.mini.mapper; |
|||
|
|||
import com.youlai.boot.mini.model.entity.MiniAiTaskMedia; |
|||
import com.baomidou.mybatisplus.core.mapper.BaseMapper; |
|||
|
|||
/** |
|||
* AI任务媒体文件表 Mapper 接口 |
|||
* |
|||
* @author jwy |
|||
* @since |
|||
*/ |
|||
public interface MiniAiTaskMediaMapper extends BaseMapper<MiniAiTaskMedia> { |
|||
|
|||
} |
|||
@ -0,0 +1,31 @@ |
|||
package com.youlai.boot.mini.model.dto; |
|||
|
|||
import lombok.Data; |
|||
|
|||
/** |
|||
* AI生成接口响应DTO |
|||
* |
|||
* @author youlai |
|||
*/ |
|||
@Data |
|||
public class GenerateResponse { |
|||
/** |
|||
* 响应消息 |
|||
*/ |
|||
private String msg; |
|||
|
|||
/** |
|||
* 响应码,0表示成功 |
|||
*/ |
|||
private Integer code; |
|||
|
|||
/** |
|||
* 生成结果数据 |
|||
*/ |
|||
private GenerationData data; |
|||
|
|||
/** |
|||
* 请求ID |
|||
*/ |
|||
private String request_id; |
|||
} |
|||
@ -0,0 +1,48 @@ |
|||
package com.youlai.boot.mini.model.dto; |
|||
|
|||
import lombok.Data; |
|||
|
|||
import java.util.List; |
|||
|
|||
/** |
|||
* AI生成结果数据DTO |
|||
* |
|||
* @author youlai |
|||
*/ |
|||
@Data |
|||
public class GenerationData { |
|||
/** |
|||
* 使用的模型ID |
|||
*/ |
|||
private String model; |
|||
|
|||
/** |
|||
* 生成的图片列表 |
|||
*/ |
|||
private List<ImageData> data; |
|||
|
|||
/** |
|||
* 错误信息,无错误则为null |
|||
*/ |
|||
private Object error; |
|||
|
|||
/** |
|||
* Token使用统计 |
|||
*/ |
|||
private Object usage; |
|||
|
|||
/** |
|||
* 创建时间戳 |
|||
*/ |
|||
private Long created_at; |
|||
|
|||
/** |
|||
* 使用的工具 |
|||
*/ |
|||
private String tool; |
|||
|
|||
/** |
|||
* 创建时间戳 |
|||
*/ |
|||
private Long created; |
|||
} |
|||
@ -0,0 +1,26 @@ |
|||
package com.youlai.boot.mini.model.dto; |
|||
|
|||
import lombok.Data; |
|||
|
|||
/** |
|||
* 生成的图片数据DTO |
|||
* |
|||
* @author youlai |
|||
*/ |
|||
@Data |
|||
public class ImageData { |
|||
/** |
|||
* 图片URL地址 |
|||
*/ |
|||
private String url; |
|||
|
|||
/** |
|||
* Base64编码的图片JSON |
|||
*/ |
|||
private String b64_json; |
|||
|
|||
/** |
|||
* 图片尺寸,格式如 1664x2496 |
|||
*/ |
|||
private String size; |
|||
} |
|||
@ -0,0 +1,78 @@ |
|||
package com.youlai.boot.mini.model.dto; |
|||
|
|||
import lombok.Data; |
|||
|
|||
import java.util.List; |
|||
|
|||
/** |
|||
* 照片漫画化请求DTO |
|||
* |
|||
* @author youlai |
|||
*/ |
|||
@Data |
|||
public class PhotoToComicRequest { |
|||
/** |
|||
* 使用的模型ID |
|||
*/ |
|||
private String model; |
|||
|
|||
/** |
|||
* 参考图片URL数组 |
|||
*/ |
|||
private List<String> reference_image; |
|||
|
|||
/** |
|||
* 画风: 1=治愈系水彩, 2=Q版卡通, 3=日式漫画, 4=吉卜力风, 5=3D毛绒, 6=搞笑夸张 |
|||
*/ |
|||
private Integer style = 1; |
|||
|
|||
/** |
|||
* 比例: 1=1:1, 2=2:3, 3=3:4, 4=4:3, 5=5:4 |
|||
*/ |
|||
private Integer ratio = 1; |
|||
|
|||
/** |
|||
* 图片尺寸: 1=2K, 2=4K, 3=8K |
|||
*/ |
|||
private Integer img_size = 1; |
|||
|
|||
/** |
|||
* 图片格式: 1=png, 2=jpeg |
|||
*/ |
|||
private Integer img_type = 1; |
|||
|
|||
/** |
|||
* 宠物物种,如:cat, dog |
|||
*/ |
|||
private String species; |
|||
|
|||
/** |
|||
* 宠物品种,如:British Shorthair, Corgi |
|||
*/ |
|||
private String breed; |
|||
|
|||
/** |
|||
* 宠物毛色,如:blue-gray, orange |
|||
*/ |
|||
private String color; |
|||
|
|||
/** |
|||
* 眼睛颜色,如:amber, blue |
|||
*/ |
|||
private String eye_color; |
|||
|
|||
/** |
|||
* 体型,如:round chubby, slim and elegant |
|||
*/ |
|||
private String body_type; |
|||
|
|||
/** |
|||
* 特殊特征,如:flat face, thick coat |
|||
*/ |
|||
private String distinctive_features = ""; |
|||
|
|||
/** |
|||
* 场景描述,用户自定义场景 |
|||
*/ |
|||
private String scene_description; |
|||
} |
|||
@ -0,0 +1,86 @@ |
|||
package com.youlai.boot.mini.model.entity; |
|||
|
|||
import com.baomidou.mybatisplus.annotation.*; |
|||
import io.swagger.v3.oas.annotations.media.Schema; |
|||
import lombok.Getter; |
|||
import lombok.Setter; |
|||
import lombok.ToString; |
|||
import lombok.experimental.Accessors; |
|||
|
|||
import java.io.Serializable; |
|||
import java.util.Date; |
|||
import com.fasterxml.jackson.annotation.JsonFormat; |
|||
|
|||
@Getter |
|||
@Setter |
|||
@ToString |
|||
@Accessors(chain = true) |
|||
@TableName("mini_ai_generation_task") |
|||
@Schema(description = "AI生成任务统一表") |
|||
public class MiniAiGenerationTask implements Serializable { |
|||
|
|||
@TableId(value = "id", type = IdType.AUTO) |
|||
@Schema(description = "笔记ID") |
|||
private Long id; |
|||
|
|||
|
|||
@TableField("uuid") |
|||
@Schema(description = "uuid唯一标识,前后端用这个进行数据交互") |
|||
private String uuid; |
|||
|
|||
@TableField("mini_user_id") |
|||
@Schema(description = "作者用户ID") |
|||
private Long miniUserId; |
|||
|
|||
@TableField("type") |
|||
@Schema(description = "生成类型:img_single, img_grid_4, video") |
|||
private String type; |
|||
|
|||
@TableField("generate_params") |
|||
@Schema(description = "生成参数JSON,不同类型参数不同") |
|||
private String generateParams; |
|||
|
|||
@TableField("status") |
|||
@Schema(description = "任务状态:0=生成中 1=成功 2=失败 3=超时 4=已取消") |
|||
private Byte status; |
|||
|
|||
@TableField("result_resource_url") |
|||
@Schema(description = "生成结果资源URL") |
|||
private String resultResourceUrl; |
|||
|
|||
@TableField("points_consumed") |
|||
@Schema(description = "消耗积分") |
|||
private Integer pointsConsumed; |
|||
|
|||
@TableField("create_time") |
|||
@Schema(description = "创建时间") |
|||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") |
|||
private Date createTime; |
|||
|
|||
@TableField("create_timestamp") |
|||
@Schema(description = "创建时间毫秒级时间戳") |
|||
private Long createTimestamp; |
|||
|
|||
@TableField("create_by") |
|||
@Schema(description = "创建人ID") |
|||
private Long createBy; |
|||
|
|||
@TableField("update_time") |
|||
@Schema(description = "更新时间") |
|||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") |
|||
private Date updateTime; |
|||
|
|||
@TableField("update_timestamp") |
|||
@Schema(description = "更新时间毫秒级时间戳") |
|||
private Long updateTimestamp; |
|||
|
|||
@TableField("update_by") |
|||
@Schema(description = "修改人ID") |
|||
private Long updateBy; |
|||
|
|||
@TableField("is_deleted") |
|||
@Schema(description = "逻辑删除标识(0-未删除 1-已删除)") |
|||
private Boolean deleted; |
|||
|
|||
|
|||
} |
|||
@ -0,0 +1,102 @@ |
|||
package com.youlai.boot.mini.model.entity; |
|||
|
|||
import com.baomidou.mybatisplus.annotation.*; |
|||
import io.swagger.v3.oas.annotations.media.Schema; |
|||
import lombok.Getter; |
|||
import lombok.Setter; |
|||
import lombok.ToString; |
|||
import lombok.experimental.Accessors; |
|||
|
|||
import java.io.Serializable; |
|||
import java.util.Date; |
|||
import com.fasterxml.jackson.annotation.JsonFormat; |
|||
|
|||
@Getter |
|||
@Setter |
|||
@ToString |
|||
@Accessors(chain = true) |
|||
@TableName("mini_ai_task_media") |
|||
@Schema(description = "AI任务媒体文件表") |
|||
public class MiniAiTaskMedia implements Serializable { |
|||
|
|||
@TableId(value = "id", type = IdType.AUTO) |
|||
@Schema(description = "") |
|||
private Long id; |
|||
|
|||
|
|||
@TableField("uuid") |
|||
@Schema(description = "uuid唯一标识,前后端用这个进行数据交互") |
|||
private String uuid; |
|||
|
|||
@TableField("mini_user_id") |
|||
@Schema(description = "用户id") |
|||
private Long miniUserId; |
|||
|
|||
@TableField("task_id") |
|||
@Schema(description = "AI生成任务id") |
|||
private Long taskId; |
|||
|
|||
@TableField("file_source") |
|||
@Schema(description = "文件来源:user_upload(用户上传), ai_generated(AI生成)") |
|||
private String fileSource; |
|||
|
|||
@TableField("media_type") |
|||
@Schema(description = "媒体类型,image-图片,video-视频") |
|||
private String mediaType; |
|||
|
|||
@TableField("source_url") |
|||
@Schema(description = "资源URL") |
|||
private String sourceUrl; |
|||
|
|||
@TableField("storage_key") |
|||
@Schema(description = "对象存储中的key") |
|||
private String storageKey; |
|||
|
|||
@TableField("thumbnail_url") |
|||
@Schema(description = "缩略图URL(视频需要)") |
|||
private String thumbnailUrl; |
|||
|
|||
@TableField("width") |
|||
@Schema(description = "宽度(像素)") |
|||
private Integer width; |
|||
|
|||
@TableField("height") |
|||
@Schema(description = "高度(像素)") |
|||
private Integer height; |
|||
|
|||
@TableField("duration") |
|||
@Schema(description = "时长(秒,视频用)") |
|||
private Integer duration; |
|||
|
|||
@TableField("create_time") |
|||
@Schema(description = "创建时间") |
|||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") |
|||
private Date createTime; |
|||
|
|||
@TableField("create_timestamp") |
|||
@Schema(description = "创建时间毫秒级时间戳") |
|||
private Long createTimestamp; |
|||
|
|||
@TableField("create_by") |
|||
@Schema(description = "创建人ID") |
|||
private Long createBy; |
|||
|
|||
@TableField("update_time") |
|||
@Schema(description = "更新时间") |
|||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") |
|||
private Date updateTime; |
|||
|
|||
@TableField("update_timestamp") |
|||
@Schema(description = "更新时间毫秒级时间戳") |
|||
private Long updateTimestamp; |
|||
|
|||
@TableField("update_by") |
|||
@Schema(description = "修改人ID") |
|||
private Long updateBy; |
|||
|
|||
@TableField("is_deleted") |
|||
@Schema(description = "逻辑删除标识(0-未删除 1-已删除)") |
|||
private Boolean deleted; |
|||
|
|||
|
|||
} |
|||
@ -0,0 +1,25 @@ |
|||
package com.youlai.boot.mini.model.vo; |
|||
|
|||
import io.swagger.v3.oas.annotations.media.Schema; |
|||
import jakarta.validation.constraints.NotBlank; |
|||
import lombok.Data; |
|||
|
|||
/** |
|||
* AI任务回调VO |
|||
* |
|||
* @author youlai |
|||
*/ |
|||
@Data |
|||
@Schema(description = "AI任务回调VO") |
|||
public class AiTaskCallbackVO { |
|||
|
|||
@NotBlank(message = "任务UUID不能为空") |
|||
@Schema(description = "任务UUID", requiredMode = Schema.RequiredMode.REQUIRED) |
|||
private String uuid; |
|||
|
|||
@Schema(description = "生成结果URL") |
|||
private String resultUrl; |
|||
|
|||
@Schema(description = "任务状态:0=生成中 1=成功 2=失败 3=超时 4=已取消") |
|||
private Byte status; |
|||
} |
|||
@ -0,0 +1,19 @@ |
|||
package com.youlai.boot.mini.service; |
|||
|
|||
import com.youlai.boot.mini.model.dto.PhotoToComicRequest; |
|||
import com.youlai.boot.mini.model.entity.MiniAiGenerationTask; |
|||
import org.springframework.web.multipart.MultipartFile; |
|||
|
|||
import java.util.List; |
|||
|
|||
public interface AiGenerationService { |
|||
|
|||
List<String> uploadReferenceFile(List<MultipartFile> images, List<MultipartFile> videos, Long userId); |
|||
|
|||
String createAndGenerateImage(PhotoToComicRequest request, Long userId); |
|||
|
|||
boolean handleTaskCallback(String taskUuid, Byte status, String externalResultUrl); |
|||
|
|||
MiniAiGenerationTask getTaskByUuid(String uuid); |
|||
|
|||
} |
|||
@ -0,0 +1,397 @@ |
|||
package com.youlai.boot.mini.service.impl; |
|||
|
|||
import cn.hutool.core.io.FileUtil; |
|||
import cn.hutool.core.util.IdUtil; |
|||
import cn.hutool.http.HttpRequest; |
|||
import cn.hutool.http.HttpResponse; |
|||
import cn.hutool.http.HttpUtil; |
|||
import cn.hutool.json.JSONUtil; |
|||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; |
|||
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; |
|||
import com.youlai.boot.common.exception.BusinessException; |
|||
import com.youlai.boot.common.util.FileUtils; |
|||
import com.youlai.boot.common.util.JavaVCUtils; |
|||
import com.youlai.boot.common.util.RandomNumberUtils; |
|||
import com.youlai.boot.file.model.FileInfo; |
|||
import com.youlai.boot.file.service.FileService; |
|||
import com.youlai.boot.file.service.impl.AliyunFileService; |
|||
import com.youlai.boot.mini.mapper.MiniAiGenerationTaskMapper; |
|||
import com.youlai.boot.mini.mapper.MiniAiTaskMediaMapper; |
|||
import com.youlai.boot.mini.model.dto.GenerateResponse; |
|||
import com.youlai.boot.mini.model.dto.ImageData; |
|||
import com.youlai.boot.mini.model.dto.PhotoToComicRequest; |
|||
import com.youlai.boot.mini.model.entity.MiniAiGenerationTask; |
|||
import com.youlai.boot.mini.model.entity.MiniAiTaskMedia; |
|||
import com.youlai.boot.mini.service.AiGenerationService; |
|||
import lombok.RequiredArgsConstructor; |
|||
import lombok.extern.slf4j.Slf4j; |
|||
import org.springframework.beans.factory.annotation.Value; |
|||
import org.springframework.stereotype.Service; |
|||
import org.springframework.web.multipart.MultipartFile; |
|||
|
|||
import javax.imageio.ImageIO; |
|||
import java.awt.image.BufferedImage; |
|||
import java.io.File; |
|||
import java.util.ArrayList; |
|||
import java.util.Date; |
|||
import java.util.List; |
|||
import java.util.UUID; |
|||
import java.util.concurrent.CompletableFuture; |
|||
|
|||
@Slf4j |
|||
@Service |
|||
@RequiredArgsConstructor |
|||
public class AiGenerationServiceImpl implements AiGenerationService { |
|||
|
|||
private final MiniAiGenerationTaskMapper aiGenerationTaskMapper; |
|||
private final MiniAiTaskMediaMapper aiTaskMediaMapper; |
|||
private final FileService fileService; |
|||
private final AliyunFileService aliyunFileService; |
|||
|
|||
/** |
|||
* AI生成服务地址 |
|||
*/ |
|||
private static final String AI_GENERATE_URL = "http://127.0.0.1:8001/api/v1/photo-to-comic"; |
|||
|
|||
/** |
|||
* OSS存储目录配置 |
|||
*/ |
|||
private static final String OSS_IMAGE_DIR = "ai/image/"; |
|||
private static final String OSS_VIDEO_DIR = "ai/video/"; |
|||
private static final String OSS_THUMBNAIL_DIR = "ai/thumbnail/"; |
|||
|
|||
@Override |
|||
public List<String> uploadReferenceFile(List<MultipartFile> images, List<MultipartFile> videos, Long userId) { |
|||
// 计算本次上传的文件总数
|
|||
int uploadCount = (images != null ? images.size() : 0) + (videos != null ? videos.size() : 0); |
|||
if (uploadCount == 0) { |
|||
throw new BusinessException("请选择要上传的文件"); |
|||
} |
|||
|
|||
// 校验用户当前未关联任务的上传文件数量,最多5个
|
|||
Long existCount = aiTaskMediaMapper.selectCount(new LambdaQueryWrapper<MiniAiTaskMedia>() |
|||
.eq(MiniAiTaskMedia::getCreateBy, userId) |
|||
.isNull(MiniAiTaskMedia::getTaskId) |
|||
.eq(MiniAiTaskMedia::getFileSource, "user_upload") |
|||
.eq(MiniAiTaskMedia::getDeleted, false)); |
|||
|
|||
if (existCount + uploadCount > 5) { |
|||
throw new BusinessException("最多只能上传5个待生成的参考文件"); |
|||
} |
|||
|
|||
List<String> urlList = new ArrayList<>(); |
|||
long timestamp = System.currentTimeMillis(); |
|||
|
|||
// 处理图片
|
|||
if (images != null && !images.isEmpty()) { |
|||
for (MultipartFile image : images) { |
|||
try { |
|||
String fileName = timestamp + RandomNumberUtils.createRandomLowerLetterAndNumber(8); |
|||
String ext = FileUtil.extName(image.getOriginalFilename()); |
|||
String objectName = OSS_IMAGE_DIR + fileName + "." + ext; |
|||
String url = aliyunFileService.uploadFile(objectName, image.getInputStream()); |
|||
|
|||
// 获取图片信息
|
|||
BufferedImage imageInfo = ImageIO.read(image.getInputStream()); |
|||
|
|||
// 保存媒体记录
|
|||
MiniAiTaskMedia media = new MiniAiTaskMedia(); |
|||
String uuid = UUID.randomUUID().toString().replace("-", ""); |
|||
media.setUuid(uuid) |
|||
.setFileSource("user_upload")//TODO 待整理成枚举,参考项目已有内容
|
|||
.setMediaType("image") |
|||
.setSourceUrl(url) |
|||
.setStorageKey(objectName) |
|||
.setWidth(imageInfo.getWidth()) |
|||
.setHeight(imageInfo.getHeight()) |
|||
.setCreateBy(userId) |
|||
.setCreateTimestamp(timestamp) |
|||
.setCreateTime(new Date(timestamp)); |
|||
|
|||
int result = aiTaskMediaMapper.insert(media); |
|||
if (result > 0) { |
|||
urlList.add(url); |
|||
} |
|||
|
|||
} catch (Exception e) { |
|||
log.error("image upload failed", e); |
|||
} |
|||
} |
|||
} |
|||
|
|||
// 处理视频
|
|||
if (videos != null && !videos.isEmpty()) { |
|||
String tmpPath = System.getProperty("user.dir") + "/tmp"; |
|||
// 确保临时目录存在
|
|||
File tmpDir = new File(tmpPath); |
|||
if (!tmpDir.exists()) { |
|||
tmpDir.mkdirs(); |
|||
} |
|||
|
|||
for (MultipartFile video : videos) { |
|||
try { |
|||
String fileName = timestamp + RandomNumberUtils.createRandomLowerLetterAndNumber(8); |
|||
String ext = FileUtil.extName(video.getOriginalFilename()); |
|||
String objectName = OSS_VIDEO_DIR + fileName + "." + ext; |
|||
String url = aliyunFileService.uploadFile(objectName, video.getInputStream()); |
|||
|
|||
// 保存媒体记录
|
|||
MiniAiTaskMedia media = new MiniAiTaskMedia(); |
|||
String uuid = UUID.randomUUID().toString().replace("-", ""); |
|||
|
|||
media.setUuid(uuid) |
|||
.setFileSource("user_upload") |
|||
.setMediaType("video") |
|||
.setSourceUrl(url) |
|||
.setStorageKey(objectName) |
|||
.setCreateBy(userId) |
|||
.setCreateTimestamp(timestamp) |
|||
.setCreateTime(new Date(timestamp)); |
|||
|
|||
// 获取视频时长
|
|||
FileUtils.saveFile(video, tmpPath, fileName); |
|||
String videoPath = tmpPath + File.separator + fileName; |
|||
double duration = JavaVCUtils.getVideoDuration(videoPath); |
|||
media.setDuration((int) Math.ceil(duration)); |
|||
|
|||
// 生成并上传视频缩略图
|
|||
BufferedImage thumbnail = JavaVCUtils.getVideoThumbnail(videoPath, 1); |
|||
String thumbnailFileName = timestamp + RandomNumberUtils.createRandomLowerLetterAndNumber(8); |
|||
String thumbnailObjectName = OSS_THUMBNAIL_DIR + thumbnailFileName + ".png"; |
|||
String thumbnailUrl = aliyunFileService.uploadFile(thumbnailObjectName, |
|||
FileUtils.bufferedImageToInputStream(thumbnail, "png")); |
|||
media.setThumbnailUrl(thumbnailUrl); |
|||
|
|||
int result = aiTaskMediaMapper.insert(media); |
|||
if (result > 0) { |
|||
urlList.add(url); |
|||
} |
|||
|
|||
// 删除临时文件
|
|||
FileUtils.delete(videoPath); |
|||
|
|||
} catch (Exception e) { |
|||
log.error("video upload failed", e); |
|||
} |
|||
} |
|||
} |
|||
|
|||
return urlList; |
|||
} |
|||
|
|||
@Override |
|||
public String createAndGenerateImage(PhotoToComicRequest request, Long userId) { |
|||
// 创建生成任务
|
|||
MiniAiGenerationTask task = new MiniAiGenerationTask(); |
|||
String taskUuid = UUID.randomUUID().toString().replace("-", ""); |
|||
Date now = new Date(); |
|||
long timestamp = System.currentTimeMillis(); |
|||
|
|||
task.setUuid(taskUuid) |
|||
.setMiniUserId(userId) |
|||
.setType("img_single") |
|||
.setGenerateParams(JSONUtil.toJsonStr(request)) |
|||
.setPointsConsumed(1) // 默认消耗1积分
|
|||
.setStatus((byte) 0) |
|||
.setCreateBy(userId) |
|||
.setCreateTime(now) |
|||
.setCreateTimestamp(timestamp) |
|||
.setUpdateTime(now) |
|||
.setUpdateTimestamp(timestamp) |
|||
.setDeleted(false); |
|||
|
|||
aiGenerationTaskMapper.insert(task); |
|||
|
|||
// 异步调用AI生成接口
|
|||
CompletableFuture.runAsync(() -> { |
|||
try { |
|||
log.info("开始调用AI生成接口,任务UUID:{},请求参数:{}", taskUuid, JSONUtil.toJsonStr(request)); |
|||
|
|||
// 发送HTTP请求
|
|||
HttpResponse response = HttpRequest.post(AI_GENERATE_URL) |
|||
.header("Content-Type", "application/json") |
|||
.body(JSONUtil.toJsonStr(request)) |
|||
.timeout(30000) // 超时30秒
|
|||
.execute(); |
|||
|
|||
if (!response.isOk()) { |
|||
log.error("AI生成接口调用失败,状态码:{},响应内容:{}", response.getStatus(), response.body()); |
|||
updateTaskStatus(taskUuid, (byte) 2, null); |
|||
return; |
|||
} |
|||
|
|||
// 解析响应
|
|||
String responseBody = response.body(); |
|||
GenerateResponse generateResponse = JSONUtil.toBean(responseBody, GenerateResponse.class); |
|||
|
|||
if (generateResponse.getCode() != 0 || generateResponse.getData() == null || generateResponse.getData().getData().isEmpty()) { |
|||
log.error("AI生成接口返回错误,响应内容:{}", responseBody); |
|||
updateTaskStatus(taskUuid, (byte) 2, null); |
|||
return; |
|||
} |
|||
|
|||
// 保存生成的图片
|
|||
ImageData imageData = generateResponse.getData().getData().get(0); |
|||
String externalResultUrl = imageData.getUrl(); |
|||
|
|||
// 下载外部图片到我们的OSS
|
|||
String ossUrl = downloadExternalUrlToOss(externalResultUrl); |
|||
|
|||
// 保存媒体记录
|
|||
MiniAiTaskMedia media = new MiniAiTaskMedia(); |
|||
String mediaUuid = UUID.randomUUID().toString().replace("-", ""); |
|||
|
|||
media.setUuid(mediaUuid) |
|||
.setTaskId(task.getId()) |
|||
.setFileSource("ai_generated") |
|||
.setMediaType("image") |
|||
.setSourceUrl(ossUrl) |
|||
.setCreateBy(userId) |
|||
.setCreateTime(new Date()) |
|||
.setCreateTimestamp(System.currentTimeMillis()) |
|||
.setUpdateTime(new Date()) |
|||
.setUpdateTimestamp(System.currentTimeMillis()) |
|||
.setDeleted(false); |
|||
|
|||
aiTaskMediaMapper.insert(media); |
|||
|
|||
// 更新任务状态为成功
|
|||
updateTaskStatus(taskUuid, (byte) 1, ossUrl); |
|||
log.info("AI生成任务完成,任务UUID:{},结果URL:{}", taskUuid, ossUrl); |
|||
|
|||
} catch (Exception e) { |
|||
log.error("AI生成任务异常,任务UUID:{},异常信息:{}", taskUuid, e.getMessage(), e); |
|||
updateTaskStatus(taskUuid, (byte) 2, null); |
|||
} |
|||
}); |
|||
|
|||
return taskUuid; |
|||
} |
|||
|
|||
@Override |
|||
public boolean handleTaskCallback(String taskUuid, Byte status, String externalResultUrl) { |
|||
try { |
|||
// 查询任务是否存在
|
|||
MiniAiGenerationTask task = getTaskByUuid(taskUuid); |
|||
if (task == null) { |
|||
log.error("回调任务不存在,UUID:{}", taskUuid); |
|||
return false; |
|||
} |
|||
|
|||
// 如果生成成功,下载外部URL到OSS
|
|||
String ossUrl = null; |
|||
if (status == 1 && externalResultUrl != null) { |
|||
ossUrl = downloadExternalUrlToOss(externalResultUrl); |
|||
|
|||
// 保存生成的媒体记录
|
|||
MiniAiTaskMedia media = new MiniAiTaskMedia(); |
|||
String mediaUuid = UUID.randomUUID().toString().replace("-", ""); |
|||
|
|||
media.setUuid(mediaUuid) |
|||
.setTaskId(task.getId()) |
|||
.setFileSource("ai_generated") |
|||
.setMediaType("image") |
|||
.setSourceUrl(ossUrl) |
|||
.setCreateBy(task.getCreateBy()) |
|||
.setCreateTime(new Date()) |
|||
.setCreateTimestamp(System.currentTimeMillis()) |
|||
.setUpdateTime(new Date()) |
|||
.setUpdateTimestamp(System.currentTimeMillis()) |
|||
.setDeleted(false); |
|||
|
|||
aiTaskMediaMapper.insert(media); |
|||
} |
|||
|
|||
// 更新任务状态
|
|||
return updateTaskStatus(taskUuid, status, ossUrl); |
|||
|
|||
} catch (Exception e) { |
|||
log.error("处理AI任务回调异常,UUID:{},异常信息:{}", taskUuid, e.getMessage(), e); |
|||
return false; |
|||
} |
|||
} |
|||
|
|||
@Override |
|||
public MiniAiGenerationTask getTaskByUuid(String uuid) { |
|||
return aiGenerationTaskMapper.selectOne(new LambdaQueryWrapper<MiniAiGenerationTask>() |
|||
.eq(MiniAiGenerationTask::getUuid, uuid) |
|||
.eq(MiniAiGenerationTask::getDeleted, false)); |
|||
} |
|||
|
|||
/** |
|||
* 更新任务状态 |
|||
*/ |
|||
private boolean updateTaskStatus(String uuid, Byte status, String resultUrl) { |
|||
LambdaUpdateWrapper<MiniAiGenerationTask> updateWrapper = new LambdaUpdateWrapper<>(); |
|||
updateWrapper.eq(MiniAiGenerationTask::getUuid, uuid) |
|||
.eq(MiniAiGenerationTask::getDeleted, false) |
|||
.set(MiniAiGenerationTask::getStatus, status) |
|||
.set(resultUrl != null, MiniAiGenerationTask::getResultResourceUrl, resultUrl) |
|||
.set(MiniAiGenerationTask::getUpdateTime, new Date()) |
|||
.set(MiniAiGenerationTask::getUpdateTimestamp, System.currentTimeMillis()); |
|||
|
|||
return aiGenerationTaskMapper.update(null, updateWrapper) > 0; |
|||
} |
|||
|
|||
/** |
|||
* 下载外部URL到OSS,返回OSS访问地址 |
|||
*/ |
|||
private String downloadExternalUrlToOss(String externalUrl) { |
|||
try { |
|||
// 下载文件并获取输入流
|
|||
byte[] fileBytes = HttpUtil.downloadBytes(externalUrl); |
|||
String fileName = IdUtil.fastSimpleUUID() + "." + FileUtil.extName(externalUrl); |
|||
|
|||
// 创建临时的MultipartFile包装类
|
|||
MultipartFile multipartFile = new MultipartFile() { |
|||
@Override |
|||
public String getName() { |
|||
return "file"; |
|||
} |
|||
|
|||
@Override |
|||
public String getOriginalFilename() { |
|||
return fileName; |
|||
} |
|||
|
|||
@Override |
|||
public String getContentType() { |
|||
return "image/png"; |
|||
} |
|||
|
|||
@Override |
|||
public boolean isEmpty() { |
|||
return fileBytes == null || fileBytes.length == 0; |
|||
} |
|||
|
|||
@Override |
|||
public long getSize() { |
|||
return fileBytes.length; |
|||
} |
|||
|
|||
@Override |
|||
public byte[] getBytes() { |
|||
return fileBytes; |
|||
} |
|||
|
|||
@Override |
|||
public java.io.InputStream getInputStream() { |
|||
return new java.io.ByteArrayInputStream(fileBytes); |
|||
} |
|||
|
|||
@Override |
|||
public void transferTo(java.io.File dest) throws java.io.IOException, IllegalStateException { |
|||
FileUtil.writeBytes(fileBytes, dest); |
|||
} |
|||
}; |
|||
|
|||
// 上传到OSS
|
|||
FileInfo fileInfo = fileService.uploadFile(multipartFile); |
|||
return fileInfo.getUrl(); |
|||
|
|||
} catch (Exception e) { |
|||
log.error("下载外部URL到OSS失败,URL:{},异常信息:{}", externalUrl, e.getMessage(), e); |
|||
throw new BusinessException("下载生成结果失败"); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,9 @@ |
|||
<?xml version="1.0" encoding="UTF-8"?> |
|||
<!DOCTYPE mapper |
|||
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" |
|||
"http://mybatis.org/dtd/mybatis-3-mapper.dtd"> |
|||
|
|||
<mapper namespace="com.youlai.boot.mini.mapper.MiniAiGenerationTaskMapper"> |
|||
|
|||
|
|||
</mapper> |
|||
@ -0,0 +1,9 @@ |
|||
<?xml version="1.0" encoding="UTF-8"?> |
|||
<!DOCTYPE mapper |
|||
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" |
|||
"http://mybatis.org/dtd/mybatis-3-mapper.dtd"> |
|||
|
|||
<mapper namespace="com.youlai.boot.mini.mapper.MiniAiTaskMediaMapper"> |
|||
|
|||
|
|||
</mapper> |
|||
Loading…
Reference in new issue