|
|
|
@ -1,85 +1,318 @@
|
|
|
|
|
package com.dsideal.aiSupport.Util.KeLing;
|
|
|
|
|
|
|
|
|
|
public class KlImage2Video {
|
|
|
|
|
/*
|
|
|
|
|
创建任务
|
|
|
|
|
网络协议 请求地址 请求方法 请求格式 响应格式
|
|
|
|
|
https /v1/videos/image2video POST application/json application/json
|
|
|
|
|
请求头
|
|
|
|
|
字段 值 描述
|
|
|
|
|
Content-Type application/json 数据交换格式
|
|
|
|
|
Authorization 鉴权信息,参考接口鉴权 鉴权信息,参考接口鉴权
|
|
|
|
|
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;
|
|
|
|
|
|
|
|
|
|
请求体:
|
|
|
|
|
字段 类型 必填 默认值 描述
|
|
|
|
|
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参数三选一,不能同时使用
|
|
|
|
|
import java.util.HashMap;
|
|
|
|
|
import java.util.Map;
|
|
|
|
|
import java.util.UUID;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 可灵AI图生视频工具类
|
|
|
|
|
*/
|
|
|
|
|
public class KlImage2Video extends KlCommon {
|
|
|
|
|
private static final Logger log = LoggerFactory.getLogger(KlImage2Video.class);
|
|
|
|
|
private static final String BASE_URL = "https://api.klingai.com";
|
|
|
|
|
private static final String GENERATION_PATH = "/v1/videos/image2video";
|
|
|
|
|
private static final String QUERY_PATH = "/v1/videos/image2video/";
|
|
|
|
|
|
|
|
|
|
响应体:
|
|
|
|
|
{
|
|
|
|
|
"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
|
|
|
|
|
/**
|
|
|
|
|
* 生成视频(使用图片URL)
|
|
|
|
|
*
|
|
|
|
|
* @param imageUrl 图片URL
|
|
|
|
|
* @param modelName 模型名称,枚举值:kling-v1, kling-v1-5, kling-v1-6
|
|
|
|
|
* @return 任务ID
|
|
|
|
|
* @throws Exception 异常信息
|
|
|
|
|
*/
|
|
|
|
|
public static String generateVideo(String imageUrl, String modelName) throws Exception {
|
|
|
|
|
return generateVideo(imageUrl, modelName, null);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
请求头
|
|
|
|
|
字段 值 描述
|
|
|
|
|
Content-Type application/json 数据交换格式
|
|
|
|
|
Authorization 鉴权信息,参考接口鉴权 鉴权信息,参考接口鉴权
|
|
|
|
|
请求路径参数
|
|
|
|
|
字段 类型 必填 默认值 描述
|
|
|
|
|
task_id string 可选 无 图生视频的任务ID
|
|
|
|
|
请求路径参数,直接将值填写在请求路径中,与external_task_id两种查询方式二选一
|
|
|
|
|
external_task_id string 可选 无 图生视频的自定义任务ID
|
|
|
|
|
创建任务时填写的external_task_id,与task_id两种查询方式二选一
|
|
|
|
|
/**
|
|
|
|
|
* 生成视频(使用图片URL)
|
|
|
|
|
*
|
|
|
|
|
* @param imageUrl 图片URL
|
|
|
|
|
* @param modelName 模型名称,枚举值:kling-v1, kling-v1-5, kling-v1-6
|
|
|
|
|
* @param externalTaskId 外部任务ID,可为空
|
|
|
|
|
* @return 任务ID
|
|
|
|
|
* @throws Exception 异常信息
|
|
|
|
|
*/
|
|
|
|
|
public static String generateVideo(String imageUrl, String modelName, String externalTaskId) throws Exception {
|
|
|
|
|
// 获取JWT令牌
|
|
|
|
|
String jwt = KeLingJwtUtil.getJwt();
|
|
|
|
|
|
|
|
|
|
// 创建请求体
|
|
|
|
|
Map<String, Object> requestBody = new HashMap<>();
|
|
|
|
|
requestBody.put("model_name", modelName);
|
|
|
|
|
requestBody.put("image", imageUrl);
|
|
|
|
|
|
|
|
|
|
// 如果提供了外部任务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();
|
|
|
|
|
|
|
|
|
|
响应体:
|
|
|
|
|
{
|
|
|
|
|
"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
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
]
|
|
|
|
|
}
|
|
|
|
|
"created_at": 1722769557708, //任务创建时间,Unix时间戳、单位ms
|
|
|
|
|
"updated_at": 1722769557708, //任务更新时间,Unix时间戳、单位ms
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
保存视频
|
|
|
|
|
|
|
|
|
|
* */
|
|
|
|
|
public static void main(String[] args) throws Exception {
|
|
|
|
|
// 获取项目根目录路径
|
|
|
|
|
String projectRoot = System.getProperty("user.dir");
|
|
|
|
|
// 拼接相对路径
|
|
|
|
|
String basePath = projectRoot + "/src/main/java/com/dsideal/aiSupport/Util/KeLing/Example/";
|
|
|
|
|
|
|
|
|
|
// 确保目录存在
|
|
|
|
|
FileUtil.mkdir(basePath);
|
|
|
|
|
log.info("保存目录: {}", basePath);
|
|
|
|
|
|
|
|
|
|
// 图片URL和模型名称
|
|
|
|
|
String imageUrl = "https://example.com/image.jpg"; // 替换为实际可访问的图片URL
|
|
|
|
|
String modelName = "kling-v1"; // 可选:kling-v1, kling-v1-5, kling-v1-6
|
|
|
|
|
|
|
|
|
|
// 生成外部任务ID(可选)
|
|
|
|
|
String externalTaskId = "image2video_" + 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(imageUrl, 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 + "image2video_" + 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);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|