Files
tencent_face_recognition/tencent_cloud_client.py
T

1189 lines
45 KiB
Python
Raw Normal View History

2025-09-07 21:48:21 +08:00
"""腾讯云客户端"""
import base64
import io
import json
import logging
import os
import random
import re
import requests
import time
import uuid
from typing import Dict, Any, List, Optional, Union, Callable
from tencentcloud.common import credential
from tencentcloud.common.exception.tencent_cloud_sdk_exception import TencentCloudSDKException
from tencentcloud.iai.v20200303 import iai_client, models
from .const import (
CONF_SECRET_ID,
CONF_SECRET_KEY,
CONF_REGION,
)
from .errors import (
handle_api_error,
ImageNotFoundError,
FaceNotDetectedError,
log_and_raise_error,
validate_config,
NetworkError,
AuthenticationError,
RateLimitError,
)
_LOGGER = logging.getLogger(__name__)
_LOGGER_POLLING = _LOGGER.getChild("polling")
_LOGGER_POLLING.setLevel(logging.DEBUG)
2025-09-07 21:48:21 +08:00
ENABLE_REQUEST_LOGGING = False
ENABLE_RESPONSE_LOGGING = False
ENABLE_PERFORMANCE_LOGGING = True
LOG_REQUEST_PAYLOAD_MAX_SIZE = 1024
LOG_RESPONSE_PAYLOAD_MAX_SIZE = 1024
2025-09-07 21:48:21 +08:00
MAX_ATTEMPTS = 3
INITIAL_RETRY_DELAY = 1
MAX_RETRY_DELAY = 30
RETRY_BACKOFF_FACTOR = 2
RETRY_JITTER = 0.1
2025-09-07 21:48:21 +08:00
NON_RETRYABLE_ERRORS = {
"AuthFailure",
"InvalidParameter",
"ResourceNotFound",
"LimitExceeded",
"RequestLimitExceeded",
"RequestQuotaExceeded",
2025-09-07 21:48:21 +08:00
}
RATE_LIMIT_ERRORS = {
"RequestLimitExceeded",
"RequestQuotaExceeded",
"LimitExceeded",
}
NETWORK_ERRORS = {
"ResourceUnavailable.NetworkError",
"ResourceUnavailable.ServiceTimeout",
}
MAX_IMAGE_SIZE = 10 * 1024 * 1024
MAX_IMAGE_DIMENSION = 4096
2025-09-07 21:48:21 +08:00
SUPPORTED_IMAGE_FORMATS = [".jpg", ".jpeg", ".png", ".bmp", ".gif"]
IMAGE_COMPRESSION_QUALITY = 85
MIN_FACE_SIZE = 34
2025-09-07 21:48:21 +08:00
class RetryConfig:
"""重试配置类"""
2025-09-07 21:48:21 +08:00
def __init__(
self,
max_attempts: int = MAX_ATTEMPTS,
2025-09-07 21:48:21 +08:00
initial_delay: float = INITIAL_RETRY_DELAY,
max_delay: float = MAX_RETRY_DELAY,
backoff_factor: float = RETRY_BACKOFF_FACTOR,
jitter: float = RETRY_JITTER
):
self.max_attempts = max_attempts
2025-09-07 21:48:21 +08:00
self.initial_delay = initial_delay
self.max_delay = max_delay
self.backoff_factor = backoff_factor
self.jitter = jitter
class TencentCloudClient:
"""腾讯云客户端"""
def __init__(
self,
secret_id: str,
secret_key: str,
region: str = "ap-shanghai",
retry_config: Optional[RetryConfig] = None
):
config = {
"secret_id": secret_id,
"secret_key": secret_key,
"region": region
}
validate_config(config)
2025-09-07 21:48:21 +08:00
self.secret_id = secret_id
self.secret_key = secret_key
self.region = region
self.retry_config = retry_config or RetryConfig()
self.client = self._create_client()
def _create_client(self) -> iai_client.IaiClient:
"""创建腾讯云人脸识别客户端"""
try:
_LOGGER.debug("创建腾讯云客户端,区域: %s", self.region)
cred = credential.Credential(self.secret_id, self.secret_key)
client = iai_client.IaiClient(cred, self.region)
_LOGGER.debug("腾讯云客户端创建成功")
return client
except Exception as ex:
_LOGGER.error("创建腾讯云客户端失败: %s", self._sanitize_error(str(ex)))
2025-09-07 21:48:21 +08:00
log_and_raise_error(
AuthenticationError,
"创建腾讯云客户端失败",
2025-09-07 21:48:21 +08:00
{"region": self.region, "error_type": type(ex).__name__}
)
def verify_credentials(self) -> bool:
"""验证凭据是否有效,通过调用 GetGroupList 测试"""
try:
req = models.GetGroupListRequest()
params = {"Limit": 1, "Offset": 0}
req.from_json_string(json.dumps(params))
self.client.GetGroupList(req)
_LOGGER.info("凭据验证成功")
return True
except TencentCloudSDKException as ex:
_LOGGER.error("凭据验证失败: %s", ex.code)
handle_api_error(ex.code, ex.message)
2025-09-07 21:48:21 +08:00
def _handle_error(self, ex: TencentCloudSDKException) -> None:
"""处理API错误"""
_LOGGER.error(
"腾讯云API调用失败: 错误码=%s, 请求ID=%s",
ex.code, getattr(ex, 'request_id', '未知')
2025-09-07 21:48:21 +08:00
)
handle_api_error(ex.code, ex.message)
def _sanitize_error(self, error_msg: str) -> str:
"""脱敏错误信息中的凭据"""
if self.secret_id and self.secret_id in error_msg:
error_msg = error_msg.replace(self.secret_id, f"{self.secret_id[:8]}***")
if self.secret_key and self.secret_key in error_msg:
error_msg = error_msg.replace(self.secret_key, "***")
return error_msg
2025-09-07 21:48:21 +08:00
def _get_image_base64(self, image_url: str = None, image_path: str = None, image_file: str = None) -> str:
"""获取图片的base64编码"""
_LOGGER.debug(
"获取图片 base64: image_url=%s, image_path=%s, image_file is not None: %s",
image_url, image_path, image_file is not None
)
2025-09-07 21:48:21 +08:00
if not any([image_url, image_path, image_file]):
log_and_raise_error(
ValueError,
"必须提供image_url、image_path或image_file"
)
2025-09-07 21:48:21 +08:00
try:
if image_url:
_LOGGER.debug("使用图片URL: %s", image_url)
return self._download_image_as_base64(image_url)
elif image_path:
_LOGGER.debug("使用本地图片路径: %s", image_path)
return self._read_local_image_as_base64(image_path)
elif image_file:
_LOGGER.debug("使用上传的图片文件")
return self._process_uploaded_image_as_base64(image_file)
except Exception as ex:
_LOGGER.error("处理图片失败: %s", ex)
2025-09-07 21:48:21 +08:00
log_and_raise_error(
ImageNotFoundError,
f"处理图片失败: {str(ex)}",
{"image_url": image_url, "image_path": image_path, "error_type": type(ex).__name__}
)
2025-09-07 21:48:21 +08:00
def _download_image_as_base64(self, image_url: str) -> str:
"""下载图片并转换为base64编码"""
try:
_LOGGER.debug("开始下载图片: %s", image_url)
2025-09-07 21:48:21 +08:00
if not self._is_valid_url(image_url):
log_and_raise_error(
ValueError,
f"无效的图片URL: {image_url}",
{"url": image_url}
)
2025-09-07 21:48:21 +08:00
session = requests.Session()
session.mount('http://', requests.adapters.HTTPAdapter(max_retries=2))
session.mount('https://', requests.adapters.HTTPAdapter(max_retries=2))
2025-09-07 21:48:21 +08:00
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
2025-09-07 21:48:21 +08:00
}
2025-09-07 21:48:21 +08:00
response = session.get(image_url, timeout=10, headers=headers)
response.raise_for_status()
2025-09-07 21:48:21 +08:00
content_type = response.headers.get('content-type', '').lower()
if not content_type.startswith('image/'):
log_and_raise_error(
ValueError,
f"URL不是有效的图片: {image_url}, 内容类型: {content_type}",
{"url": image_url, "content_type": content_type}
)
2025-09-07 21:48:21 +08:00
image_data = self._process_image_data(response.content)
encoded_image = base64.b64encode(image_data).decode("utf-8")
2025-09-07 21:48:21 +08:00
_LOGGER.debug("成功下载并编码图片,大小: %d 字节", len(encoded_image))
return encoded_image
2025-09-07 21:48:21 +08:00
except requests.exceptions.Timeout:
_LOGGER.error("下载图片超时: %s", image_url)
log_and_raise_error(
NetworkError,
f"下载图片超时: {image_url}",
{"url": image_url, "error": "timeout"}
)
except requests.exceptions.ConnectionError:
_LOGGER.error("下载图片连接错误: %s", image_url)
log_and_raise_error(
NetworkError,
f"下载图片连接错误: {image_url}",
{"url": image_url, "error": "connection_error"}
)
except requests.exceptions.RequestException as ex:
_LOGGER.error("下载图片失败: %s", image_url)
2025-09-07 21:48:21 +08:00
log_and_raise_error(
NetworkError,
f"下载图片失败: {image_url}",
{"url": image_url, "error": str(ex)}
)
2025-09-07 21:48:21 +08:00
def _read_local_image_as_base64(self, image_path: str) -> str:
"""读取本地图片并转换为base64编码"""
try:
if not os.path.exists(image_path):
_LOGGER.error("图片文件不存在: %s", image_path)
log_and_raise_error(
ImageNotFoundError,
f"图片文件不存在: {image_path}",
{"path": image_path}
)
2025-09-07 21:48:21 +08:00
if not os.path.isfile(image_path):
_LOGGER.error("路径不是文件: %s", image_path)
log_and_raise_error(
ImageNotFoundError,
f"路径不是文件: {image_path}",
{"path": image_path}
)
2025-09-07 21:48:21 +08:00
file_ext = os.path.splitext(image_path)[1].lower()
if file_ext not in SUPPORTED_IMAGE_FORMATS:
_LOGGER.error("不支持的图片格式: %s", file_ext)
log_and_raise_error(
ImageNotFoundError,
f"不支持的图片格式: {file_ext}",
{"path": image_path, "supported_formats": SUPPORTED_IMAGE_FORMATS}
)
2025-09-07 21:48:21 +08:00
file_size = os.path.getsize(image_path)
if file_size > MAX_IMAGE_SIZE:
_LOGGER.error("图片文件过大: %s, 大小: %d 字节", image_path, file_size)
log_and_raise_error(
ImageNotFoundError,
f"图片文件过大: {image_path}",
{"path": image_path, "size": file_size, "max_size": f"{MAX_IMAGE_SIZE // (1024*1024)}MB"}
)
2025-09-07 21:48:21 +08:00
with open(image_path, "rb") as img_file:
image_data = img_file.read()
2025-09-07 21:48:21 +08:00
processed_image = self._process_image_data(image_data)
encoded_image = base64.b64encode(processed_image).decode("utf-8")
2025-09-07 21:48:21 +08:00
_LOGGER.debug("成功读取图片文件,大小: %d 字节", len(encoded_image))
return encoded_image
2025-09-07 21:48:21 +08:00
except PermissionError:
_LOGGER.error("没有权限读取图片文件: %s", image_path)
log_and_raise_error(
ImageNotFoundError,
f"没有权限读取图片文件: {image_path}",
{"path": image_path, "error": "permission_denied"}
)
except ImageNotFoundError:
raise
2025-09-07 21:48:21 +08:00
except Exception as ex:
_LOGGER.error("读取图片文件失败: %s", image_path)
2025-09-07 21:48:21 +08:00
log_and_raise_error(
ImageNotFoundError,
f"读取图片文件失败: {image_path}",
{"path": image_path, "error": str(ex)}
)
2025-09-07 21:48:21 +08:00
def _process_uploaded_image_as_base64(self, image_file: str) -> str:
"""处理上传的图片文件并转换为base64编码"""
try:
if isinstance(image_file, str):
if image_file.startswith('data:image'):
_LOGGER.debug("处理 data URL 格式图片")
try:
header, encoded = image_file.split(',', 1)
image_data = base64.b64decode(encoded)
processed_image = self._process_image_data(image_data)
return base64.b64encode(processed_image).decode("utf-8")
except ValueError:
log_and_raise_error(
ImageNotFoundError,
"无效的 data URL 格式",
{"data_url_length": len(image_file)}
)
else:
_LOGGER.debug("使用 base64 编码图片,长度: %d", len(image_file))
try:
image_data = base64.b64decode(image_file)
if len(image_data) <= MAX_IMAGE_SIZE:
try:
from PIL import Image
img = Image.open(io.BytesIO(image_data))
if (img.size[0] <= MAX_IMAGE_DIMENSION
and img.size[1] <= MAX_IMAGE_DIMENSION
and img.format == 'JPEG'):
return image_file
except ImportError:
pass
except Exception:
pass
2025-09-07 21:48:21 +08:00
processed_image = self._process_image_data(image_data)
return base64.b64encode(processed_image).decode("utf-8")
except Exception as ex:
_LOGGER.error("base64解码失败: %s", ex)
log_and_raise_error(
ImageNotFoundError,
"base64解码失败",
{"error": str(ex), "data_length": len(image_file)}
)
else:
_LOGGER.debug("处理文件对象")
image_data = image_file.read()
processed_image = self._process_image_data(image_data)
encoded_image = base64.b64encode(processed_image).decode("utf-8")
_LOGGER.debug("成功读取上传的图片文件,大小: %d 字节", len(encoded_image))
return encoded_image
except ImageNotFoundError:
raise
2025-09-07 21:48:21 +08:00
except Exception as ex:
_LOGGER.error("处理上传的图片文件失败: %s", ex)
log_and_raise_error(
ImageNotFoundError,
"处理上传的图片文件失败",
2025-09-07 21:48:21 +08:00
{"error": str(ex), "error_type": type(ex).__name__}
)
def search_faces(
self,
image_url: str = None,
image_path: str = None,
image_file: str = None,
image_base64: str = None,
2025-09-07 21:48:21 +08:00
group_ids: List[str] = None,
max_face_num: int = 1,
min_face_size: int = 34,
max_user_num: int = 5,
quality_control: int = 1,
need_rotate_check: int = 1,
face_match_threshold: float = 60.0
2025-09-07 23:21:24 +08:00
) -> Dict[str, Any]:
2025-09-07 21:48:21 +08:00
"""搜索人脸"""
_LOGGER.info(
"开始搜索人脸: max_face_num=%d, min_face_size=%d, max_user_num=%d, quality_control=%d, need_rotate_check=%d, face_match_threshold=%.2f",
max_face_num, min_face_size, max_user_num, quality_control, need_rotate_check, face_match_threshold
2025-09-07 21:48:21 +08:00
)
2025-09-07 21:48:21 +08:00
if not group_ids:
_LOGGER.warning("未提供人员库ID,搜索可能失败")
if image_base64 is None:
image_base64 = self._get_image_base64(image_url, image_path, image_file)
2025-09-07 21:48:21 +08:00
def api_call():
request_id = str(uuid.uuid4())
start_time = time.time()
2025-09-07 21:48:21 +08:00
try:
req = models.SearchFacesRequest()
params = {
"NeedPersonInfo": 1,
"Image": image_base64,
2025-09-07 21:48:21 +08:00
"MaxFaceNum": max_face_num,
"MinFaceSize": min_face_size,
"MaxPersonNum": max_user_num,
"QualityControl": quality_control,
"NeedRotateCheck": need_rotate_check,
"FaceMatchThreshold": face_match_threshold,
"GroupIds": group_ids
}
self._log_request("人脸搜索", params, request_id)
2025-09-07 21:48:21 +08:00
req.from_json_string(json.dumps(params))
2025-09-07 21:48:21 +08:00
_LOGGER.debug("发送人脸搜索请求")
resp = self.client.SearchFaces(req)
2025-09-07 21:48:21 +08:00
duration = time.time() - start_time
2025-09-07 21:48:21 +08:00
self._log_response("人脸搜索", request_id, resp, duration)
2025-09-07 21:48:21 +08:00
results = []
if getattr(resp, "Results", []):
_LOGGER.info("检测到 %d 个人脸", len(resp.Results))
for result in resp.Results:
face_result = {
"face_id": getattr(result, "FaceId", None),
"candidates": []
}
2025-09-07 21:48:21 +08:00
if result.Candidates:
for candidate in result.Candidates:
candidate_info = {
"person_id": candidate.PersonId,
"person_name": candidate.PersonName,
"score": candidate.Score,
"person_tag": getattr(candidate, "PersonTag", None)
}
face_result["candidates"].append(candidate_info)
2025-09-07 21:48:21 +08:00
results.append(face_result)
else:
_LOGGER.info("未检测到人脸")
2025-09-07 23:21:24 +08:00
return {
"success": True,
"faces": results,
"error": None,
"error_code": None,
"error_message": None
}
2025-09-07 21:48:21 +08:00
except Exception as ex:
duration = time.time() - start_time
self._log_error("人脸搜索", request_id, ex, duration)
2025-09-07 23:21:24 +08:00
error_code = None
error_message = str(ex)
2025-09-07 23:21:24 +08:00
if isinstance(ex, TencentCloudSDKException):
error_code = getattr(ex, "code", None)
error_message = getattr(ex, "message", str(ex))
2025-09-07 23:21:24 +08:00
return {
"success": False,
"faces": [],
"error": str(ex),
"error_code": error_code,
"error_message": error_message
}
2025-09-07 21:48:21 +08:00
return self._execute_with_retry(api_call, "人脸搜索")
def detect_faces(
self,
image_url: str = None,
image_path: str = None,
image_file: str = None,
image_base64: str = None,
2025-09-07 21:48:21 +08:00
max_face_num: int = 1,
min_face_size: int = 34,
need_rotate_check: int = 1
2025-09-07 23:21:24 +08:00
) -> Dict[str, Any]:
2025-09-07 21:48:21 +08:00
"""检测人脸"""
_LOGGER.info(
"开始检测人脸: max_face_num=%d, min_face_size=%d, need_rotate_check=%d",
max_face_num, min_face_size, need_rotate_check
)
if image_base64 is None:
image_base64 = self._get_image_base64(image_url, image_path, image_file)
2025-09-07 21:48:21 +08:00
def api_call():
request_id = str(uuid.uuid4())
start_time = time.time()
2025-09-07 21:48:21 +08:00
try:
req = models.DetectFaceRequest()
params = {
"Image": image_base64,
2025-09-07 21:48:21 +08:00
"MaxFaceNum": max_face_num,
"MinFaceSize": min_face_size,
"NeedRotateCheck": need_rotate_check
}
self._log_request("人脸检测", params, request_id)
2025-09-07 21:48:21 +08:00
req.from_json_string(json.dumps(params))
2025-09-07 21:48:21 +08:00
_LOGGER.debug("发送人脸检测请求")
resp = self.client.DetectFace(req)
2025-09-07 21:48:21 +08:00
duration = time.time() - start_time
self._log_response("人脸检测", request_id, resp, duration)
2025-09-07 21:48:21 +08:00
results = []
if getattr(resp, "FaceInfos", []):
_LOGGER.info("检测到 %d 个人脸", len(resp.FaceInfos))
for face_info in resp.FaceInfos:
face_result = {
"face_id": getattr(face_info, "FaceId", ""),
"x": getattr(face_info, "X", 0),
"y": getattr(face_info, "Y", 0),
"width": getattr(face_info, "Width", 0),
"height": getattr(face_info, "Height", 0),
"face_rect": getattr(face_info, "FaceRect", None).__dict__ if getattr(face_info, "FaceRect", None) else None
}
results.append(face_result)
else:
_LOGGER.info("未检测到人脸")
2025-09-07 23:21:24 +08:00
return {
"success": True,
"faces": results,
"error": None,
"error_code": None,
"error_message": None
}
2025-09-07 21:48:21 +08:00
except Exception as ex:
duration = time.time() - start_time
self._log_error("人脸检测", request_id, ex, duration)
2025-09-07 23:21:24 +08:00
error_code = None
error_message = str(ex)
2025-09-07 23:21:24 +08:00
if isinstance(ex, TencentCloudSDKException):
error_code = getattr(ex, "code", None)
error_message = getattr(ex, "message", str(ex))
2025-09-07 23:21:24 +08:00
return {
"success": False,
"faces": [],
"error": str(ex),
"error_code": error_code,
"error_message": error_message
}
2025-09-07 21:48:21 +08:00
return self._execute_with_retry(api_call, "人脸检测")
def get_face_attributes(
self,
image_url: str = None,
image_path: str = None,
image_file: str = None,
image_base64: str = None,
2025-09-07 21:48:21 +08:00
max_face_num: int = 1,
need_rotate_check: int = 1
2025-09-07 23:21:24 +08:00
) -> Dict[str, Any]:
2025-09-07 21:48:21 +08:00
"""获取人脸属性"""
_LOGGER.info(
"开始获取人脸属性: max_face_num=%d, need_rotate_check=%d",
max_face_num, need_rotate_check
)
if image_base64 is None:
image_base64 = self._get_image_base64(image_url, image_path, image_file)
2025-09-07 21:48:21 +08:00
def api_call():
request_id = str(uuid.uuid4())
start_time = time.time()
2025-09-07 21:48:21 +08:00
try:
req = models.DetectFaceRequest()
params = {
"Image": image_base64,
2025-09-07 21:48:21 +08:00
"MaxFaceNum": max_face_num,
"NeedRotateCheck": need_rotate_check,
"NeedFaceAttributes": 1,
"NeedQualityDetection": 1,
2025-09-07 21:48:21 +08:00
}
self._log_request("获取人脸属性", params, request_id)
2025-09-07 21:48:21 +08:00
req.from_json_string(json.dumps(params))
2025-09-07 21:48:21 +08:00
_LOGGER.debug("发送获取人脸属性请求")
resp = self.client.DetectFace(req)
2025-09-07 21:48:21 +08:00
duration = time.time() - start_time
self._log_response("获取人脸属性", request_id, resp, duration)
2025-09-07 21:48:21 +08:00
results = []
if getattr(resp, "FaceInfos", []):
_LOGGER.info("检测到 %d 个人脸", len(resp.FaceInfos))
for face_info in resp.FaceInfos:
face_result = {
"face_id": getattr(face_info, "FaceId", ""),
"x": getattr(face_info, "X", 0),
"y": getattr(face_info, "Y", 0),
"width": getattr(face_info, "Width", 0),
"height": getattr(face_info, "Height", 0),
"gender": getattr(face_info, "Gender", None),
"age": getattr(face_info, "Age", None),
"expression": getattr(face_info, "Expression", None),
"beauty": getattr(face_info, "Beauty", None),
"face_rect": getattr(face_info, "FaceRect", None).__dict__ if getattr(face_info, "FaceRect", None) else None
}
results.append(face_result)
else:
_LOGGER.info("未检测到人脸")
2025-09-07 23:21:24 +08:00
return {
"success": True,
"faces": results,
"error": None,
"error_code": None,
"error_message": None
}
2025-09-07 21:48:21 +08:00
except Exception as ex:
duration = time.time() - start_time
self._log_error("获取人脸属性", request_id, ex, duration)
2025-09-07 23:21:24 +08:00
error_code = None
error_message = str(ex)
2025-09-07 23:21:24 +08:00
if isinstance(ex, TencentCloudSDKException):
error_code = getattr(ex, "code", None)
error_message = getattr(ex, "message", str(ex))
2025-09-07 23:21:24 +08:00
return {
"success": False,
"faces": [],
"error": str(ex),
"error_code": error_code,
"error_message": error_message
}
2025-09-07 21:48:21 +08:00
return self._execute_with_retry(api_call, "获取人脸属性")
def create_person(
self,
person_id: str,
person_name: str,
group_id: str,
image_url: str = None,
image_path: str = None,
image_file: str = None,
image_base64: str = None,
gender: int = None,
person_tag: str = None,
quality_control: int = 1,
need_rotate_check: int = 1,
) -> Dict[str, Any]:
"""创建人员"""
_LOGGER.info("开始创建人员: person_id=%s, group_id=%s", person_id, group_id)
if image_base64 is None and any([image_url, image_path, image_file]):
image_base64 = self._get_image_base64(image_url, image_path, image_file)
def api_call():
request_id = str(uuid.uuid4())
start_time = time.time()
try:
req = models.CreatePersonRequest()
params = {
"PersonId": person_id,
"PersonName": person_name,
"GroupId": group_id,
"QualityControl": quality_control,
"NeedRotateCheck": need_rotate_check,
}
if image_base64:
params["Image"] = image_base64
if gender is not None:
params["Gender"] = gender
if person_tag is not None:
params["PersonTag"] = person_tag
self._log_request("创建人员", params, request_id)
req.from_json_string(json.dumps(params))
_LOGGER.debug("发送创建人员请求")
resp = self.client.CreatePerson(req)
duration = time.time() - start_time
self._log_response("创建人员", request_id, resp, duration)
return {
"success": True,
"person_id": getattr(resp, "PersonId", person_id),
"face_id": getattr(resp, "FaceId", ""),
"face_rect": str(getattr(resp, "FaceRect", "")),
}
except Exception as ex:
duration = time.time() - start_time
self._log_error("创建人员", request_id, ex, duration)
raise
return self._execute_with_retry(api_call, "创建人员")
def delete_person(self, person_id: str) -> Dict[str, Any]:
"""删除人员"""
_LOGGER.info("开始删除人员: person_id=%s", person_id)
def api_call():
request_id = str(uuid.uuid4())
start_time = time.time()
try:
req = models.DeletePersonRequest()
params = {"PersonId": person_id}
self._log_request("删除人员", params, request_id)
req.from_json_string(json.dumps(params))
_LOGGER.debug("发送删除人员请求")
resp = self.client.DeletePerson(req)
duration = time.time() - start_time
self._log_response("删除人员", request_id, resp, duration)
return {"success": True, "person_id": person_id}
except Exception as ex:
duration = time.time() - start_time
self._log_error("删除人员", request_id, ex, duration)
raise
return self._execute_with_retry(api_call, "删除人员")
def create_face(
self,
person_id: str,
image_url: str = None,
image_path: str = None,
image_file: str = None,
image_base64: str = None,
quality_control: int = 1,
need_rotate_check: int = 1,
) -> Dict[str, Any]:
"""为人员注册人脸"""
_LOGGER.info("开始注册人脸: person_id=%s", person_id)
if image_base64 is None:
image_base64 = self._get_image_base64(image_url, image_path, image_file)
def api_call():
request_id = str(uuid.uuid4())
start_time = time.time()
try:
req = models.CreateFaceRequest()
params = {
"PersonId": person_id,
"Image": image_base64,
"QualityControl": quality_control,
"NeedRotateCheck": need_rotate_check,
}
self._log_request("注册人脸", params, request_id)
req.from_json_string(json.dumps(params))
_LOGGER.debug("发送注册人脸请求")
resp = self.client.CreateFace(req)
duration = time.time() - start_time
self._log_response("注册人脸", request_id, resp, duration)
return {
"success": True,
"person_id": person_id,
"face_ids": getattr(resp, "FaceIds", []),
"face_rects": [str(r) for r in getattr(resp, "FaceRects", [])],
}
except Exception as ex:
duration = time.time() - start_time
self._log_error("注册人脸", request_id, ex, duration)
raise
return self._execute_with_retry(api_call, "注册人脸")
def delete_face(self, person_id: str, face_id: str) -> Dict[str, Any]:
"""删除人脸"""
_LOGGER.info("开始删除人脸: person_id=%s, face_id=%s", person_id, face_id)
def api_call():
request_id = str(uuid.uuid4())
start_time = time.time()
try:
req = models.DeleteFaceRequest()
params = {
"PersonId": person_id,
"FaceId": face_id,
}
self._log_request("删除人脸", params, request_id)
req.from_json_string(json.dumps(params))
_LOGGER.debug("发送删除人脸请求")
resp = self.client.DeleteFace(req)
duration = time.time() - start_time
self._log_response("删除人脸", request_id, resp, duration)
return {"success": True, "person_id": person_id, "face_id": face_id}
except Exception as ex:
duration = time.time() - start_time
self._log_error("删除人脸", request_id, ex, duration)
raise
return self._execute_with_retry(api_call, "删除人脸")
2025-09-07 21:48:21 +08:00
def get_group_info(self, group_id: str) -> Dict[str, Any]:
"""获取人员库信息"""
_LOGGER.info("开始获取人员库信息: group_id=%s", group_id)
def api_call():
request_id = str(uuid.uuid4())
start_time = time.time()
try:
req = models.GetGroupInfoRequest()
params = {"GroupId": group_id}
self._log_request("获取人员库信息", params, request_id)
2025-09-07 21:48:21 +08:00
req.from_json_string(json.dumps(params))
2025-09-07 21:48:21 +08:00
_LOGGER.debug("发送获取人员库信息请求")
resp = self.client.GetGroupInfo(req)
2025-09-07 21:48:21 +08:00
duration = time.time() - start_time
self._log_response("获取人员库信息", request_id, resp, duration)
2025-09-07 21:48:21 +08:00
return {
"group_name": getattr(resp, "GroupName", ""),
"group_tag": getattr(resp, "GroupTag", ""),
"face_model_version": getattr(resp, "FaceModelVersion", ""),
"creation_timestamp": getattr(resp, "CreationTimestamp", 0),
}
except Exception as ex:
duration = time.time() - start_time
self._log_error("获取人员库信息", request_id, ex, duration)
raise
2025-09-07 21:48:21 +08:00
return self._execute_with_retry(api_call, "获取人员库信息")
def verify_credentials_lightweight(self) -> bool:
"""测试API连接,使用 GetGroupList 轻量验证凭据"""
try:
_LOGGER.info("开始测试API连接(轻量验证)")
req = models.GetGroupListRequest()
params = {"Limit": 1, "Offset": 0}
req.from_json_string(json.dumps(params))
self.client.GetGroupList(req)
_LOGGER.info("API连接测试成功")
return True
except Exception as ex:
_LOGGER.error("API连接测试失败: %s", self._sanitize_error(str(ex)))
return False
2025-09-07 21:48:21 +08:00
def _test_api_connection(self, group_id: str) -> bool:
"""测试API连接"""
try:
_LOGGER.info("开始测试API连接: group_id=%s", group_id)
self.get_group_info(group_id)
_LOGGER.info("API连接测试成功")
return True
except Exception as ex:
_LOGGER.error("API连接测试失败: %s", self._sanitize_error(str(ex)))
2025-09-07 21:48:21 +08:00
return False
def get_person_list(self, group_id: str, limit: int = 100, offset: int = 0) -> List[Dict[str, Any]]:
"""获取人员列表"""
_LOGGER.info("开始获取人员列表: group_id=%s, limit=%d, offset=%d", group_id, limit, offset)
def api_call():
request_id = str(uuid.uuid4())
start_time = time.time()
try:
req = models.GetPersonListRequest()
params = {
"GroupId": group_id,
"Limit": limit,
"Offset": offset
}
self._log_request("获取人员列表", params, request_id)
2025-09-07 21:48:21 +08:00
req.from_json_string(json.dumps(params))
2025-09-07 21:48:21 +08:00
_LOGGER.debug("发送获取人员列表请求")
resp = self.client.GetPersonList(req)
2025-09-07 21:48:21 +08:00
duration = time.time() - start_time
self._log_response("获取人员列表", request_id, resp, duration)
2025-09-07 21:48:21 +08:00
persons = []
if getattr(resp, "PersonInfos", []):
for person_info in resp.PersonInfos:
persons.append({
"person_name": getattr(person_info, "PersonName", ""),
"person_id": getattr(person_info, "PersonId", ""),
"gender": getattr(person_info, "Gender", 0),
"face_ids": getattr(person_info, "FaceIds", []),
})
return persons
except Exception as ex:
duration = time.time() - start_time
self._log_error("获取人员列表", request_id, ex, duration)
raise
2025-09-07 21:48:21 +08:00
return self._execute_with_retry(api_call, "获取人员列表")
def get_person_list_all(self, group_id: str) -> List[Dict[str, Any]]:
"""获取人员列表(自动分页获取全部)"""
all_persons = []
offset = 0
limit = 100
while True:
persons = self.get_person_list(group_id, limit=limit, offset=offset)
all_persons.extend(persons)
if len(persons) < limit:
break
offset += limit
return all_persons
2025-09-07 21:48:21 +08:00
def _execute_with_retry(self, api_call: Callable, operation_name: str) -> Any:
"""执行带重试的API调用,max_attempts 为总尝试次数(1次初始 + N次重试)"""
2025-09-07 21:48:21 +08:00
last_exception = None
for attempt in range(self.retry_config.max_attempts):
2025-09-07 21:48:21 +08:00
try:
if attempt > 0:
delay = self._calculate_retry_delay(attempt)
_LOGGER.info("重试第 %d%s,延迟 %.2f 秒", attempt, operation_name, delay)
2025-09-07 21:48:21 +08:00
time.sleep(delay)
2025-09-07 21:48:21 +08:00
return api_call()
2025-09-07 21:48:21 +08:00
except TencentCloudSDKException as ex:
last_exception = ex
_LOGGER.warning("%s失败 (尝试 %d/%d): %s", operation_name, attempt + 1, self.retry_config.max_attempts, ex.code)
2025-09-07 21:48:21 +08:00
if not self._should_retry(ex):
_LOGGER.error("遇到不可重试的错误: %s", ex.code)
self._handle_error(ex)
break
2025-09-07 21:48:21 +08:00
except Exception as ex:
last_exception = ex
_LOGGER.warning("%s失败 (尝试 %d/%d): %s", operation_name, attempt + 1, self.retry_config.max_attempts, ex)
2025-09-07 21:48:21 +08:00
if last_exception:
if isinstance(last_exception, TencentCloudSDKException):
self._handle_error(last_exception)
else:
_LOGGER.error("%s最终失败: %s", operation_name, last_exception)
2025-09-07 21:48:21 +08:00
log_and_raise_error(
RuntimeError,
f"{operation_name}失败: {str(last_exception)}",
{"error": str(last_exception), "error_type": type(last_exception).__name__}
)
2025-09-07 21:48:21 +08:00
def _calculate_retry_delay(self, attempt: int) -> float:
"""计算重试延迟时间"""
delay = self.retry_config.initial_delay * (
self.retry_config.backoff_factor ** (attempt - 1)
)
2025-09-07 21:48:21 +08:00
delay = min(delay, self.retry_config.max_delay)
2025-09-07 21:48:21 +08:00
jitter = delay * self.retry_config.jitter
delay = delay + random.uniform(-jitter, jitter)
2025-09-07 21:48:21 +08:00
return max(0, delay)
2025-09-07 21:48:21 +08:00
def _should_retry(self, ex: TencentCloudSDKException) -> bool:
"""判断是否应该重试"""
if ex.code in NON_RETRYABLE_ERRORS:
return False
2025-09-07 21:48:21 +08:00
if ex.code in RATE_LIMIT_ERRORS:
return True
2025-09-07 21:48:21 +08:00
if ex.code in NETWORK_ERRORS:
return True
2025-09-07 21:48:21 +08:00
return True
2025-09-07 21:48:21 +08:00
def _is_valid_url(self, url: str) -> bool:
"""验证URL格式是否有效"""
url_pattern = re.compile(
r'^https?://'
r'(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+[A-Z]{2,6}\.?|'
r'localhost|'
r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})'
r'(?::\d+)?'
2025-09-07 21:48:21 +08:00
r'(?:/?|[/?]\S+)$', re.IGNORECASE)
return url_pattern.match(url) is not None
2025-09-07 21:48:21 +08:00
def _process_image_data(self, image_data: bytes) -> bytes:
"""处理图片数据,包括压缩和格式转换"""
try:
if len(image_data) > MAX_IMAGE_SIZE:
_LOGGER.warning("图片大小超过限制,尝试压缩: %d 字节", len(image_data))
image_data = self._compress_image(image_data)
2025-09-07 21:48:21 +08:00
try:
from PIL import Image
2025-09-07 21:48:21 +08:00
img = Image.open(io.BytesIO(image_data))
2025-09-07 21:48:21 +08:00
width, height = img.size
if width > MAX_IMAGE_DIMENSION or height > MAX_IMAGE_DIMENSION:
_LOGGER.warning("图片尺寸过大,尝试缩放: %dx%d", width, height)
2025-09-07 21:48:21 +08:00
scale = min(MAX_IMAGE_DIMENSION / width, MAX_IMAGE_DIMENSION / height)
new_width = int(width * scale)
new_height = int(height * scale)
2025-09-07 21:48:21 +08:00
img = img.resize((new_width, new_height), Image.LANCZOS)
2025-09-07 21:48:21 +08:00
output = io.BytesIO()
img.save(output, format='JPEG', quality=IMAGE_COMPRESSION_QUALITY)
image_data = output.getvalue()
2025-09-07 21:48:21 +08:00
_LOGGER.info("图片缩放完成: %dx%d -> %dx%d", width, height, new_width, new_height)
2025-09-07 21:48:21 +08:00
if img.format != 'JPEG':
_LOGGER.debug("转换图片格式为JPEG: %s", img.format)
output = io.BytesIO()
if img.mode == 'RGBA':
img = img.convert('RGB')
img.save(output, format='JPEG', quality=IMAGE_COMPRESSION_QUALITY)
image_data = output.getvalue()
2025-09-07 21:48:21 +08:00
except ImportError:
_LOGGER.warning("PIL库(Pillow)未安装,无法进行图片处理。建议安装: pip install Pillow>=10.0.0")
2025-09-07 21:48:21 +08:00
except Exception as ex:
_LOGGER.warning("图片处理失败: %s", ex)
2025-09-07 21:48:21 +08:00
return image_data
2025-09-07 21:48:21 +08:00
except Exception as ex:
_LOGGER.error("处理图片数据失败: %s", ex)
2025-09-07 21:48:21 +08:00
return image_data
2025-09-07 21:48:21 +08:00
def _compress_image(self, image_data: bytes) -> bytes:
"""压缩图片数据"""
try:
from PIL import Image
2025-09-07 21:48:21 +08:00
img = Image.open(io.BytesIO(image_data))
2025-09-07 21:48:21 +08:00
original_size = len(image_data)
target_size = MAX_IMAGE_SIZE * 0.8
2025-09-07 21:48:21 +08:00
quality = IMAGE_COMPRESSION_QUALITY
2025-09-07 21:48:21 +08:00
while quality > 30 and original_size > target_size:
output = io.BytesIO()
img.save(output, format='JPEG', quality=quality)
compressed_data = output.getvalue()
2025-09-07 21:48:21 +08:00
if len(compressed_data) <= target_size:
break
2025-09-07 21:48:21 +08:00
quality -= 5
original_size = len(compressed_data)
2025-09-07 21:48:21 +08:00
_LOGGER.info("图片压缩完成: %d -> %d 字节 (质量: %d)",
len(image_data), len(compressed_data), quality)
2025-09-07 21:48:21 +08:00
return compressed_data
2025-09-07 21:48:21 +08:00
except ImportError:
_LOGGER.warning("PIL库(Pillow)未安装,无法进行图片压缩。建议安装: pip install Pillow>=10.0.0")
2025-09-07 21:48:21 +08:00
return image_data
except Exception as ex:
_LOGGER.error("图片压缩失败: %s", ex)
2025-09-07 21:48:21 +08:00
return image_data
def _log_request(self, operation: str, params: Dict[str, Any], request_id: str) -> None:
2025-09-07 21:48:21 +08:00
"""记录API请求日志"""
if not ENABLE_REQUEST_LOGGING:
return
_LOGGER.debug(
2025-09-07 21:48:21 +08:00
"API请求: operation=%s, request_id=%s, params_count=%d",
operation, request_id, len(params)
)
2025-09-07 21:48:21 +08:00
if ENABLE_PERFORMANCE_LOGGING:
safe_params = {}
for key, value in params.items():
if key == "Image":
safe_params[key] = f"<base64_image_{len(str(value))}_chars>"
else:
safe_params[key] = value
2025-09-07 21:48:21 +08:00
params_str = json.dumps(safe_params, ensure_ascii=False)
if len(params_str) > LOG_REQUEST_PAYLOAD_MAX_SIZE:
params_str = params_str[:LOG_REQUEST_PAYLOAD_MAX_SIZE] + "...[truncated]"
2025-09-07 21:48:21 +08:00
_LOGGER.debug(
"API请求详情: operation=%s, request_id=%s, params=%s",
operation, request_id, params_str
)
2025-09-07 21:48:21 +08:00
def _log_response(self, operation: str, request_id: str, response: Any, duration: float) -> None:
"""记录API响应日志"""
if not ENABLE_RESPONSE_LOGGING:
return
_LOGGER.debug(
2025-09-07 21:48:21 +08:00
"API响应: operation=%s, request_id=%s, duration=%.3fs, response_type=%s",
operation, request_id, duration, type(response).__name__
)
2025-09-07 21:48:21 +08:00
if ENABLE_PERFORMANCE_LOGGING:
if duration > 5.0:
_LOGGER.warning(
"API响应缓慢: operation=%s, request_id=%s, duration=%.3fs",
operation, request_id, duration
)
elif duration > 2.0:
_LOGGER.info(
"API响应较慢: operation=%s, request_id=%s, duration=%.3fs",
operation, request_id, duration
)
2025-09-07 21:48:21 +08:00
try:
if hasattr(response, "__dict__"):
response_dict = {}
for attr in dir(response):
if not attr.startswith("_") and not callable(getattr(response, attr)):
response_dict[attr] = getattr(response, attr)
2025-09-07 21:48:21 +08:00
response_str = json.dumps(response_dict, ensure_ascii=False, default=str)
else:
response_str = str(response)
2025-09-07 21:48:21 +08:00
if len(response_str) > LOG_RESPONSE_PAYLOAD_MAX_SIZE:
response_str = response_str[:LOG_RESPONSE_PAYLOAD_MAX_SIZE] + "...[truncated]"
2025-09-07 21:48:21 +08:00
_LOGGER.debug(
"API响应详情: operation=%s, request_id=%s, response=%s",
operation, request_id, response_str
)
except Exception as ex:
_LOGGER.debug(
"无法记录API响应详情: operation=%s, request_id=%s, error=%s",
operation, request_id, ex
)
2025-09-07 21:48:21 +08:00
def _log_error(self, operation: str, request_id: str, error: Exception, duration: float) -> None:
"""记录API错误日志"""
error_type = type(error).__name__
error_msg = self._sanitize_error(str(error))
2025-09-07 21:48:21 +08:00
_LOGGER.error(
"API错误: operation=%s, request_id=%s, duration=%.3fs, error_type=%s",
operation, request_id, duration, error_type
2025-09-07 21:48:21 +08:00
)
2025-09-07 21:48:21 +08:00
if isinstance(error, TencentCloudSDKException):
_LOGGER.error(
"腾讯云API错误详情: operation=%s, request_id=%s, code=%s",
operation, request_id, getattr(error, "code", "未知")
2025-09-07 21:48:21 +08:00
)
2025-09-07 21:48:21 +08:00
if _LOGGER.isEnabledFor(logging.DEBUG):
_LOGGER.debug(
"API错误堆栈跟踪: operation=%s, request_id=%s",
operation, request_id, exc_info=True
)