|
|
|
@ -0,0 +1,356 @@
|
|
|
|
|
package com.dsideal.aiSupport.Util.DashScope;
|
|
|
|
|
|
|
|
|
|
import com.alibaba.fastjson.JSON;
|
|
|
|
|
import com.alibaba.fastjson.JSONObject;
|
|
|
|
|
import com.dsideal.aiSupport.Plugin.YamlProp;
|
|
|
|
|
import com.jfinal.kit.Prop;
|
|
|
|
|
import lombok.SneakyThrows;
|
|
|
|
|
import okhttp3.*;
|
|
|
|
|
import org.slf4j.Logger;
|
|
|
|
|
import org.slf4j.LoggerFactory;
|
|
|
|
|
|
|
|
|
|
import java.util.concurrent.TimeUnit;
|
|
|
|
|
|
|
|
|
|
import static com.dsideal.aiSupport.AiSupportApplication.getEnvPrefix;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 阿里云达摩院灵动人像LivePortrait视频合成API工具类
|
|
|
|
|
*/
|
|
|
|
|
public class ImgSpeak {
|
|
|
|
|
private static final Logger log = LoggerFactory.getLogger(ImgSpeak.class);
|
|
|
|
|
private static final String API_URL = "https://dashscope.aliyuncs.com/api/v1/services/aigc/image2video/video-synthesis/";
|
|
|
|
|
private static final String FACE_DETECT_URL = "https://dashscope.aliyuncs.com/api/v1/services/aigc/image2video/face-detect";
|
|
|
|
|
private static final String TASK_URL = "https://dashscope.aliyuncs.com/api/v1/tasks/";
|
|
|
|
|
private static final String API_KEY;
|
|
|
|
|
// 获取项目根目录路径
|
|
|
|
|
protected static String projectRoot = System.getProperty("user.dir").replace("\\", "/") + "/dsAiSupport";
|
|
|
|
|
// 拼接相对路径
|
|
|
|
|
protected static String basePath = projectRoot + "/src/main/java/com/dsideal/aiSupport/Util/DashScope/Example/";
|
|
|
|
|
|
|
|
|
|
public static Prop PropKit; // 配置文件工具
|
|
|
|
|
|
|
|
|
|
static {
|
|
|
|
|
//加载配置文件
|
|
|
|
|
String configFile = "application_{?}.yaml".replace("{?}", getEnvPrefix());
|
|
|
|
|
PropKit = new YamlProp(configFile);
|
|
|
|
|
API_KEY = PropKit.get("aliyun.API_KEY");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 调用人脸检测API
|
|
|
|
|
*
|
|
|
|
|
* @param imageUrl 图片URL
|
|
|
|
|
* @return 检测结果JSON对象
|
|
|
|
|
* @throws Exception 异常信息
|
|
|
|
|
*/
|
|
|
|
|
@SneakyThrows
|
|
|
|
|
public static JSONObject detectFace(String imageUrl) {
|
|
|
|
|
// 创建OkHttpClient,设置超时时间
|
|
|
|
|
OkHttpClient client = new OkHttpClient().newBuilder()
|
|
|
|
|
.connectTimeout(30, TimeUnit.SECONDS)
|
|
|
|
|
.readTimeout(30, TimeUnit.SECONDS)
|
|
|
|
|
.writeTimeout(30, TimeUnit.SECONDS)
|
|
|
|
|
.build();
|
|
|
|
|
|
|
|
|
|
// 构建请求体
|
|
|
|
|
JSONObject requestBody = new JSONObject();
|
|
|
|
|
requestBody.put("model", "liveportrait-detect");
|
|
|
|
|
|
|
|
|
|
// 设置输入参数
|
|
|
|
|
JSONObject input = new JSONObject();
|
|
|
|
|
input.put("image_url", imageUrl);
|
|
|
|
|
requestBody.put("input", input);
|
|
|
|
|
|
|
|
|
|
// 创建请求
|
|
|
|
|
MediaType mediaType = MediaType.parse("application/json");
|
|
|
|
|
RequestBody body = RequestBody.create(mediaType, requestBody.toJSONString());
|
|
|
|
|
Request request = new Request.Builder()
|
|
|
|
|
.url(FACE_DETECT_URL)
|
|
|
|
|
.method("POST", body)
|
|
|
|
|
.addHeader("Content-Type", "application/json")
|
|
|
|
|
.addHeader("Authorization", "Bearer " + API_KEY)
|
|
|
|
|
.build();
|
|
|
|
|
|
|
|
|
|
// 发送请求并获取响应
|
|
|
|
|
log.info("发送人脸检测请求: {}", requestBody.toJSONString());
|
|
|
|
|
Response response = client.newCall(request).execute();
|
|
|
|
|
|
|
|
|
|
// 检查响应状态
|
|
|
|
|
if (!response.isSuccessful()) {
|
|
|
|
|
String errorMsg = "人脸检测API请求失败,状态码: " + response.code();
|
|
|
|
|
log.error(errorMsg);
|
|
|
|
|
throw new Exception(errorMsg);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 解析响应
|
|
|
|
|
String responseBody = response.body().string();
|
|
|
|
|
log.info("人脸检测响应: {}", responseBody);
|
|
|
|
|
|
|
|
|
|
JSONObject responseJson = JSON.parseObject(responseBody);
|
|
|
|
|
|
|
|
|
|
// 检查是否通过人脸检测
|
|
|
|
|
JSONObject output = responseJson.getJSONObject("output");
|
|
|
|
|
if (output != null && !output.getBooleanValue("pass")) {
|
|
|
|
|
String message = output.getString("message");
|
|
|
|
|
String errorMsg = "人脸检测未通过: " + message;
|
|
|
|
|
log.error(errorMsg);
|
|
|
|
|
throw new Exception(errorMsg);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return responseJson;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 调用灵动人像LivePortrait唱歌视频合成API
|
|
|
|
|
*
|
|
|
|
|
* @param imageUrl 图片URL
|
|
|
|
|
* @param audioUrl 音频URL
|
|
|
|
|
* @param templateId 模板ID,可选值:normal, dance, rap等
|
|
|
|
|
* @param eyeMoveFreq 眼睛移动频率,范围0-1
|
|
|
|
|
* @param videoFps 视频帧率,默认30
|
|
|
|
|
* @param mouthMoveStrength 嘴部动作强度,范围0-1
|
|
|
|
|
* @param pasteBack 是否贴回原图,true或false
|
|
|
|
|
* @param headMoveStrength 头部动作强度,范围0-1
|
|
|
|
|
* @return 任务ID
|
|
|
|
|
* @throws Exception 异常信息
|
|
|
|
|
*/
|
|
|
|
|
@SneakyThrows
|
|
|
|
|
public static String synthesisVideo(String imageUrl, String audioUrl, String templateId,
|
|
|
|
|
double eyeMoveFreq, int videoFps, double mouthMoveStrength,
|
|
|
|
|
boolean pasteBack, double headMoveStrength) {
|
|
|
|
|
// 先进行人脸检测
|
|
|
|
|
detectFace(imageUrl);
|
|
|
|
|
|
|
|
|
|
// 创建OkHttpClient,设置超时时间
|
|
|
|
|
OkHttpClient client = new OkHttpClient().newBuilder()
|
|
|
|
|
.connectTimeout(30, TimeUnit.SECONDS)
|
|
|
|
|
.readTimeout(30, TimeUnit.SECONDS)
|
|
|
|
|
.writeTimeout(30, TimeUnit.SECONDS)
|
|
|
|
|
.build();
|
|
|
|
|
|
|
|
|
|
// 构建请求体
|
|
|
|
|
JSONObject requestBody = new JSONObject();
|
|
|
|
|
requestBody.put("model", "liveportrait");
|
|
|
|
|
|
|
|
|
|
// 设置输入参数
|
|
|
|
|
JSONObject input = new JSONObject();
|
|
|
|
|
input.put("image_url", imageUrl);
|
|
|
|
|
input.put("audio_url", audioUrl);
|
|
|
|
|
requestBody.put("input", input);
|
|
|
|
|
|
|
|
|
|
// 设置其他参数
|
|
|
|
|
JSONObject parameters = new JSONObject();
|
|
|
|
|
parameters.put("template_id", templateId);
|
|
|
|
|
parameters.put("eye_move_freq", eyeMoveFreq);
|
|
|
|
|
parameters.put("video_fps", videoFps);
|
|
|
|
|
parameters.put("mouth_move_strength", mouthMoveStrength);
|
|
|
|
|
parameters.put("paste_back", pasteBack);
|
|
|
|
|
parameters.put("head_move_strength", headMoveStrength);
|
|
|
|
|
requestBody.put("parameters", parameters);
|
|
|
|
|
|
|
|
|
|
// 创建请求
|
|
|
|
|
MediaType mediaType = MediaType.parse("application/json");
|
|
|
|
|
RequestBody body = RequestBody.create(mediaType, requestBody.toJSONString());
|
|
|
|
|
Request request = new Request.Builder()
|
|
|
|
|
.url(API_URL)
|
|
|
|
|
.method("POST", body)
|
|
|
|
|
.addHeader("Content-Type", "application/json")
|
|
|
|
|
.addHeader("Authorization", "Bearer " + API_KEY)
|
|
|
|
|
.addHeader("X-DashScope-Async", "enable")
|
|
|
|
|
.build();
|
|
|
|
|
|
|
|
|
|
// 发送请求并获取响应
|
|
|
|
|
log.info("发送灵动人像LivePortrait唱歌视频合成请求: {}", requestBody.toJSONString());
|
|
|
|
|
Response response = client.newCall(request).execute();
|
|
|
|
|
|
|
|
|
|
// 检查响应状态
|
|
|
|
|
if (!response.isSuccessful()) {
|
|
|
|
|
log.info(response.message());
|
|
|
|
|
String errorMsg = "灵动人像LivePortrait唱歌视频合成API请求失败,状态码: " + response.code();
|
|
|
|
|
log.error(errorMsg);
|
|
|
|
|
throw new Exception(errorMsg);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 解析响应
|
|
|
|
|
String responseBody = response.body().string();
|
|
|
|
|
log.info("灵动人像LivePortrait唱歌视频合成响应: {}", responseBody);
|
|
|
|
|
|
|
|
|
|
JSONObject responseJson = JSON.parseObject(responseBody);
|
|
|
|
|
|
|
|
|
|
// 获取任务ID
|
|
|
|
|
String taskId = responseJson.getJSONObject("output").getString("task_id");
|
|
|
|
|
log.info("灵动人像LivePortrait唱歌视频合成任务ID: {}", taskId);
|
|
|
|
|
|
|
|
|
|
return taskId;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 查询任务状态
|
|
|
|
|
*
|
|
|
|
|
* @param taskId 任务ID
|
|
|
|
|
* @return 任务结果
|
|
|
|
|
* @throws Exception 异常信息
|
|
|
|
|
*/
|
|
|
|
|
@SneakyThrows
|
|
|
|
|
public static JSONObject queryTaskStatus(String taskId) {
|
|
|
|
|
// 创建OkHttpClient
|
|
|
|
|
OkHttpClient client = new OkHttpClient().newBuilder()
|
|
|
|
|
.connectTimeout(30, TimeUnit.SECONDS)
|
|
|
|
|
.readTimeout(30, TimeUnit.SECONDS)
|
|
|
|
|
.build();
|
|
|
|
|
|
|
|
|
|
// 创建请求
|
|
|
|
|
Request request = new Request.Builder()
|
|
|
|
|
.url(TASK_URL + taskId)
|
|
|
|
|
.method("GET", null)
|
|
|
|
|
.addHeader("Authorization", "Bearer " + API_KEY)
|
|
|
|
|
.build();
|
|
|
|
|
|
|
|
|
|
// 发送请求并获取响应
|
|
|
|
|
log.info("查询灵动人像LivePortrait唱歌视频合成任务状态: {}", taskId);
|
|
|
|
|
Response response = client.newCall(request).execute();
|
|
|
|
|
|
|
|
|
|
// 检查响应状态
|
|
|
|
|
if (!response.isSuccessful()) {
|
|
|
|
|
String errorMsg = "灵动人像LivePortrait唱歌视频合成API请求失败,状态码: " + response.code();
|
|
|
|
|
log.error(errorMsg);
|
|
|
|
|
throw new Exception(errorMsg);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 解析响应
|
|
|
|
|
String responseBody = response.body().string();
|
|
|
|
|
log.info("查询灵动人像LivePortrait唱歌视频合成任务状态响应: {}", responseBody);
|
|
|
|
|
|
|
|
|
|
return JSON.parseObject(responseBody);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 下载视频并保存到本地
|
|
|
|
|
*
|
|
|
|
|
* @param videoUrl 视频URL
|
|
|
|
|
* @param savePath 保存路径
|
|
|
|
|
* @throws Exception 异常信息
|
|
|
|
|
*/
|
|
|
|
|
@SneakyThrows
|
|
|
|
|
public static void downloadVideo(String videoUrl, String savePath) {
|
|
|
|
|
// 创建OkHttpClient
|
|
|
|
|
OkHttpClient client = new OkHttpClient().newBuilder()
|
|
|
|
|
.connectTimeout(60, TimeUnit.SECONDS)
|
|
|
|
|
.readTimeout(60, TimeUnit.SECONDS)
|
|
|
|
|
.build();
|
|
|
|
|
|
|
|
|
|
// 创建请求
|
|
|
|
|
Request request = new Request.Builder()
|
|
|
|
|
.url(videoUrl)
|
|
|
|
|
.method("GET", null)
|
|
|
|
|
.build();
|
|
|
|
|
|
|
|
|
|
// 发送请求并获取响应
|
|
|
|
|
log.info("开始下载视频: {}", videoUrl);
|
|
|
|
|
Response response = client.newCall(request).execute();
|
|
|
|
|
|
|
|
|
|
// 检查响应状态
|
|
|
|
|
if (!response.isSuccessful()) {
|
|
|
|
|
String errorMsg = "下载视频失败,状态码: " + response.code();
|
|
|
|
|
log.error(errorMsg);
|
|
|
|
|
throw new Exception(errorMsg);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 确保目录存在
|
|
|
|
|
java.io.File file = new java.io.File(savePath);
|
|
|
|
|
java.io.File parentDir = file.getParentFile();
|
|
|
|
|
if (parentDir != null && !parentDir.exists()) {
|
|
|
|
|
parentDir.mkdirs();
|
|
|
|
|
log.info("创建目录: {}", parentDir.getAbsolutePath());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 保存视频
|
|
|
|
|
try (java.io.InputStream inputStream = response.body().byteStream();
|
|
|
|
|
java.io.FileOutputStream outputStream = new java.io.FileOutputStream(savePath)) {
|
|
|
|
|
byte[] buffer = new byte[4096];
|
|
|
|
|
int bytesRead;
|
|
|
|
|
while ((bytesRead = inputStream.read(buffer)) != -1) {
|
|
|
|
|
outputStream.write(buffer, 0, bytesRead);
|
|
|
|
|
}
|
|
|
|
|
outputStream.flush();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
log.info("视频下载成功,保存路径: {}", savePath);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 使用示例
|
|
|
|
|
*/
|
|
|
|
|
@SneakyThrows
|
|
|
|
|
public static void main(String[] args) {
|
|
|
|
|
// 图片URL
|
|
|
|
|
String imageUrl = "https://dsideal.obs.myhuaweicloud.com/HuangHai/%E5%A4%87%E4%BB%BD/p874897.png";
|
|
|
|
|
// 音频URL(唱歌音频)
|
|
|
|
|
String audioUrl = "https://dsideal.obs.myhuaweicloud.com/HuangHai/%E5%A4%87%E4%BB%BD/p874897.wav";
|
|
|
|
|
|
|
|
|
|
// 模板ID - 使用唱歌模板
|
|
|
|
|
String templateId = "sing"; // 可选值:normal, dance, rap, sing等
|
|
|
|
|
// 眼睛移动频率
|
|
|
|
|
double eyeMoveFreq = 0.5;
|
|
|
|
|
// 视频帧率
|
|
|
|
|
int videoFps = 30;
|
|
|
|
|
// 嘴部动作强度
|
|
|
|
|
double mouthMoveStrength = 1.0;
|
|
|
|
|
// 是否贴回原图
|
|
|
|
|
boolean pasteBack = true;
|
|
|
|
|
// 头部动作强度
|
|
|
|
|
double headMoveStrength = 0.7;
|
|
|
|
|
|
|
|
|
|
// 调用灵动人像LivePortrait唱歌视频合成API
|
|
|
|
|
String taskId = synthesisVideo(imageUrl, audioUrl, templateId, eyeMoveFreq,
|
|
|
|
|
videoFps, mouthMoveStrength, pasteBack, headMoveStrength);
|
|
|
|
|
|
|
|
|
|
// 轮询查询任务状态
|
|
|
|
|
int maxRetries = 100;
|
|
|
|
|
int retryCount = 0;
|
|
|
|
|
int retryInterval = 5000; // 5秒
|
|
|
|
|
String videoUrl = null;
|
|
|
|
|
|
|
|
|
|
while (retryCount < maxRetries) {
|
|
|
|
|
JSONObject result = queryTaskStatus(taskId);
|
|
|
|
|
String status = result.getJSONObject("output").getString("task_status");
|
|
|
|
|
log.info("任务状态: {}", status);
|
|
|
|
|
|
|
|
|
|
if ("SUCCEEDED".equals(status)) {
|
|
|
|
|
// 任务成功,获取视频URL
|
|
|
|
|
videoUrl = result.getJSONObject("output").getJSONObject("results").getString("video_url");
|
|
|
|
|
log.info("生成的视频URL: {}", videoUrl);
|
|
|
|
|
|
|
|
|
|
// 获取视频时长和比例信息
|
|
|
|
|
double videoDuration = result.getJSONObject("usage").getDoubleValue("video_duration");
|
|
|
|
|
String videoRatio = result.getJSONObject("usage").getString("video_ratio");
|
|
|
|
|
log.info("视频时长: {}秒, 视频比例: {}", videoDuration, videoRatio);
|
|
|
|
|
|
|
|
|
|
break;
|
|
|
|
|
} else if ("FAILED".equals(status)) {
|
|
|
|
|
// 任务失败
|
|
|
|
|
String message = result.getJSONObject("output").getString("message");
|
|
|
|
|
log.error("任务失败: {}", message);
|
|
|
|
|
break;
|
|
|
|
|
} else {
|
|
|
|
|
// 任务仍在进行中,等待后重试
|
|
|
|
|
log.info("任务进行中,等待{}毫秒后重试...", retryInterval);
|
|
|
|
|
Thread.sleep(retryInterval);
|
|
|
|
|
retryCount++;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (retryCount >= maxRetries) {
|
|
|
|
|
log.error("查询任务状态超时,已达到最大重试次数: {}", maxRetries);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 如果获取到了视频URL,则下载保存
|
|
|
|
|
if (videoUrl != null && !videoUrl.isEmpty()) {
|
|
|
|
|
String fileName = "liveportrait_sing_" + System.currentTimeMillis() + "_" + taskId + ".mp4";
|
|
|
|
|
// 完整保存路径
|
|
|
|
|
String savePath = basePath + "/" + fileName;
|
|
|
|
|
// 下载视频
|
|
|
|
|
downloadVideo(videoUrl, savePath);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|