229 lines
7.6 KiB
Python
229 lines
7.6 KiB
Python
|
|
import json
|
|||
|
|
import io
|
|||
|
|
import uuid
|
|||
|
|
from urllib.parse import unquote, urljoin, urlparse
|
|||
|
|
|
|||
|
|
import requests
|
|||
|
|
from alibabacloud_tea_openapi.client import Client as OpenApiClient
|
|||
|
|
from alibabacloud_tea_openapi import models as open_api_models
|
|||
|
|
from alibabacloud_tea_openapi import utils_models as open_api_util_models
|
|||
|
|
from alibabacloud_tea_util import models as util_models
|
|||
|
|
|
|||
|
|
from config import Config
|
|||
|
|
from extensions import db, s3_client
|
|||
|
|
from models import GenerationRecord
|
|||
|
|
from services.generation_service import refund_points, try_deduct_points
|
|||
|
|
from services.logger import system_logger
|
|||
|
|
from utils import get_proxied_url
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _create_client():
|
|||
|
|
config = open_api_models.Config(
|
|||
|
|
access_key_id=Config.ALIBABA_CLOUD_ACCESS_KEY_ID,
|
|||
|
|
access_key_secret=Config.ALIBABA_CLOUD_ACCESS_KEY_SECRET,
|
|||
|
|
region_id=Config.AIGC_DETECTOR_REGION_ID,
|
|||
|
|
endpoint=Config.AIGC_DETECTOR_ENDPOINT,
|
|||
|
|
)
|
|||
|
|
return OpenApiClient(config)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _normalize_image_url(image_url, request_root):
|
|||
|
|
if not image_url:
|
|||
|
|
return None
|
|||
|
|
if image_url.startswith(("http://", "https://")):
|
|||
|
|
return image_url
|
|||
|
|
return urljoin(request_root, image_url.lstrip("/"))
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _is_own_file_url(image_url, request_root):
|
|||
|
|
public_path = Config.MINIO["public_url"].rstrip("/") + "/"
|
|||
|
|
parsed = urlparse(image_url)
|
|||
|
|
if not parsed.scheme:
|
|||
|
|
return image_url.startswith(public_path)
|
|||
|
|
|
|||
|
|
root = urlparse(request_root)
|
|||
|
|
return parsed.netloc == root.netloc and parsed.path.startswith(public_path)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _extract_minio_object_key(image_url, request_root):
|
|||
|
|
public_path = Config.MINIO["public_url"].rstrip("/") + "/"
|
|||
|
|
parsed = urlparse(_normalize_image_url(image_url, request_root))
|
|||
|
|
minio_endpoint = urlparse(Config.MINIO["endpoint"])
|
|||
|
|
bucket_path = f"/{Config.MINIO['bucket']}/"
|
|||
|
|
|
|||
|
|
if parsed.netloc == urlparse(request_root).netloc and parsed.path.startswith(public_path):
|
|||
|
|
return unquote(parsed.path[len(public_path):])
|
|||
|
|
|
|||
|
|
if parsed.netloc == minio_endpoint.netloc and parsed.path.startswith(bucket_path):
|
|||
|
|
return unquote(parsed.path[len(bucket_path):])
|
|||
|
|
|
|||
|
|
return None
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _get_presigned_minio_url(object_key):
|
|||
|
|
return s3_client.generate_presigned_url(
|
|||
|
|
"get_object",
|
|||
|
|
Params={"Bucket": Config.MINIO["bucket"], "Key": object_key},
|
|||
|
|
ExpiresIn=1800,
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _mirror_image_for_detection(image_url, request_root):
|
|||
|
|
"""把外部临时图转存到 MinIO,并返回给阿里云可直接下载的签名 URL。"""
|
|||
|
|
normalized_url = _normalize_image_url(image_url, request_root)
|
|||
|
|
object_key = _extract_minio_object_key(normalized_url, request_root)
|
|||
|
|
if object_key:
|
|||
|
|
return _get_presigned_minio_url(object_key)
|
|||
|
|
|
|||
|
|
resp = requests.get(get_proxied_url(normalized_url), timeout=Config.PROXY_TIMEOUT_SHORT)
|
|||
|
|
resp.raise_for_status()
|
|||
|
|
|
|||
|
|
content_type = resp.headers.get("Content-Type") or "image/png"
|
|||
|
|
ext = ".png"
|
|||
|
|
if "jpeg" in content_type or "jpg" in content_type:
|
|||
|
|
ext = ".jpg"
|
|||
|
|
elif "webp" in content_type:
|
|||
|
|
ext = ".webp"
|
|||
|
|
|
|||
|
|
filename = f"aigc-detect-{uuid.uuid4().hex}{ext}"
|
|||
|
|
s3_client.upload_fileobj(
|
|||
|
|
io.BytesIO(resp.content),
|
|||
|
|
Config.MINIO["bucket"],
|
|||
|
|
filename,
|
|||
|
|
ExtraArgs={"ContentType": content_type},
|
|||
|
|
)
|
|||
|
|
return _get_presigned_minio_url(filename)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _extract_result(resp):
|
|||
|
|
body = resp.get("body", resp)
|
|||
|
|
data = body.get("Data") or body.get("data") or {}
|
|||
|
|
if isinstance(data, str):
|
|||
|
|
try:
|
|||
|
|
data = json.loads(data)
|
|||
|
|
except json.JSONDecodeError:
|
|||
|
|
data = {"raw": data}
|
|||
|
|
|
|||
|
|
result = data.get("Result") or data.get("result") or []
|
|||
|
|
result_items = result if isinstance(result, list) else []
|
|||
|
|
first = result_items[0] if result_items else {}
|
|||
|
|
if not isinstance(first, dict):
|
|||
|
|
first = {"raw": first}
|
|||
|
|
|
|||
|
|
label = first.get("Label") or first.get("label") or data.get("Label") or data.get("label")
|
|||
|
|
confidence = (
|
|||
|
|
first.get("Confidence")
|
|||
|
|
or first.get("confidence")
|
|||
|
|
or first.get("Rate")
|
|||
|
|
or first.get("rate")
|
|||
|
|
or data.get("Confidence")
|
|||
|
|
or data.get("confidence")
|
|||
|
|
)
|
|||
|
|
risk_level = (
|
|||
|
|
first.get("RiskLevel")
|
|||
|
|
or first.get("riskLevel")
|
|||
|
|
or data.get("RiskLevel")
|
|||
|
|
or data.get("riskLevel")
|
|||
|
|
)
|
|||
|
|
suggestion = (
|
|||
|
|
first.get("Suggestion")
|
|||
|
|
or first.get("suggestion")
|
|||
|
|
or data.get("Suggestion")
|
|||
|
|
or data.get("suggestion")
|
|||
|
|
)
|
|||
|
|
description = (
|
|||
|
|
first.get("Description")
|
|||
|
|
or first.get("description")
|
|||
|
|
or data.get("Description")
|
|||
|
|
or data.get("description")
|
|||
|
|
)
|
|||
|
|
details = []
|
|||
|
|
for item in result_items:
|
|||
|
|
if not isinstance(item, dict):
|
|||
|
|
continue
|
|||
|
|
details.append({
|
|||
|
|
"label": item.get("Label") or item.get("label"),
|
|||
|
|
"description": item.get("Description") or item.get("description"),
|
|||
|
|
"confidence": item.get("Confidence") or item.get("confidence"),
|
|||
|
|
"risk_level": item.get("RiskLevel") or item.get("riskLevel") or risk_level,
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
return {
|
|||
|
|
"label": label,
|
|||
|
|
"description": description,
|
|||
|
|
"confidence": confidence,
|
|||
|
|
"risk_level": risk_level,
|
|||
|
|
"suggestion": suggestion,
|
|||
|
|
"details": details,
|
|||
|
|
"raw": body,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _ensure_success(resp):
|
|||
|
|
body = resp.get("body", resp)
|
|||
|
|
code = body.get("Code") if isinstance(body, dict) else None
|
|||
|
|
if code is None and isinstance(body, dict):
|
|||
|
|
code = body.get("code")
|
|||
|
|
if code is None or str(code) == "200":
|
|||
|
|
return
|
|||
|
|
|
|||
|
|
message = body.get("Msg") or body.get("Message") or body.get("msg") or body.get("message") or "检测接口返回失败"
|
|||
|
|
raise RuntimeError(f"{message} (code={code})")
|
|||
|
|
|
|||
|
|
|
|||
|
|
def detect_aigc_image(user_id, image_url, request_root):
|
|||
|
|
cost = Config.AIGC_DETECTOR_COST
|
|||
|
|
if not image_url:
|
|||
|
|
return {"error": "缺少图片地址"}, 400
|
|||
|
|
|
|||
|
|
if not try_deduct_points(user_id, cost):
|
|||
|
|
return {"error": f"积分不足,本次 AI 生成检测需要 {cost} 积分"}, 400
|
|||
|
|
|
|||
|
|
try:
|
|||
|
|
normalized_url = _mirror_image_for_detection(image_url, request_root)
|
|||
|
|
client = _create_client()
|
|||
|
|
params = open_api_util_models.Params(
|
|||
|
|
action="ImageModeration",
|
|||
|
|
version="2022-03-02",
|
|||
|
|
protocol="HTTPS",
|
|||
|
|
pathname="/",
|
|||
|
|
method="POST",
|
|||
|
|
auth_type="AK",
|
|||
|
|
body_type="json",
|
|||
|
|
req_body_type="json",
|
|||
|
|
style="ROA",
|
|||
|
|
)
|
|||
|
|
request = open_api_util_models.OpenApiRequest(
|
|||
|
|
body={
|
|||
|
|
"Service": Config.AIGC_DETECTOR_SERVICE,
|
|||
|
|
"ServiceParameters": json.dumps(
|
|||
|
|
{"imageUrl": normalized_url},
|
|||
|
|
ensure_ascii=False,
|
|||
|
|
),
|
|||
|
|
}
|
|||
|
|
)
|
|||
|
|
runtime = util_models.RuntimeOptions(
|
|||
|
|
read_timeout=Config.PROXY_TIMEOUT_DEFAULT * 1000,
|
|||
|
|
connect_timeout=Config.PROXY_TIMEOUT_SHORT * 1000,
|
|||
|
|
)
|
|||
|
|
resp = client.do_request(params, request, runtime)
|
|||
|
|
_ensure_success(resp)
|
|||
|
|
result = _extract_result(resp)
|
|||
|
|
|
|||
|
|
record = GenerationRecord(
|
|||
|
|
user_id=user_id,
|
|||
|
|
prompt=f"AI生成检测: {normalized_url}",
|
|||
|
|
model=Config.AIGC_DETECTOR_SERVICE,
|
|||
|
|
cost=cost,
|
|||
|
|
image_urls=json.dumps([{"type": "aigc_detection", "url": normalized_url, "result": result}], ensure_ascii=False),
|
|||
|
|
)
|
|||
|
|
db.session.add(record)
|
|||
|
|
db.session.commit()
|
|||
|
|
|
|||
|
|
return {"message": "检测完成", "cost": cost, "result": result}, 200
|
|||
|
|
except Exception as e:
|
|||
|
|
db.session.rollback()
|
|||
|
|
refund_points(user_id, cost)
|
|||
|
|
system_logger.error("AI生成检测失败", user_id=user_id, image_url=image_url, error=str(e))
|
|||
|
|
return {"error": f"AI生成检测失败: {str(e)}"}, 500
|