134 lines
4.0 KiB
Python
134 lines
4.0 KiB
Python
"""
|
|
内容指纹工具
|
|
============
|
|
|
|
用于 AI 第三方平台(如 Vidu)的审核结果缓存与防重复提交。
|
|
|
|
核心逻辑:
|
|
- 对提交的音频/视频/图片 URL + 任务参数生成 SHA256 指纹。
|
|
- 如果相同内容近期因审核失败被缓存,则直接返回错误,不再调用第三方平台。
|
|
- 仅规范化 URL(去掉 token 等动态参数),不下载大文件本身。
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import hashlib
|
|
from urllib.parse import parse_qs, urlencode, urlparse
|
|
|
|
# Vidu 内容安全/审核类错误码
|
|
# 这些错误在内容不变的情况下重试也没用,应该被缓存。
|
|
VIDU_AUDIT_ERROR_CODES = frozenset(
|
|
{
|
|
"TaskPromptPolicyViolation", # Prompt 触发安审风控
|
|
"AuditSubmitIllegal", # 输入未通过安全审核
|
|
"CreationPolicyViolation", # 生成物触发风控
|
|
"PhotoAuditNotPass", # 图片审核不通过
|
|
"AuditFailed", # 审核失败
|
|
"ImageCheckBodyJointsFailed", # 输入图人体检测失败
|
|
"ImageCheckFaceFailed", # 输入图人脸检测失败
|
|
"ImageObjectsUndetected", # 人体或人脸有遮挡
|
|
"FaceDetectFailure", # 人脸检测失败
|
|
"FaceDetectNotPass", # 人脸检测不通过
|
|
"NoFaceDetected", # 未检测到人脸
|
|
"MultiFaceDetected", # 检测到多张人脸
|
|
}
|
|
)
|
|
|
|
|
|
# 常见动态 query 参数,生成指纹时应忽略
|
|
_DYNAMIC_QUERY_PARAMS = frozenset(
|
|
{
|
|
"token",
|
|
"e", # 七牛过期时间戳
|
|
"t", # 时间戳
|
|
"sign",
|
|
"x-oss-signature",
|
|
"x-oss-expires",
|
|
"x-oss-access-key-id",
|
|
"response-content-disposition",
|
|
"v", # 版本号/缓存戳
|
|
}
|
|
)
|
|
|
|
|
|
def normalize_url(url: str | None) -> str:
|
|
"""规范化 URL,去掉动态参数,确保同一资源不同 token 得到相同指纹。
|
|
|
|
Args:
|
|
url: 原始 URL,可能包含七牛私有 token、时间戳等动态参数
|
|
|
|
Returns:
|
|
规范化后的 URL 字符串,None 或空字符串返回空串
|
|
"""
|
|
if not url:
|
|
return ""
|
|
|
|
parsed = urlparse(url)
|
|
query_params = parse_qs(parsed.query, keep_blank_values=True)
|
|
|
|
for key in list(query_params.keys()):
|
|
if key.lower() in _DYNAMIC_QUERY_PARAMS:
|
|
del query_params[key]
|
|
|
|
query = urlencode(sorted(query_params.items()), doseq=True)
|
|
path = parsed.path or "/"
|
|
|
|
if query:
|
|
return f"{parsed.scheme}://{parsed.netloc}{path}?{query}"
|
|
return f"{parsed.scheme}://{parsed.netloc}{path}"
|
|
|
|
|
|
def compute_content_fingerprint(
|
|
task_type: str,
|
|
*,
|
|
video_url: str | None = None,
|
|
audio_url: str | None = None,
|
|
ref_photo_url: str | None = None,
|
|
text: str | None = None,
|
|
voice_id: str | None = None,
|
|
) -> str:
|
|
"""计算内容指纹。
|
|
|
|
指纹字段选择原则:只包含会影响 Vidu 审核结果的输入内容。
|
|
不包含 callback_url、speed、volume、payload 等业务/技术参数。
|
|
|
|
Args:
|
|
task_type: 任务类型,如 "lip_sync", "tts", "clone_voice"
|
|
video_url: 视频 URL
|
|
audio_url: 音频 URL
|
|
ref_photo_url: 参考图片 URL
|
|
text: 文本/Prompt
|
|
voice_id: 音色 ID
|
|
|
|
Returns:
|
|
SHA256 十六进制指纹字符串
|
|
"""
|
|
parts = [
|
|
task_type.strip().lower(),
|
|
normalize_url(video_url),
|
|
normalize_url(audio_url),
|
|
normalize_url(ref_photo_url),
|
|
(text or "").strip(),
|
|
(voice_id or "").strip().lower(),
|
|
]
|
|
raw = "|".join(parts)
|
|
return hashlib.sha256(raw.encode("utf-8")).hexdigest()
|
|
|
|
|
|
def is_vidu_audit_error(err_code: str | None) -> bool:
|
|
"""判断是否为 Vidu 审核类错误码。"""
|
|
if not err_code:
|
|
return False
|
|
return err_code.strip() in VIDU_AUDIT_ERROR_CODES
|
|
|
|
|
|
def extract_vidu_error_code(message: str | None) -> str | None:
|
|
"""从 Vidu 错误信息中提取错误码。
|
|
|
|
Vidu 错误信息格式通常为:"ErrorCode: 中文描述"
|
|
"""
|
|
if not message:
|
|
return None
|
|
candidate = message.split(":")[0].strip()
|
|
return candidate or None
|