Files
tencent_face_recognition/tencent_cloud_client.py
T
Hermes 18a6a8ca9b refactor: 全面重构腾讯云人脸识别插件
- 拆分 tencent_cloud_client.py(1311行)为 api_operations.py / image_processor.py / retry.py
- 更新 HA 废弃 API: SupportsResponse.OPTIONAL, async_unload_platforms, native_value, 移除 CONNECTION_CLASS
- 新增动态人员传感器管理(自动增/删)
- 新增 detect_face 服务定义(services.yaml)
- 移除 secret_key 持久化存储,增强安全性
- 完善错误映射前缀匹配 + 异常类型安全退避
- 修复 Coordinator 双重人员 ID 跟踪死代码
- 添加 _sanitize_error 脱敏,ImageCache LRU 缓存
- manifest.json 版本 2.1.0 + Pillow 依赖
2026-07-07 02:33:38 +08:00

349 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""腾讯云人脸识别客户端(精简门面)。
原 1311 行的客户端已拆分为:
- ``retry.py``: 重试配置与执行器
- ``image_processor.py``: 图片获取/压缩/编码
- ``api_operations.py``: 各 API 操作的实现(搜索/检测/属性/人员/人脸/库信息/列表)
本文件仅保留:凭据创建、HTTP Session 管理、错误脱敏、以及对外暴露的
``TencentCloudClient`` 门面,委托给上述模块。
"""
from __future__ import annotations
import logging
from typing import Any, Dict, List, Optional
import requests
from tencentcloud.common import credential
from tencentcloud.common.exception.tencent_cloud_sdk_exception import (
TencentCloudSDKException,
)
from tencentcloud.iai.v20200303 import iai_client
from . import api_operations
from .errors import (
ApiError,
AuthenticationError,
log_and_raise_error,
validate_config,
)
from .image_processor import ImageCache, get_image_base64
from .retry import RetryConfig
from .const import CONF_REGION, CONF_SECRET_ID, CONF_SECRET_KEY
_LOGGER = logging.getLogger(__name__)
class TencentCloudClient:
"""腾讯云人脸识别客户端门面。"""
def __init__(
self,
secret_id: str,
secret_key: str,
region: str = "ap-shanghai",
retry_config: Optional[RetryConfig] = None,
) -> None:
validate_config({
CONF_SECRET_ID: secret_id,
CONF_SECRET_KEY: secret_key,
CONF_REGION: region,
})
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()
# HTTP 连接池化
self._session = requests.Session()
self._session.mount(
"http://", requests.adapters.HTTPAdapter(max_retries=2)
)
self._session.mount(
"https://", requests.adapters.HTTPAdapter(max_retries=2)
)
# 图片缓存(url/path),通过 session 下载
self._image_cache = ImageCache(
maxsize=32,
download_fn=lambda url: get_image_base64(
image_url=url, session=self._session
),
read_fn=None,
)
# --- 资源管理 --------------------------------------------------------
def close(self) -> None:
"""释放 HTTP Session 等资源。"""
if self._session:
self._session.close()
self._image_cache.clear()
# --- 客户端创建 -------------------------------------------------------
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)))
log_and_raise_error(
AuthenticationError,
"创建腾讯云客户端失败",
{"region": self.region, "error_type": type(ex).__name__},
)
# --- 凭据验证 ---------------------------------------------------------
def verify_credentials(self) -> bool:
"""验证凭据(轻量 GetGroupList)。失败抛出对应错误。"""
return api_operations.verify_credentials(
self.client, retry_config=self.retry_config
)
def verify_credentials_lightweight(self) -> bool:
"""测试 API 连接(轻量验证)。失败返回 False,不抛异常。"""
try:
self.verify_credentials()
return True
except Exception as ex: # noqa: BLE001
_LOGGER.error("API连接测试失败: %s", self._sanitize_error(str(ex)))
return False
def test_api_connection(self, group_id: str) -> bool:
"""测试与指定人员库的连接。失败返回 False。"""
try:
_LOGGER.info("开始测试API连接: group_id=%s", group_id)
result = self.get_group_info(group_id)
if result.get("success", False):
_LOGGER.info("API连接测试成功")
return True
_LOGGER.error("API连接测试失败: %s", result.get("error_message"))
return False
except Exception as ex: # noqa: BLE001
_LOGGER.error("API连接测试失败: %s", self._sanitize_error(str(ex)))
return False
# 兼容旧调用名
_test_api_connection = test_api_connection
# --- 图片 ------------------------------------------------------------
def _get_image_base64(
self,
image_url: Optional[str] = None,
image_path: Optional[str] = None,
image_file: Optional[Any] = None,
) -> str:
"""获取图片 base64(带 url/path 缓存)。"""
if image_url:
return self._image_cache.get_url(image_url)
if image_path:
return self._image_cache.get_path(image_path)
if image_file:
return self._image_cache.get(image_file=image_file)
log_and_raise_error(
ApiError,
"必须提供 image_url、image_path 或 image_file",
{"reason": "no_image_source"},
)
# --- 业务 API(委托 api_operations ----------------------------------
def search_faces(
self,
image_url: Optional[str] = None,
image_path: Optional[str] = None,
image_file: Optional[Any] = None,
image_base64: Optional[str] = None,
group_ids: Optional[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,
) -> Dict[str, Any]:
if image_base64 is None:
image_base64 = self._get_image_base64(image_url, image_path, image_file)
if not group_ids:
_LOGGER.warning("未提供人员库ID,搜索可能失败")
return api_operations.search_faces(
self.client,
image_base64=image_base64,
group_ids=group_ids or [],
max_face_num=max_face_num,
min_face_size=min_face_size,
max_user_num=max_user_num,
quality_control=quality_control,
need_rotate_check=need_rotate_check,
face_match_threshold=face_match_threshold,
retry_config=self.retry_config,
sanitize=self._sanitize_error,
)
def detect_faces(
self,
image_url: Optional[str] = None,
image_path: Optional[str] = None,
image_file: Optional[Any] = None,
image_base64: Optional[str] = None,
max_face_num: int = 1,
min_face_size: int = 34,
need_rotate_check: int = 1,
) -> Dict[str, Any]:
if image_base64 is None:
image_base64 = self._get_image_base64(image_url, image_path, image_file)
return api_operations.detect_faces(
self.client,
image_base64=image_base64,
max_face_num=max_face_num,
min_face_size=min_face_size,
need_rotate_check=need_rotate_check,
retry_config=self.retry_config,
sanitize=self._sanitize_error,
)
def get_face_attributes(
self,
image_url: Optional[str] = None,
image_path: Optional[str] = None,
image_file: Optional[Any] = None,
image_base64: Optional[str] = None,
max_face_num: int = 1,
need_rotate_check: int = 1,
) -> Dict[str, Any]:
if image_base64 is None:
image_base64 = self._get_image_base64(image_url, image_path, image_file)
return api_operations.get_face_attributes(
self.client,
image_base64=image_base64,
max_face_num=max_face_num,
need_rotate_check=need_rotate_check,
retry_config=self.retry_config,
sanitize=self._sanitize_error,
)
def create_person(
self,
person_id: str,
person_name: str,
group_id: str,
image_url: Optional[str] = None,
image_path: Optional[str] = None,
image_file: Optional[Any] = None,
image_base64: Optional[str] = None,
gender: Optional[int] = None,
person_tag: Optional[str] = None,
quality_control: int = 1,
need_rotate_check: int = 1,
) -> Dict[str, Any]:
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)
return api_operations.create_person(
self.client,
person_id=person_id,
person_name=person_name,
group_id=group_id,
image_base64=image_base64,
gender=gender,
person_tag=person_tag,
quality_control=quality_control,
need_rotate_check=need_rotate_check,
retry_config=self.retry_config,
sanitize=self._sanitize_error,
)
def delete_person(self, person_id: str) -> Dict[str, Any]:
return api_operations.delete_person(
self.client,
person_id=person_id,
retry_config=self.retry_config,
sanitize=self._sanitize_error,
)
def create_face(
self,
person_id: str,
image_url: Optional[str] = None,
image_path: Optional[str] = None,
image_file: Optional[Any] = None,
image_base64: Optional[str] = None,
quality_control: int = 1,
need_rotate_check: int = 1,
) -> Dict[str, Any]:
if image_base64 is None:
image_base64 = self._get_image_base64(image_url, image_path, image_file)
return api_operations.create_face(
self.client,
person_id=person_id,
image_base64=image_base64,
quality_control=quality_control,
need_rotate_check=need_rotate_check,
retry_config=self.retry_config,
sanitize=self._sanitize_error,
)
def delete_face(self, person_id: str, face_id: str) -> Dict[str, Any]:
return api_operations.delete_face(
self.client,
person_id=person_id,
face_id=face_id,
retry_config=self.retry_config,
sanitize=self._sanitize_error,
)
def get_group_info(self, group_id: str) -> Dict[str, Any]:
return api_operations.get_group_info(
self.client,
group_id=group_id,
retry_config=self.retry_config,
sanitize=self._sanitize_error,
)
def get_person_list(
self, group_id: str, limit: int = 100, offset: int = 0
) -> Dict[str, Any]:
return api_operations.get_person_list(
self.client,
group_id=group_id,
limit=limit,
offset=offset,
retry_config=self.retry_config,
sanitize=self._sanitize_error,
)
def get_person_list_all(self, group_id: str) -> Dict[str, Any]:
return api_operations.get_person_list_all(
self.client,
group_id=group_id,
retry_config=self.retry_config,
sanitize=self._sanitize_error,
)
# --- 辅助 ------------------------------------------------------------
def _sanitize_error(self, error_msg: str) -> str:
"""脱敏错误信息中的凭据。"""
if not isinstance(error_msg, str):
return str(error_msg)
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
# 兼容旧代码:暴露 TencentCloudSDKException 别名
@staticmethod
def is_sdk_exception(ex: Exception) -> bool:
return isinstance(ex, TencentCloudSDKException)