头像视频合成

This commit is contained in:
spllzh
2025-10-08 08:02:21 +08:00
parent 76dedb657a
commit 45d061bad7
28 changed files with 689 additions and 13 deletions

View File

@@ -135,6 +135,9 @@ public class PasswordUtil {

View File

@@ -119,6 +119,9 @@ public class ServiceManager {

View File

@@ -97,6 +97,9 @@ public class AliyunConfig {

View File

@@ -26,7 +26,7 @@ import java.util.Map;
@Slf4j
@RestController
@RequestMapping("/api/tts/log")
@Tag(name = "TTS请求日志", description = "TTS请求日志管理接口")
@Tag(name = "TTS请求日志", description = "音频合成,TTS请求日志管理接口")
public class TtsRequestLogController {
@Autowired

View File

@@ -1,7 +1,10 @@
package com.rj.controller;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.rj.dto.VideoSynthesisRequestDto;
import com.rj.entity.VideoSynthesisLog;
import com.rj.mapper.VideoSynthesisLogMapper;
import com.rj.service.IVideoSynthesisService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
@@ -29,6 +32,9 @@ public class VideoSynthesisFromController {
@Autowired
private IVideoSynthesisService videoSynthesisService;
@Autowired
private VideoSynthesisLogMapper videoSynthesisLogMapper;
/**
* 视频合成接口
@@ -47,7 +53,7 @@ public class VideoSynthesisFromController {
// 构建响应结果
result.put("success", synthesisLog.getSuccess());
result.put("message", synthesisLog.getSuccess() ? "视频合成成功" : "视频合成失败");
result.put("message", synthesisLog.getTaskStatus());
result.put("requestId", synthesisLog.getRequestId());
result.put("taskId", synthesisLog.getTaskId());
result.put("taskStatus", synthesisLog.getTaskStatus());
@@ -177,4 +183,77 @@ public class VideoSynthesisFromController {
return ResponseEntity.internalServerError().body(result);
}
}
/**
* 分页查询视频合成日志列表
*/
@GetMapping("/list")
@Operation(summary = "查询视频合成日志列表", description = "分页查询视频合成日志")
public ResponseEntity<Map<String, Object>> getVideoSynthesisLogs(
@Parameter(description = "页码") @RequestParam(defaultValue = "1") Integer pageNum,
@Parameter(description = "每页大小") @RequestParam(defaultValue = "10") Integer pageSize,
@Parameter(description = "任务状态") @RequestParam(required = false) String taskStatus,
@Parameter(description = "模型") @RequestParam(required = false) String model,
@Parameter(description = "模型供应商") @RequestParam(required = false) String modelProvider,
@Parameter(description = "归属人姓名") @RequestParam(required = false) String ownerName,
@Parameter(description = "归属人电话") @RequestParam(required = false) String ownerPhone,
@Parameter(description = "是否成功") @RequestParam(required = false) Boolean success,
@Parameter(description = "开始时间") @RequestParam(required = false) String startTime,
@Parameter(description = "结束时间") @RequestParam(required = false) String endTime) {
Map<String, Object> result = new HashMap<>();
try {
Page<VideoSynthesisLog> page = new Page<>(pageNum, pageSize);
QueryWrapper<VideoSynthesisLog> queryWrapper = new QueryWrapper<>();
// 添加查询条件
if (taskStatus != null && !taskStatus.trim().isEmpty()) {
queryWrapper.eq("task_status", taskStatus);
}
if (model != null && !model.trim().isEmpty()) {
queryWrapper.like("model", model);
}
if (modelProvider != null && !modelProvider.trim().isEmpty()) {
queryWrapper.like("model_provider", modelProvider);
}
if (ownerName != null && !ownerName.trim().isEmpty()) {
queryWrapper.like("owner_name", ownerName);
}
if (ownerPhone != null && !ownerPhone.trim().isEmpty()) {
queryWrapper.like("owner_phone", ownerPhone);
}
if (success != null) {
queryWrapper.eq("success", success);
}
if (startTime != null && !startTime.trim().isEmpty()) {
queryWrapper.ge("request_time", startTime);
}
if (endTime != null && !endTime.trim().isEmpty()) {
queryWrapper.le("request_time", endTime);
}
// 按请求时间倒序排列
queryWrapper.orderByDesc("request_time");
// 执行查询
Page<VideoSynthesisLog> pageResult = videoSynthesisLogMapper.selectPage(page, queryWrapper);
result.put("success", true);
result.put("message", "查询成功");
result.put("data", pageResult.getRecords());
result.put("total", pageResult.getTotal());
result.put("pageNum", pageResult.getCurrent());
result.put("pageSize", pageResult.getSize());
result.put("pages", pageResult.getPages());
return ResponseEntity.ok(result);
} catch (Exception e) {
log.error("查询视频合成日志失败: {}", e.getMessage(), e);
result.put("success", false);
result.put("message", "查询失败: " + e.getMessage());
return ResponseEntity.status(500).body(result);
}
}
}

View File

@@ -357,6 +357,9 @@ public class MenuController {

View File

@@ -327,6 +327,9 @@ public class RoleController {

View File

@@ -333,6 +333,9 @@ public class UserRoleController {

View File

@@ -100,6 +100,9 @@ public class DifyWorkflowResponseDto {

View File

@@ -5,6 +5,8 @@ import lombok.Data;
import jakarta.validation.constraints.NotBlank;
import java.math.BigDecimal;
import java.util.HashMap;
import java.util.Map;
/**
* 视频合成请求DTO
@@ -17,11 +19,11 @@ import java.math.BigDecimal;
public class VideoSynthesisRequestDto {
@NotBlank(message = "图片URL不能为空")
@Schema(description = "图片URL", required = true, example = "https://example.com/image.jpg")
@Schema(description = "图片URL", required = true)
private String imageUrl;
@NotBlank(message = "音频URL不能为空")
@Schema(description = "音频URL", required = true, example = "https://example.com/audio.wav")
@Schema(description = "音频URL", required = true)
private String audioUrl;
@Schema(description = "使用的模型", example = "liveportrait")
@@ -30,7 +32,7 @@ public class VideoSynthesisRequestDto {
@Schema(description = "图片ID", example = "img-12345")
private String imageId;
@Schema(description = "音频ID", example = "audio-67890")
@Schema(description = "音频ID", example ="audio-67890")
private String audioId;
@Schema(description = "归属人姓名", example = "张三")
@@ -59,4 +61,47 @@ public class VideoSynthesisRequestDto {
@Schema(description = "头部动作强度", example = "0.7")
private BigDecimal headMoveStrength = new BigDecimal("0.7");
@Schema(description = "输入数据", example = "{\"image_url\": \"http://xxx/1.jpg\", \"audio_url\": \"http://xxx/1.wav\"}")
private Map<String, String> input;
@Schema(description = "参数配置", example = "{\"template_id\": \"normal\", \"eye_move_freq\": 0.5, \"video_fps\": 30, \"mouth_move_strength\": 1, \"paste_back\": true, \"head_move_strength\": 0.7}")
private Map<String, Object> parameters;
/**
* 构建参数方法
* 用于构建 input 和 parameters 属性
*/
public void buildParam() {
// 构建 input 属性
this.input = new HashMap<>();
if (this.imageUrl != null) {
this.input.put("image_url", this.imageUrl);
}
if (this.audioUrl != null) {
this.input.put("audio_url", this.audioUrl);
}
// 构建 parameters 属性
this.parameters = new HashMap<>();
if (this.templateId != null) {
this.parameters.put("template_id", this.templateId);
}
if (this.eyeMoveFreq != null) {
this.parameters.put("eye_move_freq", this.eyeMoveFreq);
}
if (this.videoFps != null) {
this.parameters.put("video_fps", this.videoFps);
}
if (this.mouthMoveStrength != null) {
this.parameters.put("mouth_move_strength", this.mouthMoveStrength);
}
if (this.pasteBack != null) {
this.parameters.put("paste_back", this.pasteBack);
}
if (this.headMoveStrength != null) {
this.parameters.put("head_move_strength", this.headMoveStrength);
}
}
}

View File

@@ -43,6 +43,9 @@ public interface CustomerProfileAnalysisMapper extends BaseMapper<CustomerProfil

View File

@@ -103,6 +103,9 @@ public class AudioStatisticsScheduler {

View File

@@ -0,0 +1,358 @@
package com.rj.scheduler;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.rj.config.AliyunConfig;
import com.rj.entity.VideoSynthesisLog;
import com.rj.mapper.VideoSynthesisLogMapper;
import com.rj.service.MinIOService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.time.LocalDateTime;
import java.util.List;
/**
* 视频合成任务状态检查定时器
* 每5分钟检查一次PENDING状态的任务调用阿里云API查询任务状态并更新数据库
*
* @author rj
* @date 2025-01-30
*/
@Slf4j
@Component
public class VideoSynthesisStatusScheduler {
@Autowired
private VideoSynthesisLogMapper videoSynthesisLogMapper;
@Autowired
private AliyunConfig aliyunConfig;
@Autowired
private RestTemplate restTemplate;
@Autowired
private MinIOService minIOService;
/**
* 每5分钟执行一次任务状态检查
*/
@Scheduled(fixedRate = 5 * 60 * 1000) // 5分钟 = 5 * 60 * 1000毫秒
public void checkVideoSynthesisStatus() {
try {
log.info("开始执行视频合成任务状态检查...");
// 查询所有PENDING状态的任务
List<VideoSynthesisLog> pendingTasks = getPendingTasks();
if (pendingTasks.isEmpty()) {
log.info("没有找到PENDING状态的任务");
return;
}
log.info("找到{}个PENDING状态的任务开始检查状态", pendingTasks.size());
// 遍历每个任务,检查状态
for (VideoSynthesisLog task : pendingTasks) {
try {
checkAndUpdateTaskStatus(task);
} catch (Exception e) {
log.error("检查任务状态失败任务ID: {}, 错误: {}", task.getTaskId(), e.getMessage(), e);
}
}
log.info("视频合成任务状态检查完成");
} catch (Exception e) {
log.error("视频合成任务状态检查执行失败", e);
}
}
/**
* 查询所有PENDING状态的任务
*/
private List<VideoSynthesisLog> getPendingTasks() {
QueryWrapper<VideoSynthesisLog> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("task_status", "PENDING");
return videoSynthesisLogMapper.selectList(queryWrapper);
}
/**
* 检查并更新单个任务状态
*/
private void checkAndUpdateTaskStatus(VideoSynthesisLog task) {
try {
log.info("检查任务状态任务ID: {}", task.getTaskId());
if (task == null || task.getTaskId() == null || task.getTaskId().isEmpty()) return;
// 调用阿里云API检查任务状态
String apiUrl = "https://dashscope.aliyuncs.com/api/v1/tasks/" + task.getTaskId();
HttpHeaders headers = new HttpHeaders();
headers.set("Authorization", "Bearer " + aliyunConfig.getApiKey());
headers.set("Content-Type", "application/json");
HttpEntity<String> entity = new HttpEntity<>(headers);
ResponseEntity<String> response = restTemplate.exchange(
apiUrl,
HttpMethod.GET,
entity,
String.class
);
if (response.getStatusCode().is2xxSuccessful()) {
String responseBody = response.getBody();
log.info("API响应: {}", responseBody);
// 解析响应并更新数据库
updateTaskFromResponse(task, responseBody);
} else {
log.error("API调用失败状态码: {}, 任务ID: {}", response.getStatusCode(), task.getTaskId());
}
} catch (Exception e) {
log.error("检查任务状态异常任务ID: {}, 错误: {}", task.getTaskId(), e.getMessage(), e);
}
}
/**
* 根据API响应更新任务状态
*/
private void updateTaskFromResponse(VideoSynthesisLog task, String responseBody) {
try {
JSONObject responseJson = JSON.parseObject(responseBody);
if (responseJson.containsKey("output")) {
JSONObject output = responseJson.getJSONObject("output");
// 更新任务状态
String taskStatus = output.getString("task_status");
task.setTaskStatus(taskStatus);
task.setUpdatedAt(LocalDateTime.now());
// 如果任务成功完成,处理视频文件
if ("SUCCEEDED".equals(taskStatus)) {
JSONObject results = output.getJSONObject("results");
if (results != null && results.containsKey("video_url")) {
String originalVideoUrl = results.getString("video_url");
log.info("任务成功完成原始视频URL: {}", originalVideoUrl);
// 从阿里云OSS下载视频并上传到MinIO
String minioVideoUrl = downloadAndUploadToMinIO(originalVideoUrl, task.getTaskId());
if (minioVideoUrl != null) {
task.setVideoUrl(minioVideoUrl);
log.info("视频已成功转存到MinIO: {}", minioVideoUrl);
} else {
// 如果转存失败保留原始URL
task.setVideoUrl(originalVideoUrl);
log.warn("视频转存到MinIO失败保留原始URL: {}", originalVideoUrl);
}
task.setSuccess(true);
task.setResponseTime(LocalDateTime.now());
}
} else if ("FAILED".equals(taskStatus)) {
task.setSuccess(false);
task.setErrorMessage("任务执行失败");
task.setResponseTime(LocalDateTime.now());
}
// 更新usage信息
if (responseJson.containsKey("usage")) {
JSONObject usage = responseJson.getJSONObject("usage");
if (usage.containsKey("video_duration")) {
task.setVideoDuration(usage.getBigDecimal("video_duration"));
}
if (usage.containsKey("video_ratio")) {
task.setVideoRatio(usage.getString("video_ratio"));
}
}
// 保存到数据库
int updateResult = videoSynthesisLogMapper.updateById(task);
if (updateResult > 0) {
log.info("任务状态更新成功任务ID: {}, 新状态: {}", task.getTaskId(), taskStatus);
} else {
log.error("任务状态更新失败任务ID: {}", task.getTaskId());
}
} else {
log.error("API响应格式异常任务ID: {}, 响应: {}", task.getTaskId(), responseBody);
}
} catch (Exception e) {
log.error("解析API响应失败任务ID: {}, 响应: {}, 错误: {}", task.getTaskId(), responseBody, e.getMessage(), e);
}
}
/**
* 从阿里云OSS下载视频并上传到MinIO
*
* @param ossVideoUrl 阿里云OSS视频URL
* @param taskId 任务ID用于生成文件名
* @return MinIO中的视频URL失败时返回null
*/
private String downloadAndUploadToMinIO(String ossVideoUrl, String taskId) {
try {
log.info("开始从阿里云OSS下载视频: {}", ossVideoUrl);
// 从阿里云OSS下载视频
byte[] videoData = downloadVideoFromOSS(ossVideoUrl);
if (videoData == null || videoData.length == 0) {
log.error("从阿里云OSS下载视频失败数据为空");
return null;
}
log.info("视频下载成功,大小: {} bytes", videoData.length);
// 生成MinIO文件名
String fileName = "video_synthesis/" + taskId + ".mp4";
// 上传到MinIO
String minioUrl = minIOService.uploadFile(
new ByteArrayInputStream(videoData),
fileName,
"video/mp4"
);
log.info("视频已成功上传到MinIO: {}", minioUrl);
return minioUrl;
} catch (Exception e) {
log.error("下载并上传视频到MinIO失败: {}", e.getMessage(), e);
return null;
}
}
/**
* 从阿里云OSS下载视频文件
*
* @param videoUrl 视频URL
* @return 视频字节数组
*/
private byte[] downloadVideoFromOSS(String videoUrl) {
try {
log.info("开始下载视频: {}", videoUrl);
// 方法1: 使用HttpURLConnection下载视频避免RestTemplate的URL编码问题
byte[] videoData = downloadWithHttpURLConnection(videoUrl);
if (videoData != null) {
return videoData;
}
// 方法2: 如果HttpURLConnection失败尝试使用RestTemplate但禁用URL编码
log.warn("HttpURLConnection下载失败尝试使用RestTemplate备用方案");
return downloadWithRestTemplate(videoUrl);
} catch (Exception e) {
log.error("下载视频异常: {}", e.getMessage(), e);
return null;
}
}
/**
* 使用HttpURLConnection下载视频
*/
private byte[] downloadWithHttpURLConnection(String videoUrl) {
try {
URL url = new URL(videoUrl);
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
// 设置请求头
connection.setRequestMethod("GET");
connection.setConnectTimeout(30000); // 30秒连接超时
connection.setReadTimeout(300000); // 5分钟读取超时
connection.setRequestProperty("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36");
connection.setRequestProperty("Accept", "*/*");
connection.setRequestProperty("Accept-Encoding", "identity"); // 禁用压缩
int responseCode = connection.getResponseCode();
if (responseCode == 200) {
try (InputStream inputStream = connection.getInputStream();
ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
byte[] buffer = new byte[8192];
int bytesRead;
while ((bytesRead = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, bytesRead);
}
byte[] videoData = outputStream.toByteArray();
log.info("HttpURLConnection视频下载成功大小: {} bytes", videoData.length);
return videoData;
}
} else {
log.error("HttpURLConnection下载失败HTTP状态码: {}", responseCode);
// 读取错误信息
try (InputStream errorStream = connection.getErrorStream()) {
if (errorStream != null) {
byte[] errorBytes = new byte[1024];
int errorBytesRead = errorStream.read(errorBytes);
if (errorBytesRead > 0) {
String errorMessage = new String(errorBytes, 0, errorBytesRead);
log.error("错误响应内容: {}", errorMessage);
}
}
}
return null;
}
} catch (Exception e) {
log.error("HttpURLConnection下载异常: {}", e.getMessage(), e);
return null;
}
}
/**
* 使用RestTemplate下载视频备用方案
*/
private byte[] downloadWithRestTemplate(String videoUrl) {
try {
// 创建自定义的RestTemplate禁用URL编码
RestTemplate customRestTemplate = new RestTemplate();
// 设置请求头
HttpHeaders headers = new HttpHeaders();
headers.set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36");
headers.set("Accept", "*/*");
headers.set("Accept-Encoding", "identity");
HttpEntity<String> entity = new HttpEntity<>(headers);
ResponseEntity<byte[]> response = customRestTemplate.exchange(
videoUrl,
HttpMethod.GET,
entity,
byte[].class
);
if (response.getStatusCode().is2xxSuccessful() && response.getBody() != null) {
log.info("RestTemplate视频下载成功大小: {} bytes", response.getBody().length);
return response.getBody();
} else {
log.error("RestTemplate下载失败状态码: {}", response.getStatusCode());
return null;
}
} catch (Exception e) {
log.error("RestTemplate下载异常: {}", e.getMessage(), e);
return null;
}
}
}

View File

@@ -64,6 +64,9 @@ public interface ICustomerProfileAnalysisService extends IService<CustomerProfil

View File

@@ -99,6 +99,9 @@ public class CustomerProfileAnalysisServiceImpl extends ServiceImpl<CustomerProf

View File

@@ -1,5 +1,7 @@
package com.rj.service.impl;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.rj.dto.VideoSynthesisRequestDto;
import com.rj.entity.VideoSynthesisLog;
import com.rj.mapper.VideoSynthesisLogMapper;
@@ -59,7 +61,7 @@ public class VideoSynthesisServiceImpl implements IVideoSynthesisService {
headers.setContentType(MediaType.APPLICATION_JSON);
headers.set("X-DashScope-Async", "enable");
headers.set("Authorization", "Bearer " + apiKey);
request.buildParam();
// 构建请求体
HttpEntity<VideoSynthesisRequestDto> entity = new HttpEntity<>(request, headers);
@@ -76,14 +78,33 @@ public class VideoSynthesisServiceImpl implements IVideoSynthesisService {
String responseBody = response.getBody();
log.info("API响应: {}", responseBody);
// 简单解析关键字段
if (responseBody != null && responseBody.contains("task_id")) {
// 这里可以添加更复杂的JSON解析逻辑
// 暂时设置基本状态
synthesisLog.setTaskStatus("PENDING");
synthesisLog.setSuccess(true);
// 解析JSON响应
if (responseBody != null && !responseBody.trim().isEmpty()) {
try {
// 使用fastjson解析JSON响应
JSONObject responseJson = JSON.parseObject(responseBody);
// 提取task_id
if (responseJson.containsKey("output")) {
JSONObject output = responseJson.getJSONObject("output");
if (output != null && output.containsKey("task_id")) {
String taskId = output.getString("task_id");
synthesisLog.setTaskId(taskId);
log.info("提取到任务ID: {}", taskId);
}
}
// 设置基本状态
synthesisLog.setTaskStatus("PENDING");
synthesisLog.setSuccess(true);
} catch (Exception e) {
log.error("解析API响应JSON失败: {}", e.getMessage(), e);
synthesisLog.setErrorMessage("解析API响应失败: " + e.getMessage());
synthesisLog.setSuccess(false);
}
} else {
synthesisLog.setErrorMessage("API响应格式异常");
synthesisLog.setErrorMessage("API响应为空");
synthesisLog.setSuccess(false);
}

View File

@@ -66,6 +66,9 @@ public interface IMenuService extends IService<Menu> {

View File

@@ -66,6 +66,9 @@ public interface IUserRoleService extends IService<UserRole> {

View File

@@ -70,6 +70,9 @@ public class MenuServiceImpl extends ServiceImpl<MenuMapper, Menu> implements IM

View File

@@ -70,6 +70,9 @@ public class RoleServiceImpl extends ServiceImpl<RoleMapper, Role> implements IR

View File

@@ -70,6 +70,9 @@ public class UserRoleServiceImpl extends ServiceImpl<UserRoleMapper, UserRole> i