|
|
|
|
package com.youlai.boot.common.util;
|
|
|
|
|
|
|
|
|
|
import com.alibaba.fastjson.JSONArray;
|
|
|
|
|
import com.alibaba.fastjson.JSONObject;
|
|
|
|
|
|
|
|
|
|
import java.util.List;
|
|
|
|
|
import com.aliyun.green20220302.Client;
|
|
|
|
|
import com.aliyun.green20220302.models.*;
|
|
|
|
|
import com.aliyun.teaopenapi.models.Config;
|
|
|
|
|
import com.youlai.boot.common.exception.MsgException;
|
|
|
|
|
import lombok.extern.slf4j.Slf4j;
|
|
|
|
|
import org.springframework.beans.factory.annotation.Value;
|
|
|
|
|
import org.springframework.stereotype.Component;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 阿里云内容安全统一审核工具类(v2.0)
|
|
|
|
|
* ----------------------------------------
|
|
|
|
|
* 支持:
|
|
|
|
|
* 1. 文本审核
|
|
|
|
|
* 2. 图片审核
|
|
|
|
|
* 3. 视频审核(异步)
|
|
|
|
|
* 4. 取消视频审核任务(直播流)
|
|
|
|
|
*
|
|
|
|
|
* 特性:
|
|
|
|
|
* - 自动切换备用节点(cn-beijing)
|
|
|
|
|
* - 从 application.yml 中读取配置
|
|
|
|
|
* - 返回 null 表示请求失败(业务层自行判断)
|
|
|
|
|
*/
|
|
|
|
|
@Slf4j
|
|
|
|
|
@Component
|
|
|
|
|
public class AliyunContentAuditUtil {
|
|
|
|
|
|
|
|
|
|
@Value("${audit.aliyun.green.accessKeyId}")
|
|
|
|
|
private String accessKeyId;
|
|
|
|
|
|
|
|
|
|
@Value("${audit.aliyun.green.accessKeySecret}")
|
|
|
|
|
private String accessKeySecret;
|
|
|
|
|
|
|
|
|
|
// 主节点(上海)
|
|
|
|
|
private static final String REGION_PRIMARY = "cn-shanghai";
|
|
|
|
|
private static final String ENDPOINT_PRIMARY = "green-cip.cn-shanghai.aliyuncs.com";
|
|
|
|
|
|
|
|
|
|
// 备用节点(北京)
|
|
|
|
|
private static final String REGION_BACKUP = "cn-beijing";
|
|
|
|
|
private static final String ENDPOINT_BACKUP = "green-cip.cn-beijing.aliyuncs.com";
|
|
|
|
|
|
|
|
|
|
// 批量审核上限
|
|
|
|
|
private static final int BATCH_TEXT_MAX = 100;
|
|
|
|
|
private static final int BATCH_TEXT_SINGLE_MAX_CHARS = 600;
|
|
|
|
|
private static final int BATCH_IMAGE_MAX = 100;
|
|
|
|
|
private static final int BATCH_VIDEO_MAX = 10;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 构建客户端
|
|
|
|
|
*/
|
|
|
|
|
private Client createClient(boolean useBackup) throws Exception {
|
|
|
|
|
String region = useBackup ? REGION_BACKUP : REGION_PRIMARY;
|
|
|
|
|
String endpoint = useBackup ? ENDPOINT_BACKUP : ENDPOINT_PRIMARY;
|
|
|
|
|
|
|
|
|
|
Config config = new Config()
|
|
|
|
|
.setAccessKeyId(accessKeyId)
|
|
|
|
|
.setAccessKeySecret(accessKeySecret)
|
|
|
|
|
.setRegionId(region)
|
|
|
|
|
.setEndpoint(endpoint)
|
|
|
|
|
.setReadTimeout(6000)
|
|
|
|
|
.setConnectTimeout(3000);
|
|
|
|
|
|
|
|
|
|
return new Client(config);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 自动调用机制:
|
|
|
|
|
* - 优先使用上海节点
|
|
|
|
|
* - 如果响应码为 500 或出现异常,自动切换至北京节点重试一次
|
|
|
|
|
*/
|
|
|
|
|
private <T> T executeWithFailover(AliyunRequestExecutor<T> executor) {
|
|
|
|
|
try {
|
|
|
|
|
Client primary = createClient(false);
|
|
|
|
|
T result = executor.execute(primary);
|
|
|
|
|
if (isServerError(result)) {
|
|
|
|
|
log.warn("阿里云内容安全服务端异常,切换到北京节点重试...");
|
|
|
|
|
Client backup = createClient(true);
|
|
|
|
|
return executor.execute(backup);
|
|
|
|
|
}
|
|
|
|
|
return result;
|
|
|
|
|
} catch (Exception e) {
|
|
|
|
|
log.error("阿里云内容安全调用异常(主节点失败),切换至备用节点", e);
|
|
|
|
|
try {
|
|
|
|
|
Client backup = createClient(true);
|
|
|
|
|
return executor.execute(backup);
|
|
|
|
|
} catch (Exception ex) {
|
|
|
|
|
log.error("备用节点调用也失败", ex);
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 判断是否服务端异常(statusCode=500)
|
|
|
|
|
*/
|
|
|
|
|
private boolean isServerError(Object response) {
|
|
|
|
|
try {
|
|
|
|
|
Integer code = (Integer) response.getClass().getMethod("getStatusCode").invoke(response);
|
|
|
|
|
return code != null && code == 500;
|
|
|
|
|
} catch (Exception ignored) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ===================== 文本审核 =====================
|
|
|
|
|
public TextModerationResponse textModeration(String content) {
|
|
|
|
|
return executeWithFailover(client -> {
|
|
|
|
|
JSONObject params = new JSONObject();
|
|
|
|
|
params.put("content", content);
|
|
|
|
|
|
|
|
|
|
TextModerationRequest request = new TextModerationRequest()
|
|
|
|
|
.setService("comment_detection")
|
|
|
|
|
.setServiceParameters(params.toJSONString());
|
|
|
|
|
|
|
|
|
|
return client.textModeration(request);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 批量文本审核(上限100条,单条不超过600字)
|
|
|
|
|
*/
|
|
|
|
|
public TextModerationResponse batchTextModeration(List<String> contents) {
|
|
|
|
|
if (contents == null || contents.isEmpty()) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
if (contents.size() > BATCH_TEXT_MAX) {
|
|
|
|
|
throw new MsgException("批量文本审核单次上限" + BATCH_TEXT_MAX + "条,当前" + contents.size() + "条");
|
|
|
|
|
}
|
|
|
|
|
for (int i = 0; i < contents.size(); i++) {
|
|
|
|
|
String text = contents.get(i);
|
|
|
|
|
if (text != null && text.length() > BATCH_TEXT_SINGLE_MAX_CHARS) {
|
|
|
|
|
throw new MsgException("第" + (i + 1) + "条文本超过" + BATCH_TEXT_SINGLE_MAX_CHARS + "字上限");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return executeWithFailover(client -> {
|
|
|
|
|
JSONObject params = new JSONObject();
|
|
|
|
|
JSONArray arr = new JSONArray();
|
|
|
|
|
arr.addAll(contents);
|
|
|
|
|
params.put("content", arr);
|
|
|
|
|
|
|
|
|
|
TextModerationRequest request = new TextModerationRequest()
|
|
|
|
|
.setService("comment_detection")
|
|
|
|
|
.setServiceParameters(params.toJSONString());
|
|
|
|
|
|
|
|
|
|
return client.textModeration(request);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ===================== 文本审核Plus =====================
|
|
|
|
|
//nickname_detection_pro 用户昵称检测_专业版(用户昵称) ;ugc_moderation_byllm_pro UGC场景文本审核大模型服务_专业版(个人简介 / 作品内容) ;
|
|
|
|
|
//comment_detection_pro 公聊评论内容检测_专业版(评论); ugc_moderation_byllm UGC场景文本审核大模型服务 (作品标题)
|
|
|
|
|
public TextModerationPlusResponse textModerationPlus(String content) {
|
|
|
|
|
return executeWithFailover(client -> {
|
|
|
|
|
JSONObject params = new JSONObject();
|
|
|
|
|
params.put("content", content);
|
|
|
|
|
|
|
|
|
|
TextModerationPlusRequest textModerationPlusRequest = new TextModerationPlusRequest()
|
|
|
|
|
.setService("ugc_moderation_byllm_pro")
|
|
|
|
|
.setServiceParameters(params.toJSONString());
|
|
|
|
|
|
|
|
|
|
return client.textModerationPlus(textModerationPlusRequest);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 批量文本审核Plus(上限100条,单条不超过600字)
|
|
|
|
|
*/
|
|
|
|
|
public TextModerationPlusResponse batchTextModerationPlus(List<String> contents) {
|
|
|
|
|
if (contents == null || contents.isEmpty()) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
if (contents.size() > BATCH_TEXT_MAX) {
|
|
|
|
|
throw new MsgException("批量文本审核单次上限" + BATCH_TEXT_MAX + "条,当前" + contents.size() + "条");
|
|
|
|
|
}
|
|
|
|
|
for (int i = 0; i < contents.size(); i++) {
|
|
|
|
|
String text = contents.get(i);
|
|
|
|
|
if (text != null && text.length() > BATCH_TEXT_SINGLE_MAX_CHARS) {
|
|
|
|
|
throw new MsgException("第" + (i + 1) + "条文本超过" + BATCH_TEXT_SINGLE_MAX_CHARS + "字上限");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return executeWithFailover(client -> {
|
|
|
|
|
JSONObject params = new JSONObject();
|
|
|
|
|
JSONArray arr = new JSONArray();
|
|
|
|
|
arr.addAll(contents);
|
|
|
|
|
params.put("content", arr);
|
|
|
|
|
|
|
|
|
|
TextModerationPlusRequest request = new TextModerationPlusRequest()
|
|
|
|
|
.setService("ugc_moderation_byllm_pro")
|
|
|
|
|
.setServiceParameters(params.toJSONString());
|
|
|
|
|
|
|
|
|
|
return client.textModerationPlus(request);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ===================== 图片审核 =====================
|
|
|
|
|
// 头像图片检测:profilePhotoCheck (头像) ; AI生成图片鉴别_含隐式标识版:aigcDetectorFull(AI生成图片) ;
|
|
|
|
|
// 大小模型融合图片审核服务:postlmageCheckByVL (用户上传的图片) ; OSS基线检测(OSS普惠版专用):oss_baselineCheck
|
|
|
|
|
// 通用图片审核大模型服务:baselineCheckByVL(用户作品);图片万物识别:generalRecognition ;营销素材检测:advertisingCheck
|
|
|
|
|
public ImageModerationResponse imageModeration(String imageUrl) {
|
|
|
|
|
return executeWithFailover(client -> {
|
|
|
|
|
JSONObject params = new JSONObject();
|
|
|
|
|
params.put("imageUrl", imageUrl);
|
|
|
|
|
|
|
|
|
|
ImageModerationRequest request = new ImageModerationRequest()
|
|
|
|
|
.setService("oss_baselineCheck")
|
|
|
|
|
.setServiceParameters(params.toJSONString());
|
|
|
|
|
|
|
|
|
|
return client.imageModeration(request);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 批量图片审核(上限100张)
|
|
|
|
|
*/
|
|
|
|
|
public ImageModerationResponse batchImageModeration(List<String> imageUrls) {
|
|
|
|
|
if (imageUrls == null || imageUrls.isEmpty()) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
if (imageUrls.size() > BATCH_IMAGE_MAX) {
|
|
|
|
|
throw new MsgException("批量图片审核单次上限" + BATCH_IMAGE_MAX + "张,当前" + imageUrls.size() + "张");
|
|
|
|
|
}
|
|
|
|
|
return executeWithFailover(client -> {
|
|
|
|
|
JSONObject params = new JSONObject();
|
|
|
|
|
JSONArray arr = new JSONArray();
|
|
|
|
|
arr.addAll(imageUrls);
|
|
|
|
|
params.put("images", arr);
|
|
|
|
|
|
|
|
|
|
ImageModerationRequest request = new ImageModerationRequest()
|
|
|
|
|
.setService("oss_baselineCheck")
|
|
|
|
|
.setServiceParameters(params.toJSONString());
|
|
|
|
|
|
|
|
|
|
return client.imageModeration(request);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ===================== 视频审核(异步) =====================
|
|
|
|
|
//视频文件检测:videoDetection ; AI生成视频判定:videoAigcDetector ; 视频文件检测_大模型版:videoDetectionByVL
|
|
|
|
|
public VideoModerationResponse videoModeration(String videoUrl) {
|
|
|
|
|
return executeWithFailover(client -> {
|
|
|
|
|
JSONObject params = new JSONObject();
|
|
|
|
|
params.put("url", videoUrl);
|
|
|
|
|
|
|
|
|
|
VideoModerationRequest request = new VideoModerationRequest()
|
|
|
|
|
.setService("videoDetection")
|
|
|
|
|
.setServiceParameters(params.toJSONString());
|
|
|
|
|
|
|
|
|
|
return client.videoModeration(request);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 批量视频审核(异步,上限10个)
|
|
|
|
|
*/
|
|
|
|
|
public VideoModerationResponse batchVideoModeration(List<String> videoUrls) {
|
|
|
|
|
if (videoUrls == null || videoUrls.isEmpty()) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
if (videoUrls.size() > BATCH_VIDEO_MAX) {
|
|
|
|
|
throw new MsgException("批量视频审核单次上限" + BATCH_VIDEO_MAX + "个,当前" + videoUrls.size() + "个");
|
|
|
|
|
}
|
|
|
|
|
return executeWithFailover(client -> {
|
|
|
|
|
JSONObject params = new JSONObject();
|
|
|
|
|
JSONArray arr = new JSONArray();
|
|
|
|
|
arr.addAll(videoUrls);
|
|
|
|
|
params.put("urls", arr);
|
|
|
|
|
|
|
|
|
|
VideoModerationRequest request = new VideoModerationRequest()
|
|
|
|
|
.setService("videoDetection")
|
|
|
|
|
.setServiceParameters(params.toJSONString());
|
|
|
|
|
|
|
|
|
|
return client.videoModeration(request);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ===================== 查询视频审核结果 =====================
|
|
|
|
|
public VideoModerationResultResponse videoModerationResult(String taskId) {
|
|
|
|
|
return executeWithFailover(client -> {
|
|
|
|
|
JSONObject params = new JSONObject();
|
|
|
|
|
params.put("taskId", taskId);
|
|
|
|
|
|
|
|
|
|
VideoModerationResultRequest request = new VideoModerationResultRequest()
|
|
|
|
|
.setService("videoDetection")
|
|
|
|
|
.setServiceParameters(params.toJSONString());
|
|
|
|
|
|
|
|
|
|
return client.videoModerationResult(request);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ===================== 取消视频直播流检测任务 =====================
|
|
|
|
|
public VideoModerationCancelResponse cancelVideoTask(String taskId) {
|
|
|
|
|
return executeWithFailover(client -> {
|
|
|
|
|
JSONObject params = new JSONObject();
|
|
|
|
|
params.put("taskId", taskId);
|
|
|
|
|
|
|
|
|
|
VideoModerationCancelRequest request = new VideoModerationCancelRequest()
|
|
|
|
|
.setService("liveStreamDetection")
|
|
|
|
|
.setServiceParameters(params.toJSONString());
|
|
|
|
|
|
|
|
|
|
return client.videoModerationCancel(request);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 函数式接口:用于统一封装 SDK 调用逻辑
|
|
|
|
|
*/
|
|
|
|
|
@FunctionalInterface
|
|
|
|
|
private interface AliyunRequestExecutor<T> {
|
|
|
|
|
T execute(Client client) throws Exception;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|