|
@@ -1,72 +1,27 @@
|
|
|
package shop.alien.store.util.ai;
|
|
package shop.alien.store.util.ai;
|
|
|
|
|
|
|
|
import com.alibaba.fastjson2.JSONObject;
|
|
import com.alibaba.fastjson2.JSONObject;
|
|
|
-import lombok.RequiredArgsConstructor;
|
|
|
|
|
import lombok.extern.slf4j.Slf4j;
|
|
import lombok.extern.slf4j.Slf4j;
|
|
|
-import org.springframework.beans.factory.annotation.Value;
|
|
|
|
|
-import org.springframework.cloud.context.config.annotation.RefreshScope;
|
|
|
|
|
-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.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;
|
|
import java.util.List;
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
- * 视频审核工具类
|
|
|
|
|
- * 调用AI视频审核接口审核视频内容
|
|
|
|
|
|
|
+ * 视频审核占位:视频由前端直连 AI,服务端不调用视频审核接口。
|
|
|
|
|
+ * <p>保留 {@link #auditVideos} 供业务异步流程调用,恒为通过,避免历史代码空指针。</p>
|
|
|
*/
|
|
*/
|
|
|
@Slf4j
|
|
@Slf4j
|
|
|
@Component
|
|
@Component
|
|
|
-@RefreshScope
|
|
|
|
|
-@RequiredArgsConstructor
|
|
|
|
|
public class AiVideoModerationUtil {
|
|
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 {
|
|
public static class VideoAuditResult {
|
|
|
- private boolean passed;
|
|
|
|
|
- private String failureReason;
|
|
|
|
|
- private String taskId;
|
|
|
|
|
- private JSONObject rawResult;
|
|
|
|
|
|
|
+ private final boolean passed;
|
|
|
|
|
+ private final String failureReason;
|
|
|
|
|
+ private final String taskId;
|
|
|
|
|
+ private final JSONObject rawResult;
|
|
|
|
|
|
|
|
public VideoAuditResult(boolean passed, String failureReason) {
|
|
public VideoAuditResult(boolean passed, String failureReason) {
|
|
|
- this.passed = passed;
|
|
|
|
|
- this.failureReason = failureReason;
|
|
|
|
|
|
|
+ this(passed, failureReason, null, null);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
public VideoAuditResult(boolean passed, String failureReason, String taskId, JSONObject rawResult) {
|
|
public VideoAuditResult(boolean passed, String failureReason, String taskId, JSONObject rawResult) {
|
|
@@ -94,260 +49,13 @@ public class AiVideoModerationUtil {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
- * 审核视频内容
|
|
|
|
|
- *
|
|
|
|
|
- * @param videoUrls 视频URL列表
|
|
|
|
|
- * @return 审核结果
|
|
|
|
|
|
|
+ * 不进行服务端视频审核;前端已审则此处仅打日志并返回通过。
|
|
|
*/
|
|
*/
|
|
|
public VideoAuditResult auditVideos(List<String> videoUrls) {
|
|
public VideoAuditResult auditVideos(List<String> videoUrls) {
|
|
|
if (videoUrls == null || videoUrls.isEmpty()) {
|
|
if (videoUrls == null || videoUrls.isEmpty()) {
|
|
|
- log.info("视频URL列表为空,跳过审核");
|
|
|
|
|
return new VideoAuditResult(true, null);
|
|
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);
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ log.info("跳过服务端视频审核(前端已处理),条数={}", videoUrls.size());
|
|
|
|
|
+ return new VideoAuditResult(true, null);
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
-
|
|
|