diff --git a/dsAiSupport/src/main/java/com/dsideal/aiSupport/Util/KeLing/KlImage2Video.java b/dsAiSupport/src/main/java/com/dsideal/aiSupport/Util/KeLing/KlImage2Video.java new file mode 100644 index 00000000..25eecba8 --- /dev/null +++ b/dsAiSupport/src/main/java/com/dsideal/aiSupport/Util/KeLing/KlImage2Video.java @@ -0,0 +1,85 @@ +package com.dsideal.aiSupport.Util.KeLing; + +public class KlImage2Video { + /* + 创建任务 + 网络协议 请求地址 请求方法 请求格式 响应格式 + https /v1/videos/image2video POST application/json application/json + 请求头 + 字段 值 描述 + Content-Type application/json 数据交换格式 + Authorization 鉴权信息,参考接口鉴权 鉴权信息,参考接口鉴权 + + 请求体: + 字段 类型 必填 默认值 描述 + model_name string 可选 kling-v1 模型名称 枚举值:kling-v1, kling-v1-5, kling-v1-6 + image string 必须 空 参考图像 支持传入图片URL(确保可访问) + 图片格式支持.jpg / .jpeg / .png + 图片文件大小不能超过10MB,图片分辨率不小于300*300px,图片宽高比要在1:2.5 ~ 2.5:1之间 + image 参数与 image_tail 参数至少二选一,二者不能同时为空 + image + image_tail参数、dynamic_masks/static_mask参数、camera_control参数三选一,不能同时使用 + + + 响应体: + { + "code": 0, //错误码;具体定义见错误码 + "message": "string", //错误信息 + "request_id": "string", //请求ID,系统生成,用于跟踪请求、排查问题 + "data": { + "task_id": "string", //任务ID,系统生成 + "task_info": { + //任务创建时的参数信息 + "external_task_id": "string" //客户自定义任务ID + }, + "task_status": "string", //任务状态,枚举值:submitted(已提交)、processing(处理中)、succeed(成功)、failed(失败) + "created_at": 1722769557708, //任务创建时间,Unix时间戳、单位ms + "updated_at": 1722769557708 //任务更新时间,Unix时间戳、单位ms + } +} + +查询任务(单个) +网络协议 请求地址 请求方法 请求格式 响应格式 +https /v1/videos/image2video/{id} GET application/json application/json + +请求头 +字段 值 描述 +Content-Type application/json 数据交换格式 +Authorization 鉴权信息,参考接口鉴权 鉴权信息,参考接口鉴权 +请求路径参数 +字段 类型 必填 默认值 描述 +task_id string 可选 无 图生视频的任务ID +请求路径参数,直接将值填写在请求路径中,与external_task_id两种查询方式二选一 +external_task_id string 可选 无 图生视频的自定义任务ID +创建任务时填写的external_task_id,与task_id两种查询方式二选一 + +响应体: +{ + "code": 0, //错误码;具体定义见错误码 + "message": "string", //错误信息 + "request_id": "string", //请求ID,系统生成,用于跟踪请求、排查问题 + "data":{ + "task_id": "string", //任务ID,系统生成 + "task_status": "string", //任务状态,枚举值:submitted(已提交)、processing(处理中)、succeed(成功)、failed(失败) + "task_status_msg": "string", //任务状态信息,当任务失败时展示失败原因(如触发平台的内容风控等) + "task_info": { //任务创建时的参数信息 + "external_task_id": "string"//客户自定义任务ID + }, + "task_result":{ + "videos":[ + { + "id": "string", //生成的视频ID;全局唯一 + "url": "string", //生成视频的URL,例如https://p1.a.kwimgs.com/bs2/upload-ylab-stunt/special-effect/output/HB1_PROD_ai_web_46554461/-2878350957757294165/output.mp4(请注意,为保障信息安全,生成的图片/视频会在30天后被清理,请及时转存) + "duration": "string" //视频总时长,单位s + } + ] + } + "created_at": 1722769557708, //任务创建时间,Unix时间戳、单位ms + "updated_at": 1722769557708, //任务更新时间,Unix时间戳、单位ms + } +} + + +保存视频 + + * */ +} diff --git a/dsAiSupport/src/main/java/com/dsideal/aiSupport/Util/KeLing/KlText2Video.java b/dsAiSupport/src/main/java/com/dsideal/aiSupport/Util/KeLing/KlText2Video.java new file mode 100644 index 00000000..e5e9f03f --- /dev/null +++ b/dsAiSupport/src/main/java/com/dsideal/aiSupport/Util/KeLing/KlText2Video.java @@ -0,0 +1,309 @@ +package com.dsideal.aiSupport.Util.KeLing; + +import cn.hutool.core.io.FileUtil; +import cn.hutool.http.HttpRequest; +import cn.hutool.http.HttpResponse; +import cn.hutool.http.HttpUtil; +import cn.hutool.json.JSONArray; +import cn.hutool.json.JSONObject; +import cn.hutool.json.JSONUtil; +import com.dsideal.aiSupport.Util.KeLing.Kit.KeLingJwtUtil; +import com.dsideal.aiSupport.Util.KeLing.Kit.KlCommon; +import com.dsideal.aiSupport.Util.KeLing.Kit.KlErrorCode; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +/** + * 可灵AI文生视频工具类 + */ +public class KlText2Video extends KlCommon { + private static final Logger log = LoggerFactory.getLogger(KlText2Video.class); + private static final String BASE_URL = "https://api.klingai.com"; + private static final String GENERATION_PATH = "/v1/videos/text2video"; + private static final String QUERY_PATH = "/v1/videos/text2video/"; + + /** + * 生成视频 + * + * @param prompt 提示词 + * @param modelName 模型名称,枚举值:kling-v1, kling-v1-6 + * @return 任务ID + * @throws Exception 异常信息 + */ + public static String generateVideo(String prompt, String modelName) throws Exception { + return generateVideo(prompt, modelName, null); + } + + /** + * 生成视频 + * + * @param prompt 提示词 + * @param modelName 模型名称,枚举值:kling-v1, kling-v1-6 + * @param externalTaskId 外部任务ID,可为空 + * @return 任务ID + * @throws Exception 异常信息 + */ + public static String generateVideo(String prompt, String modelName, String externalTaskId) throws Exception { + // 获取JWT令牌 + String jwt = KeLingJwtUtil.getJwt(); + + // 创建请求体 + Map requestBody = new HashMap<>(); + requestBody.put("model_name", modelName); + requestBody.put("prompt", prompt); + + // 如果提供了外部任务ID,则添加到请求体中 + if (externalTaskId != null && !externalTaskId.isEmpty()) { + requestBody.put("external_task_id", externalTaskId); + } + + // 使用Hutool发送POST请求 + HttpResponse response = HttpRequest.post(BASE_URL + GENERATION_PATH) + .header("Content-Type", "application/json") + .header("Authorization", "Bearer " + jwt) + .body(JSONUtil.toJsonStr(requestBody)) + .execute(); + + log.info("生成视频请求体:{}", JSONUtil.toJsonStr(requestBody)); + log.info("生成视频响应体:{}", response.body()); + + // 检查响应状态码 + if (response.getStatus() != 200) { + throw new Exception("请求失败,状态码:" + response.getStatus()); + } + + // 解析响应 + String responseBody = response.body(); + JSONObject responseJson = JSONUtil.parseObj(responseBody); + log.info("生成视频响应:{}", responseBody); + + // 检查响应状态 + int code = responseJson.getInt("code"); + if (code != 0) { + String message = responseJson.getStr("message"); + String solution = KlErrorCode.getSolutionByCode(code); + String errorMsg = String.format("生成视频失败:[%d] %s - %s", code, message, solution); + + // 特殊处理资源包耗尽的情况 + if (code == KlErrorCode.RESOURCE_EXHAUSTED.getCode()) { + log.error("可灵AI资源包已耗尽,请充值后再试"); + throw new Exception("可灵AI资源包已耗尽,请充值后再试"); + } + + throw new Exception(errorMsg); + } + + // 获取任务ID + String taskId = responseJson.getJSONObject("data").getStr("task_id"); + log.info("生成视频任务ID:{}", taskId); + + return taskId; + } + + /** + * 查询任务状态 + * + * @param taskId 任务ID + * @return 任务结果 + * @throws Exception 异常信息 + */ + public static JSONObject queryTaskStatus(String taskId) throws Exception { + // 获取JWT令牌 + String jwt = KeLingJwtUtil.getJwt(); + + // 使用Hutool发送GET请求 + HttpResponse response = HttpRequest.get(BASE_URL + QUERY_PATH + taskId) + .header("Content-Type", "application/json") + .header("Authorization", "Bearer " + jwt) + .execute(); + + // 检查响应状态码 + if (response.getStatus() != 200) { + throw new Exception("请求失败,状态码:" + response.getStatus()); + } + + // 解析响应 + String responseBody = response.body(); + JSONObject responseJson = JSONUtil.parseObj(responseBody); + log.info("查询任务状态响应:{}", responseBody); + + // 检查响应状态 + int code = responseJson.getInt("code"); + if (code != 0) { + String message = responseJson.getStr("message"); + String solution = KlErrorCode.getSolutionByCode(code); + String errorMsg = String.format("查询任务状态失败:[%d] %s - %s", code, message, solution); + throw new Exception(errorMsg); + } + + return responseJson; + } + + /** + * 查询任务状态(通过外部任务ID) + * + * @param externalTaskId 外部任务ID + * @return 任务结果 + * @throws Exception 异常信息 + */ + public static JSONObject queryTaskStatusByExternalId(String externalTaskId) throws Exception { + // 获取JWT令牌 + String jwt = KeLingJwtUtil.getJwt(); + + // 使用Hutool发送GET请求 + HttpResponse response = HttpRequest.get(BASE_URL + GENERATION_PATH + "?external_task_id=" + externalTaskId) + .header("Content-Type", "application/json") + .header("Authorization", "Bearer " + jwt) + .execute(); + + // 检查响应状态码 + if (response.getStatus() != 200) { + throw new Exception("请求失败,状态码:" + response.getStatus()); + } + + // 解析响应 + String responseBody = response.body(); + JSONObject responseJson = JSONUtil.parseObj(responseBody); + log.info("查询任务状态响应:{}", responseBody); + + // 检查响应状态 + int code = responseJson.getInt("code"); + if (code != 0) { + String message = responseJson.getStr("message"); + String solution = KlErrorCode.getSolutionByCode(code); + String errorMsg = String.format("查询任务状态失败:[%d] %s - %s", code, message, solution); + throw new Exception(errorMsg); + } + + return responseJson; + } + + /** + * 从URL下载文件到指定路径 + * + * @param fileUrl 文件URL + * @param saveFilePath 保存路径 + * @throws Exception 下载过程中的异常 + */ + public static void downloadFile(String fileUrl, String saveFilePath) throws Exception { + try { + // 使用Hutool下载文件 + long fileSize = HttpUtil.downloadFile(fileUrl, FileUtil.file(saveFilePath)); + log.info("文件下载成功,保存路径: {}, 文件大小: {}字节", saveFilePath, fileSize); + } catch (Exception e) { + log.error("文件下载失败: {}", e.getMessage(), e); + throw e; + } + } + + public static void main(String[] args) throws Exception { + // 提示词和模型名称 + String prompt = "一只可爱的小猫咪在草地上奔跑,阳光明媚"; + String modelName = "kling-v1"; // 可选:kling-v1, kling-v1-6 + + // 生成外部任务ID(可选) + String externalTaskId = "video_" + UUID.randomUUID().toString().replace("-", ""); + + // 添加重试逻辑 + int generateRetryCount = 0; + int maxGenerateRetries = 5; // 最大重试次数 + int generateRetryInterval = 5000; // 重试间隔(毫秒) + + String taskId = null; + boolean accountIssue = false; + + while (generateRetryCount < maxGenerateRetries && !accountIssue) { + try { + taskId = generateVideo(prompt, modelName, externalTaskId); + break; + } catch (Exception e) { + log.error("生成视频异常: {}", e.getMessage(), e); + + // 检查是否是账户问题 + if (e.getMessage().contains("资源包已耗尽") || + e.getMessage().contains("账户欠费") || + e.getMessage().contains("无权限")) { + log.error("账户问题,停止重试"); + accountIssue = true; + } else { + generateRetryCount++; + if (generateRetryCount < maxGenerateRetries) { + log.warn("等待{}毫秒后重试...", generateRetryInterval); + Thread.sleep(generateRetryInterval); + } else { + throw e; // 达到最大重试次数,抛出异常 + } + } + } + } + + if (taskId == null) { + if (accountIssue) { + log.error("账户问题,请检查账户状态或充值后再试"); + } else { + log.error("生成视频失败,已达到最大重试次数: {}", maxGenerateRetries); + } + return; + } + + // 查询任务状态 + int queryRetryCount = 0; + int maxQueryRetries = 60; // 最大查询次数 + int queryRetryInterval = 5000; // 查询间隔(毫秒) + + while (queryRetryCount < maxQueryRetries) { + try { + JSONObject result = queryTaskStatus(taskId); + JSONObject data = result.getJSONObject("data"); + String taskStatus = data.getStr("task_status"); + + if ("failed".equals(taskStatus)) { + String taskStatusMsg = data.getStr("task_status_msg"); + log.error("任务失败: {}", taskStatusMsg); + break; + } else if ("succeed".equals(taskStatus)) { + // 获取视频URL + JSONObject taskResult = data.getJSONObject("task_result"); + JSONArray videos = taskResult.getJSONArray("videos"); + + for (int i = 0; i < videos.size(); i++) { + JSONObject video = videos.getJSONObject(i); + String videoId = video.getStr("id"); + String videoUrl = video.getStr("url"); + String duration = video.getStr("duration"); + + log.info("视频ID: {}, 时长: {}秒", videoId, duration); + + // 下载视频 + String saveVideoPath = basePath + "video_" + videoId + ".mp4"; + log.info("开始下载视频..."); + downloadFile(videoUrl, saveVideoPath); + log.info("视频已下载到: {}", saveVideoPath); + } + break; + } else { + log.info("任务状态: {}, 等待{}毫秒后重试...", taskStatus, queryRetryInterval); + Thread.sleep(queryRetryInterval); + queryRetryCount++; + } + } catch (Exception e) { + log.error("查询任务状态异常: {}", e.getMessage(), e); + queryRetryCount++; + if (queryRetryCount < maxQueryRetries) { + log.warn("等待{}毫秒后重试...", queryRetryInterval); + Thread.sleep(queryRetryInterval); + } else { + throw e; // 达到最大重试次数,抛出异常 + } + } + } + + if (queryRetryCount >= maxQueryRetries) { + log.error("任务查询超时,已达到最大查询次数: {}", maxQueryRetries); + } + } +}