|
@@ -0,0 +1,265 @@
|
|
|
|
|
+package cn.iocoder.byzs.module.ai.service.video;
|
|
|
|
|
+
|
|
|
|
|
+import cn.hutool.core.collection.CollUtil;
|
|
|
|
|
+import cn.hutool.core.util.ObjUtil;
|
|
|
|
|
+import cn.hutool.core.util.StrUtil;
|
|
|
|
|
+import cn.hutool.extra.spring.SpringUtil;
|
|
|
|
|
+import cn.hutool.http.HttpUtil;
|
|
|
|
|
+import cn.iocoder.byzs.framework.common.pojo.PageResult;
|
|
|
|
|
+import cn.iocoder.byzs.framework.common.util.object.BeanUtils;
|
|
|
|
|
+import cn.iocoder.byzs.module.ai.controller.admin.video.vo.AiVideoDrawReqVO;
|
|
|
|
|
+import cn.iocoder.byzs.module.ai.controller.admin.video.vo.AiVideoPageReqVO;
|
|
|
|
|
+import cn.iocoder.byzs.module.ai.controller.admin.video.vo.AiVideoPublicPageReqVO;
|
|
|
|
|
+import cn.iocoder.byzs.module.ai.controller.admin.video.vo.AiVideoUpdateReqVO;
|
|
|
|
|
+import cn.iocoder.byzs.module.ai.dal.dataobject.model.AiApiKeyDO;
|
|
|
|
|
+import cn.iocoder.byzs.module.ai.dal.dataobject.model.AiModelDO;
|
|
|
|
|
+import cn.iocoder.byzs.module.ai.dal.dataobject.video.AiVideoDO;
|
|
|
|
|
+import cn.iocoder.byzs.module.ai.dal.mysql.video.AiVideoMapper;
|
|
|
|
|
+import cn.iocoder.byzs.module.ai.enums.video.AiVideoStatusEnum;
|
|
|
|
|
+import cn.iocoder.byzs.module.ai.service.model.AiApiKeyService;
|
|
|
|
|
+import cn.iocoder.byzs.module.ai.service.model.AiModelService;
|
|
|
|
|
+import cn.iocoder.byzs.module.ai.service.model.AiModelServiceImpl;
|
|
|
|
|
+import cn.iocoder.byzs.module.infra.api.file.FileApi;
|
|
|
|
|
+import com.volcengine.ark.runtime.model.content.generation.CreateContentGenerationTaskRequest;
|
|
|
|
|
+import com.volcengine.ark.runtime.model.content.generation.CreateContentGenerationTaskRequest.Content;
|
|
|
|
|
+import com.volcengine.ark.runtime.model.content.generation.CreateContentGenerationTaskResult;
|
|
|
|
|
+import com.volcengine.ark.runtime.model.content.generation.GetContentGenerationTaskRequest;
|
|
|
|
|
+import com.volcengine.ark.runtime.model.content.generation.GetContentGenerationTaskResponse;
|
|
|
|
|
+import com.volcengine.ark.runtime.service.ArkService;
|
|
|
|
|
+import jakarta.annotation.Resource;
|
|
|
|
|
+import lombok.extern.slf4j.Slf4j;
|
|
|
|
|
+import okhttp3.ConnectionPool;
|
|
|
|
|
+import okhttp3.Dispatcher;
|
|
|
|
|
+import org.springframework.scheduling.annotation.Async;
|
|
|
|
|
+import org.springframework.stereotype.Service;
|
|
|
|
|
+
|
|
|
|
|
+import java.time.LocalDateTime;
|
|
|
|
|
+import java.util.ArrayList;
|
|
|
|
|
+import java.util.Collections;
|
|
|
|
|
+import java.util.List;
|
|
|
|
|
+import java.util.concurrent.TimeUnit;
|
|
|
|
|
+
|
|
|
|
|
+import static cn.iocoder.byzs.framework.common.exception.util.ServiceExceptionUtil.exception;
|
|
|
|
|
+import static cn.iocoder.byzs.module.ai.enums.ErrorCodeConstants.VIDEO_NOT_EXISTS;
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * AI 视频 Service 实现类
|
|
|
|
|
+ *
|
|
|
|
|
+ * @author fansili
|
|
|
|
|
+ */
|
|
|
|
|
+@Service
|
|
|
|
|
+@Slf4j
|
|
|
|
|
+public class AiVideoServiceImpl implements AiVideoService {
|
|
|
|
|
+
|
|
|
|
|
+ @Resource
|
|
|
|
|
+ private AiModelService modelService;
|
|
|
|
|
+
|
|
|
|
|
+ @Resource
|
|
|
|
|
+ private AiVideoMapper videoMapper;
|
|
|
|
|
+
|
|
|
|
|
+ @Resource
|
|
|
|
|
+ private FileApi fileApi;
|
|
|
|
|
+ @Resource
|
|
|
|
|
+ private AiApiKeyService apiKeyService;
|
|
|
|
|
+ @Resource
|
|
|
|
|
+ private AiModelServiceImpl aiModelService;
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public PageResult<AiVideoDO> getVideoPageMy(Long userId, AiVideoPageReqVO pageReqVO) {
|
|
|
|
|
+ return videoMapper.selectPageMy(userId, pageReqVO);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public PageResult<AiVideoDO> getVideoPagePublic(AiVideoPublicPageReqVO pageReqVO) {
|
|
|
|
|
+ return videoMapper.selectPage(pageReqVO);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public AiVideoDO getVideo(Long id) {
|
|
|
|
|
+ return videoMapper.selectById(id);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public List<AiVideoDO> getVideoList(List<Long> ids) {
|
|
|
|
|
+ if (CollUtil.isEmpty(ids)) {
|
|
|
|
|
+ return Collections.emptyList();
|
|
|
|
|
+ }
|
|
|
|
|
+ return videoMapper.selectBatchIds(ids);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public Long drawVideo(Long userId, AiVideoDrawReqVO drawReqVO) {
|
|
|
|
|
+ // 1. 校验模型
|
|
|
|
|
+ AiModelDO model = modelService.validateModel(drawReqVO.getModelId());
|
|
|
|
|
+
|
|
|
|
|
+ // 2. 保存数据库
|
|
|
|
|
+ AiVideoDO video = BeanUtils.toBean(drawReqVO, AiVideoDO.class).setUserId(userId)
|
|
|
|
|
+ .setPlatform(model.getPlatform()).setPromptImage(drawReqVO.getPromptImage())
|
|
|
|
|
+ .setModelId(model.getId()).setModel(model.getModel())
|
|
|
|
|
+ .setPublicStatus(false).setStatus(AiVideoStatusEnum.IN_PROGRESS.getStatus());
|
|
|
|
|
+ videoMapper.insert(video);
|
|
|
|
|
+
|
|
|
|
|
+ // 3. 异步绘制,后续前端通过返回的 id 进行轮询结果
|
|
|
|
|
+ executeDrawVideo(video, drawReqVO, model);
|
|
|
|
|
+ return video.getId();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Async
|
|
|
|
|
+ public void executeDrawVideo(AiVideoDO video, AiVideoDrawReqVO reqVO, AiModelDO model) {
|
|
|
|
|
+
|
|
|
|
|
+ String filePath = "";
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+
|
|
|
|
|
+ AiApiKeyDO apiKeyDo = apiKeyService.validateApiKey(model.getKeyId());
|
|
|
|
|
+ String apiKey = apiKeyDo.getApiKey();
|
|
|
|
|
+
|
|
|
|
|
+ ConnectionPool connectionPool = new ConnectionPool(5, 1, TimeUnit.SECONDS);
|
|
|
|
|
+ Dispatcher dispatcher = new Dispatcher();
|
|
|
|
|
+ ArkService service = ArkService.builder()
|
|
|
|
|
+ .dispatcher(dispatcher)
|
|
|
|
|
+ .connectionPool(connectionPool)
|
|
|
|
|
+ .apiKey(apiKey)
|
|
|
|
|
+ .build();
|
|
|
|
|
+
|
|
|
|
|
+ List<Content> contents = new ArrayList<>();
|
|
|
|
|
+ // 图生视频功能
|
|
|
|
|
+ //文本提示词
|
|
|
|
|
+ String promptText = reqVO.getPrompt();
|
|
|
|
|
+ // 第一帧图片
|
|
|
|
|
+ String promptImage = reqVO.getPromptImage();
|
|
|
|
|
+ //视频分辨率
|
|
|
|
|
+ String resolution = reqVO.getResolution();
|
|
|
|
|
+ //视频时长
|
|
|
|
|
+ int duration = reqVO.getDuration();
|
|
|
|
|
+ //是否固定相机位置
|
|
|
|
|
+ boolean cameraFixed = false;
|
|
|
|
|
+ //是否添加水印
|
|
|
|
|
+ boolean watermark = true;
|
|
|
|
|
+
|
|
|
|
|
+ String formattedPrompt = String.format("%s --resolution %s --duration %d --camerafixed %s --watermark %s",
|
|
|
|
|
+ promptText, resolution, duration, cameraFixed, watermark);
|
|
|
|
|
+ contents.add(Content.builder()
|
|
|
|
|
+ .type("text")
|
|
|
|
|
+ .text(formattedPrompt)
|
|
|
|
|
+ .build());
|
|
|
|
|
+
|
|
|
|
|
+ // 首帧图片
|
|
|
|
|
+ if (StrUtil.isNotBlank(promptImage)) {
|
|
|
|
|
+ contents.add(Content.builder()
|
|
|
|
|
+ .type("image_url")
|
|
|
|
|
+ .imageUrl(CreateContentGenerationTaskRequest.ImageUrl.builder()
|
|
|
|
|
+ .url(promptImage) // 请上传可以访问的图片URL
|
|
|
|
|
+ .build())
|
|
|
|
|
+ .build());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 创建视频生成任务
|
|
|
|
|
+ CreateContentGenerationTaskRequest createRequest =
|
|
|
|
|
+ CreateContentGenerationTaskRequest.builder()
|
|
|
|
|
+ .model(model.getModel())
|
|
|
|
|
+ .content(contents)
|
|
|
|
|
+ .build();
|
|
|
|
|
+
|
|
|
|
|
+ CreateContentGenerationTaskResult createResult = service.createContentGenerationTask(createRequest);
|
|
|
|
|
+
|
|
|
|
|
+ // 获取任务详情
|
|
|
|
|
+ String taskId = createResult.getId();
|
|
|
|
|
+ GetContentGenerationTaskRequest getRequest = GetContentGenerationTaskRequest.builder()
|
|
|
|
|
+ .taskId(taskId)
|
|
|
|
|
+ .build();
|
|
|
|
|
+
|
|
|
|
|
+ Integer videoSstatus = AiVideoStatusEnum.SUCCESS.getStatus();
|
|
|
|
|
+ // 轮询查询部分
|
|
|
|
|
+ while (true) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ GetContentGenerationTaskResponse getResponse = service.getContentGenerationTask(getRequest);
|
|
|
|
|
+ String status = getResponse.getStatus();
|
|
|
|
|
+ if ("succeeded".equalsIgnoreCase(status)) {
|
|
|
|
|
+ GetContentGenerationTaskResponse.Content content = getResponse.getContent();
|
|
|
|
|
+
|
|
|
|
|
+ System.out.println("---------图片地址:" + content.getVideoUrl());
|
|
|
|
|
+ // 下载图片并上传到文件服务
|
|
|
|
|
+ byte[] fileContent = HttpUtil.downloadBytes(content.getVideoUrl());
|
|
|
|
|
+ filePath = fileApi.createFile(fileContent);
|
|
|
|
|
+ break;
|
|
|
|
|
+ } else if ("failed".equalsIgnoreCase(status)) {
|
|
|
|
|
+ filePath = "http://errr.png";
|
|
|
|
|
+ videoSstatus = AiVideoStatusEnum.FAIL.getStatus();
|
|
|
|
|
+ break;
|
|
|
|
|
+ } else {
|
|
|
|
|
+ TimeUnit.SECONDS.sleep(3);
|
|
|
|
|
+ log.info("[executeDrawVideo][video({}) 定时生成查询, 状态({})]", video, getResponse);
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (InterruptedException ie) {
|
|
|
|
|
+ Thread.currentThread().interrupt();
|
|
|
|
|
+ break;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 3. 更新数据库
|
|
|
|
|
+ videoMapper.updateById(new AiVideoDO().setId(video.getId()).setStatus(videoSstatus)
|
|
|
|
|
+ .setVideoUrl(filePath).setFinishTime(LocalDateTime.now()));
|
|
|
|
|
+
|
|
|
|
|
+ // shutdown service after all requests is finished
|
|
|
|
|
+ service.shutdownExecutor();
|
|
|
|
|
+
|
|
|
|
|
+ } catch (Exception ex) {
|
|
|
|
|
+ log.error("[executeDrawVideo][video({}) 生成异常]", video, ex);
|
|
|
|
|
+ videoMapper.updateById(new AiVideoDO().setId(video.getId())
|
|
|
|
|
+ .setStatus(AiVideoStatusEnum.FAIL.getStatus())
|
|
|
|
|
+ .setErrorMessage(ex.getMessage()).setFinishTime(LocalDateTime.now()));
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public void deleteVideoMy(Long id, Long userId) {
|
|
|
|
|
+ // 1. 校验是否存在
|
|
|
|
|
+ AiVideoDO video = validateVideoExists(id);
|
|
|
|
|
+ if (ObjUtil.notEqual(video.getUserId(), userId)) {
|
|
|
|
|
+ throw exception(VIDEO_NOT_EXISTS);
|
|
|
|
|
+ }
|
|
|
|
|
+ // 2. 删除记录
|
|
|
|
|
+ videoMapper.deleteById(id);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public PageResult<AiVideoDO> getVideoPage(AiVideoPageReqVO pageReqVO) {
|
|
|
|
|
+ return videoMapper.selectPage(pageReqVO);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public void updateVideo(AiVideoUpdateReqVO updateReqVO) {
|
|
|
|
|
+ // 1. 校验存在
|
|
|
|
|
+ validateVideoExists(updateReqVO.getId());
|
|
|
|
|
+ // 2. 更新发布状态
|
|
|
|
|
+ videoMapper.updateById(BeanUtils.toBean(updateReqVO, AiVideoDO.class));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public void deleteVideo(Long id) {
|
|
|
|
|
+ // 1. 校验存在
|
|
|
|
|
+ validateVideoExists(id);
|
|
|
|
|
+ // 2. 删除
|
|
|
|
|
+ videoMapper.deleteById(id);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private AiVideoDO validateVideoExists(Long id) {
|
|
|
|
|
+ AiVideoDO video = videoMapper.selectById(id);
|
|
|
|
|
+ if (video == null) {
|
|
|
|
|
+ throw exception(VIDEO_NOT_EXISTS);
|
|
|
|
|
+ }
|
|
|
|
|
+ return video;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 获得自身的代理对象,解决 AOP 生效问题
|
|
|
|
|
+ *
|
|
|
|
|
+ * @return 自己
|
|
|
|
|
+ */
|
|
|
|
|
+ private AiVideoServiceImpl getSelf() {
|
|
|
|
|
+ return SpringUtil.getBean(getClass());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+}
|