|
|
@@ -0,0 +1,351 @@
|
|
|
+package shop.alien.store.util.ai;
|
|
|
+
|
|
|
+import com.alibaba.fastjson2.JSONObject;
|
|
|
+import lombok.RequiredArgsConstructor;
|
|
|
+import lombok.extern.slf4j.Slf4j;
|
|
|
+import org.springframework.beans.factory.annotation.Value;
|
|
|
+import org.springframework.http.HttpEntity;
|
|
|
+import org.springframework.http.HttpHeaders;
|
|
|
+import org.springframework.http.HttpStatus;
|
|
|
+import org.springframework.http.MediaType;
|
|
|
+import org.springframework.http.ResponseEntity;
|
|
|
+import org.springframework.stereotype.Component;
|
|
|
+import org.springframework.util.StringUtils;
|
|
|
+import org.springframework.util.LinkedMultiValueMap;
|
|
|
+import org.springframework.util.MultiValueMap;
|
|
|
+import org.springframework.web.client.RestTemplate;
|
|
|
+
|
|
|
+import java.util.ArrayList;
|
|
|
+import java.util.List;
|
|
|
+
|
|
|
+/**
|
|
|
+ * 视频审核工具类
|
|
|
+ * 调用AI视频审核接口审核视频内容
|
|
|
+ */
|
|
|
+@Slf4j
|
|
|
+@Component
|
|
|
+@RequiredArgsConstructor
|
|
|
+public class AiVideoModerationUtil {
|
|
|
+
|
|
|
+ private final RestTemplate restTemplate;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 视频上传接口地址
|
|
|
+ */
|
|
|
+ @Value("${ai.service.video-submit-url}")
|
|
|
+ private String videoSubmitUrl;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 视频审查接口地址
|
|
|
+ */
|
|
|
+ @Value("${ai.service.video-status-url}")
|
|
|
+ private String videoStatusUrl;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 轮询间隔(毫秒)
|
|
|
+ */
|
|
|
+ @Value("${ai.service.video-poll-interval:2000}")
|
|
|
+ private long pollInterval;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 最大轮询次数
|
|
|
+ */
|
|
|
+ @Value("${ai.service.video-max-poll-count:30}")
|
|
|
+ private int maxPollCount;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 审核结果类
|
|
|
+ */
|
|
|
+ public static class VideoAuditResult {
|
|
|
+ private boolean passed;
|
|
|
+ private String failureReason;
|
|
|
+ private String taskId;
|
|
|
+ private JSONObject rawResult;
|
|
|
+
|
|
|
+ public VideoAuditResult(boolean passed, String failureReason) {
|
|
|
+ this.passed = passed;
|
|
|
+ this.failureReason = failureReason;
|
|
|
+ }
|
|
|
+
|
|
|
+ public VideoAuditResult(boolean passed, String failureReason, String taskId, JSONObject rawResult) {
|
|
|
+ this.passed = passed;
|
|
|
+ this.failureReason = failureReason;
|
|
|
+ this.taskId = taskId;
|
|
|
+ this.rawResult = rawResult;
|
|
|
+ }
|
|
|
+
|
|
|
+ public boolean isPassed() {
|
|
|
+ return passed;
|
|
|
+ }
|
|
|
+
|
|
|
+ public String getFailureReason() {
|
|
|
+ return failureReason;
|
|
|
+ }
|
|
|
+
|
|
|
+ public String getTaskId() {
|
|
|
+ return taskId;
|
|
|
+ }
|
|
|
+
|
|
|
+ public JSONObject getRawResult() {
|
|
|
+ return rawResult;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 审核视频内容
|
|
|
+ *
|
|
|
+ * @param videoUrls 视频URL列表
|
|
|
+ * @return 审核结果
|
|
|
+ */
|
|
|
+ public VideoAuditResult auditVideos(List<String> videoUrls) {
|
|
|
+ if (videoUrls == null || videoUrls.isEmpty()) {
|
|
|
+ log.info("视频URL列表为空,跳过审核");
|
|
|
+ return new VideoAuditResult(true, null);
|
|
|
+ }
|
|
|
+
|
|
|
+ log.info("开始审核视频:videoCount={}", videoUrls.size());
|
|
|
+
|
|
|
+ try {
|
|
|
+ List<VideoAuditResult> results = new ArrayList<>();
|
|
|
+ for (String videoUrl : videoUrls) {
|
|
|
+ if (StringUtils.hasText(videoUrl)) {
|
|
|
+ VideoAuditResult result = auditSingleVideo(videoUrl);
|
|
|
+ results.add(result);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 如果任何一个视频审核失败,整体审核失败
|
|
|
+ for (VideoAuditResult result : results) {
|
|
|
+ if (!result.isPassed()) {
|
|
|
+ log.warn("视频审核失败:{}", result.getFailureReason());
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ log.info("所有视频审核通过");
|
|
|
+ return new VideoAuditResult(true, null);
|
|
|
+
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("审核视频异常", e);
|
|
|
+ return new VideoAuditResult(false, "审核服务异常:" + e.getMessage());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 审核单个视频
|
|
|
+ *
|
|
|
+ * @param videoUrl 视频URL
|
|
|
+ * @return 审核结果
|
|
|
+ */
|
|
|
+ private VideoAuditResult auditSingleVideo(String videoUrl) {
|
|
|
+ try {
|
|
|
+ // 1. 提交视频审核任务
|
|
|
+ String taskId = submitVideo(videoUrl);
|
|
|
+ if (taskId == null) {
|
|
|
+ return new VideoAuditResult(false, "视频提交失败");
|
|
|
+ }
|
|
|
+
|
|
|
+ log.info("视频提交成功,taskId={}, videoUrl={}", taskId, videoUrl);
|
|
|
+
|
|
|
+ // 2. 轮询审核状态
|
|
|
+ return pollVideoStatus(taskId, videoUrl);
|
|
|
+
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("审核视频异常,videoUrl={}", videoUrl, e);
|
|
|
+ return new VideoAuditResult(false, "审核服务异常:" + e.getMessage());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 提交视频审核任务
|
|
|
+ *
|
|
|
+ * @param videoUrl 视频URL
|
|
|
+ * @return 任务ID
|
|
|
+ */
|
|
|
+ private String submitVideo(String videoUrl) {
|
|
|
+ try {
|
|
|
+ // 构建 form-data 请求头
|
|
|
+ HttpHeaders headers = new HttpHeaders();
|
|
|
+ headers.setContentType(MediaType.MULTIPART_FORM_DATA);
|
|
|
+
|
|
|
+ // 构建 form-data 请求体,兼容服务端可能的三种参数名
|
|
|
+ MultiValueMap<String, Object> formData = new LinkedMultiValueMap<>();
|
|
|
+ formData.add("videoUrl", videoUrl);
|
|
|
+ formData.add("video_path", videoUrl);
|
|
|
+ formData.add("video_url", videoUrl);
|
|
|
+
|
|
|
+ HttpEntity<MultiValueMap<String, Object>> requestEntity = new HttpEntity<>(formData, headers);
|
|
|
+
|
|
|
+ log.info("提交视频审核任务:url={}, videoUrl={}", videoSubmitUrl, videoUrl);
|
|
|
+
|
|
|
+ // 发送请求
|
|
|
+ ResponseEntity<String> response = restTemplate.postForEntity(videoSubmitUrl, requestEntity, String.class);
|
|
|
+
|
|
|
+ if (response.getStatusCode() == HttpStatus.OK) {
|
|
|
+ String responseBody = response.getBody();
|
|
|
+ log.info("视频提交接口响应:{}", responseBody);
|
|
|
+
|
|
|
+ if (StringUtils.hasText(responseBody)) {
|
|
|
+ JSONObject jsonResponse = JSONObject.parseObject(responseBody);
|
|
|
+ String taskId = jsonResponse.getString("task_id");
|
|
|
+ if (StringUtils.hasText(taskId)) {
|
|
|
+ return taskId;
|
|
|
+ } else {
|
|
|
+ log.error("视频提交接口返回的task_id为空");
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ log.error("视频提交接口返回空响应");
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ log.error("视频提交接口调用失败,状态码:{}", response.getStatusCode());
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ } catch (org.springframework.web.client.HttpClientErrorException e) {
|
|
|
+ log.error("调用视频提交接口异常,status={}, body={}", e.getStatusCode(), e.getResponseBodyAsString(), e);
|
|
|
+ return null;
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("调用视频提交接口异常", e);
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 轮询视频审核状态
|
|
|
+ *
|
|
|
+ * @param taskId 任务ID
|
|
|
+ * @param videoUrl 视频URL(用于日志)
|
|
|
+ * @return 审核结果
|
|
|
+ */
|
|
|
+ private VideoAuditResult pollVideoStatus(String taskId, String videoUrl) {
|
|
|
+ int pollCount = 0;
|
|
|
+ while (pollCount < maxPollCount) {
|
|
|
+ try {
|
|
|
+ // 等待一段时间再查询
|
|
|
+ if (pollCount > 0) {
|
|
|
+ Thread.sleep(pollInterval);
|
|
|
+ }
|
|
|
+
|
|
|
+ String statusUrl = videoStatusUrl + "/" + taskId;
|
|
|
+ log.info("查询视频审核状态:url={}, taskId={}, pollCount={}", statusUrl, taskId, pollCount + 1);
|
|
|
+
|
|
|
+ ResponseEntity<String> response = restTemplate.getForEntity(statusUrl, String.class);
|
|
|
+
|
|
|
+ if (response.getStatusCode() == HttpStatus.OK) {
|
|
|
+ String responseBody = response.getBody();
|
|
|
+ log.info("视频审核状态接口响应:{}", responseBody);
|
|
|
+
|
|
|
+ if (StringUtils.hasText(responseBody)) {
|
|
|
+ JSONObject jsonResponse = JSONObject.parseObject(responseBody);
|
|
|
+ String status = jsonResponse.getString("status");
|
|
|
+
|
|
|
+ if ("completed".equals(status)) {
|
|
|
+ // 审核完成,解析结果
|
|
|
+ return parseVideoAuditResult(jsonResponse, taskId);
|
|
|
+ } else if ("failed".equals(status) || "error".equals(status)) {
|
|
|
+ // 审核失败
|
|
|
+ String error = jsonResponse.getString("error");
|
|
|
+ log.error("视频审核失败,taskId={}, error={}", taskId, error);
|
|
|
+ return new VideoAuditResult(false, "视频审核失败:" + (error != null ? error : "未知错误"), taskId, jsonResponse);
|
|
|
+ } else {
|
|
|
+ // 审核中,继续轮询
|
|
|
+ log.info("视频审核中,taskId={}, status={}, 继续等待...", taskId, status);
|
|
|
+ pollCount++;
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ log.error("视频审核状态接口返回空响应");
|
|
|
+ return new VideoAuditResult(false, "审核状态接口返回空响应");
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ log.error("视频审核状态接口调用失败,状态码:{}", response.getStatusCode());
|
|
|
+ return new VideoAuditResult(false, "审核状态接口调用失败,状态码:" + response.getStatusCode());
|
|
|
+ }
|
|
|
+
|
|
|
+ } catch (InterruptedException e) {
|
|
|
+ Thread.currentThread().interrupt();
|
|
|
+ log.error("轮询视频审核状态被中断,taskId={}", taskId, e);
|
|
|
+ return new VideoAuditResult(false, "审核被中断");
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("查询视频审核状态异常,taskId={}", taskId, e);
|
|
|
+ pollCount++;
|
|
|
+ // 继续重试
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ log.error("视频审核超时,taskId={}, maxPollCount={}", taskId, maxPollCount);
|
|
|
+ return new VideoAuditResult(false, "视频审核超时");
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 解析视频审核结果
|
|
|
+ *
|
|
|
+ * @param jsonResponse API响应JSON
|
|
|
+ * @param taskId 任务ID
|
|
|
+ * @return 审核结果
|
|
|
+ */
|
|
|
+ private VideoAuditResult parseVideoAuditResult(JSONObject jsonResponse, String taskId) {
|
|
|
+ try {
|
|
|
+ JSONObject result = jsonResponse.getJSONObject("result");
|
|
|
+ if (result == null) {
|
|
|
+ log.warn("视频审核结果中result字段为空");
|
|
|
+ return new VideoAuditResult(false, "审核结果格式错误:result字段为空", taskId, jsonResponse);
|
|
|
+ }
|
|
|
+
|
|
|
+ Boolean flagged = result.getBoolean("flagged");
|
|
|
+ String riskLevel = result.getString("risk_level");
|
|
|
+ String summary = result.getString("summary");
|
|
|
+
|
|
|
+ if (flagged != null && flagged) {
|
|
|
+ // 有违规内容,审核失败
|
|
|
+ List<String> violationReasons = new ArrayList<>();
|
|
|
+ if (StringUtils.hasText(riskLevel)) {
|
|
|
+ violationReasons.add("风险等级:" + riskLevel);
|
|
|
+ }
|
|
|
+ if (StringUtils.hasText(summary)) {
|
|
|
+ violationReasons.add(summary);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 解析违规证据
|
|
|
+ com.alibaba.fastjson2.JSONArray evidence = result.getJSONArray("evidence");
|
|
|
+ if (evidence != null && !evidence.isEmpty()) {
|
|
|
+ List<String> evidenceReasons = new ArrayList<>();
|
|
|
+ for (int i = 0; i < evidence.size(); i++) {
|
|
|
+ JSONObject item = evidence.getJSONObject(i);
|
|
|
+ if (item != null) {
|
|
|
+ String reason = item.getString("reason");
|
|
|
+ String timeOffset = item.getString("time_offset");
|
|
|
+ if (StringUtils.hasText(reason)) {
|
|
|
+ String evidenceReason = StringUtils.hasText(timeOffset)
|
|
|
+ ? String.format("时间点%s: %s", timeOffset, reason)
|
|
|
+ : reason;
|
|
|
+ evidenceReasons.add(evidenceReason);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (!evidenceReasons.isEmpty()) {
|
|
|
+ violationReasons.add("违规证据:" + String.join("; ", evidenceReasons));
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ String failureReason = String.join("; ", violationReasons);
|
|
|
+ if (failureReason.isEmpty()) {
|
|
|
+ failureReason = "视频内容违规";
|
|
|
+ }
|
|
|
+
|
|
|
+ log.warn("视频审核失败:{}", failureReason);
|
|
|
+ return new VideoAuditResult(false, failureReason, taskId, jsonResponse);
|
|
|
+ } else {
|
|
|
+ // 无违规内容,审核通过
|
|
|
+ log.info("视频审核通过:taskId={}", taskId);
|
|
|
+ return new VideoAuditResult(true, null, taskId, jsonResponse);
|
|
|
+ }
|
|
|
+
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("解析视频审核结果异常", e);
|
|
|
+ return new VideoAuditResult(false, "解析审核结果异常:" + e.getMessage(), taskId, jsonResponse);
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|