diff --git a/dsAiSupport/src/main/java/com/dsideal/aiSupport/Util/DashScope/Example/emo_1747125227234_e7e2bd15-6465-4254-86ba-1eaaa6a4353e.mp4 b/dsAiSupport/src/main/java/com/dsideal/aiSupport/Util/DashScope/Example/emo_1747125227234_e7e2bd15-6465-4254-86ba-1eaaa6a4353e.mp4 new file mode 100644 index 00000000..f5f217e0 Binary files /dev/null and b/dsAiSupport/src/main/java/com/dsideal/aiSupport/Util/DashScope/Example/emo_1747125227234_e7e2bd15-6465-4254-86ba-1eaaa6a4353e.mp4 differ diff --git a/dsAiSupport/src/main/java/com/dsideal/aiSupport/Util/DashScope/ImgSpeak.java b/dsAiSupport/src/main/java/com/dsideal/aiSupport/Util/DashScope/ImgSpeak.java new file mode 100644 index 00000000..898ebf7b --- /dev/null +++ b/dsAiSupport/src/main/java/com/dsideal/aiSupport/Util/DashScope/ImgSpeak.java @@ -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); + } + } +} \ No newline at end of file