diff --git a/python-api/app/ai/providers/vidu_provider.py b/python-api/app/ai/providers/vidu_provider.py index cab5902..fccfbbf 100644 --- a/python-api/app/ai/providers/vidu_provider.py +++ b/python-api/app/ai/providers/vidu_provider.py @@ -24,8 +24,90 @@ from app.core.exceptions import PlatformError, PlatformErrorType logger = logging.getLogger(__name__) -def _map_vidu_error(status: int, message: str) -> PlatformError: - """把 Vidu HTTP 错误映射为标准 PlatformError""" +# Vidu 错误码分类 +_VIDU_AUDIT_ERROR_CODES = { + "TaskPromptPolicyViolation", + "AuditSubmitIllegal", + "CreationPolicyViolation", + "PhotoAuditNotPass", + "AuditFailed", + "ImageCheckBodyJointsFailed", + "ImageCheckFaceFailed", + "ImageObjectsUndetected", + "FaceDetectFailure", + "FaceDetectNotPass", + "NoFaceDetected", + "MultiFaceDetected", +} + +_VIDU_RETRYABLE_ERROR_CODES = { + "InternalServiceFailure", + "ModelUnavailable", + "Unknown", +} + +_VIDU_RATE_LIMIT_ERROR_CODES = { + "QuotaExceeded", + "TooManyRequests", + "SystemThrottling", + "OperationInProcess", +} + + +def _extract_vidu_error_code(message: str | None) -> str | None: + """从 Vidu 错误信息中提取错误码""" + if not message: + return None + # Vidu 错误码格式:"ErrorCode: 中文描述" + return message.split(":")[0].strip() or None + + +def _map_vidu_error( + status: int, + message: str, + *, + err_code: str | None = None, +) -> PlatformError: + """把 Vidu HTTP 错误映射为标准 PlatformError + + 优先根据 Vidu 业务错误码(err_code)判断类型,HTTP status 仅作为兜底。 + """ + raw_code = err_code or _extract_vidu_error_code(message) + + # 1. 内容安全/审核类:不可重试 + if raw_code in _VIDU_AUDIT_ERROR_CODES: + return PlatformError( + message=message, + platform="vidu", + retryable=False, + error_type=PlatformErrorType.CONTENT_VIOLATION, + status_code=status, + raw_code=raw_code, + ) + + # 2. 平台内部/模型不可用:可重试 + if raw_code in _VIDU_RETRYABLE_ERROR_CODES: + return PlatformError( + message=message, + platform="vidu", + retryable=True, + error_type=PlatformErrorType.SERVER_ERROR, + status_code=status, + raw_code=raw_code, + ) + + # 3. 限流类:可重试 + if raw_code in _VIDU_RATE_LIMIT_ERROR_CODES: + return PlatformError( + message=message, + platform="vidu", + retryable=True, + error_type=PlatformErrorType.RATE_LIMIT, + status_code=status, + raw_code=raw_code, + ) + + # 4. HTTP status 兜底 mapping = { 429: (PlatformErrorType.RATE_LIMIT, True), 401: (PlatformErrorType.AUTH_FAILED, False), @@ -43,6 +125,7 @@ def _map_vidu_error(status: int, message: str) -> PlatformError: retryable=retryable, error_type=error_type, status_code=status, + raw_code=raw_code, ) @@ -66,7 +149,9 @@ class ViduProvider: from app.core.platform_config import get_platform_config_loader platform_config = get_platform_config_loader().get_platform("vidu") - self.base_url = (platform_config.base_url if platform_config else "https://api.vidu.cn").rstrip("/") + self.base_url = ( + platform_config.base_url if platform_config else "https://api.vidu.cn" + ).rstrip("/") if not self.api_key: raise ValueError("Vidu API Key 未配置,请在 .env 中设置 VIDU_API_KEY") @@ -135,9 +220,12 @@ class ViduProvider: resp = await self.client.post(url, json=body, timeout=httpx.Timeout(120.0, connect=5.0)) data = resp.json() if resp.status_code != 200 or data.get("state") == "failed": - msg = data.get("err_code") or data.get("message") or f"HTTP {resp.status_code}" - logger.error(f"[Vidu TTS] 请求失败: url={url}, status={resp.status_code}, response={data}") - raise _map_vidu_error(resp.status_code, f"Vidu TTS error: {msg}") + err_code = data.get("err_code") or _extract_vidu_error_code(data.get("message")) + msg = err_code or data.get("message") or f"HTTP {resp.status_code}" + logger.error( + f"[Vidu TTS] 请求失败: url={url}, status={resp.status_code}, response={data}" + ) + raise _map_vidu_error(resp.status_code, f"Vidu TTS error: {msg}", err_code=err_code) return data except (httpx.NetworkError, httpx.TimeoutException) as e: logger.error(f"[Vidu TTS] 网络错误: {e}") @@ -182,9 +270,14 @@ class ViduProvider: resp = await self.client.post(url, json=body, timeout=httpx.Timeout(120.0, connect=5.0)) data = resp.json() if resp.status_code != 200 or data.get("state") == "failed": - msg = data.get("err_code") or data.get("message") or f"HTTP {resp.status_code}" - logger.error(f"[Vidu Clone] 请求失败: url={url}, status={resp.status_code}, response={data}") - raise _map_vidu_error(resp.status_code, f"Vidu clone error: {msg}") + err_code = data.get("err_code") or _extract_vidu_error_code(data.get("message")) + msg = err_code or data.get("message") or f"HTTP {resp.status_code}" + logger.error( + f"[Vidu Clone] 请求失败: url={url}, status={resp.status_code}, response={data}" + ) + raise _map_vidu_error( + resp.status_code, f"Vidu clone error: {msg}", err_code=err_code + ) return data except (httpx.NetworkError, httpx.TimeoutException) as e: logger.error(f"[Vidu Clone] 网络错误: {e}") @@ -238,9 +331,14 @@ class ViduProvider: resp = await self.client.post(url, json=body) data = resp.json() if resp.status_code != 200 or data.get("state") == "failed": - msg = data.get("err_code") or data.get("message") or f"HTTP {resp.status_code}" - logger.error(f"[Vidu LipSync] 请求失败: url={url}, status={resp.status_code}, response={data}") - raise _map_vidu_error(resp.status_code, f"Vidu lip-sync error: {msg}") + err_code = data.get("err_code") or _extract_vidu_error_code(data.get("message")) + msg = err_code or data.get("message") or f"HTTP {resp.status_code}" + logger.error( + f"[Vidu LipSync] 请求失败: url={url}, status={resp.status_code}, response={data}" + ) + raise _map_vidu_error( + resp.status_code, f"Vidu lip-sync error: {msg}", err_code=err_code + ) return data except (httpx.NetworkError, httpx.TimeoutException) as e: logger.error(f"[Vidu LipSync] 网络错误: {e}") @@ -264,9 +362,14 @@ class ViduProvider: resp = await self.client.get(url) data = resp.json() if resp.status_code != 200: - msg = data.get("err_code") or data.get("message") or f"HTTP {resp.status_code}" - logger.error(f"[Vidu Query] 请求失败: url={url}, status={resp.status_code}, response={data}") - raise _map_vidu_error(resp.status_code, f"Vidu query task error: {msg}") + err_code = data.get("err_code") or _extract_vidu_error_code(data.get("message")) + msg = err_code or data.get("message") or f"HTTP {resp.status_code}" + logger.error( + f"[Vidu Query] 请求失败: url={url}, status={resp.status_code}, response={data}" + ) + raise _map_vidu_error( + resp.status_code, f"Vidu query task error: {msg}", err_code=err_code + ) return data except (httpx.NetworkError, httpx.TimeoutException) as e: logger.error(f"[Vidu Query] 网络错误: {e}") diff --git a/python-api/app/ai/providers/volcengine_caption_provider.py b/python-api/app/ai/providers/volcengine_caption_provider.py index 0f2f271..09ddcc7 100644 --- a/python-api/app/ai/providers/volcengine_caption_provider.py +++ b/python-api/app/ai/providers/volcengine_caption_provider.py @@ -37,8 +37,10 @@ def _map_caption_error(status: int, message: str, code: int | None = None) -> Pl if code is not None and code in error_mapping: error_type, retryable = error_mapping[code] return PlatformError( - message, platform="volcengine_caption", - retryable=retryable, error_type=error_type, + message, + platform="volcengine_caption", + retryable=retryable, + error_type=error_type, status_code=status, ) @@ -53,8 +55,10 @@ def _map_caption_error(status: int, message: str, code: int | None = None) -> Pl } error_type, retryable = http_mapping.get(status, (PlatformErrorType.UNKNOWN, False)) return PlatformError( - message, platform="volcengine_caption", - retryable=retryable, error_type=error_type, + message, + platform="volcengine_caption", + retryable=retryable, + error_type=error_type, status_code=status, ) @@ -124,7 +128,7 @@ class VolcengineCaptionProvider: max_lines: int = 1, ) -> dict[str, Any]: """提交字幕生成任务,返回 {id: task_id}""" - params = { + params: dict[str, str | int] = { "appid": self.appid, "language": language, "caption_type": caption_type, @@ -150,11 +154,15 @@ class VolcengineCaptionProvider: except PlatformError: raise except httpx.HTTPStatusError as e: - raise _map_caption_error(e.response.status_code, f"HTTP错误: {e.response.status_code}") from e + raise _map_caption_error( + e.response.status_code, f"HTTP错误: {e.response.status_code}" + ) from e except (httpx.NetworkError, httpx.TimeoutException) as e: raise PlatformError( - f"字幕服务网络错误: {e}", platform="volcengine_caption", - retryable=True, error_type=PlatformErrorType.TIMEOUT, + f"字幕服务网络错误: {e}", + platform="volcengine_caption", + retryable=True, + error_type=PlatformErrorType.TIMEOUT, ) from e except Exception as e: raise _map_caption_error(500, f"提交任务失败: {str(e)}") from e @@ -165,7 +173,7 @@ class VolcengineCaptionProvider: blocking: bool = False, ) -> dict[str, Any]: """查询字幕任务结果,返回原始 JSON""" - params = { + params: dict[str, str | int] = { "appid": self.appid, "id": task_id, "blocking": 1 if blocking else 0, @@ -182,11 +190,15 @@ class VolcengineCaptionProvider: except PlatformError: raise except httpx.HTTPStatusError as e: - raise _map_caption_error(e.response.status_code, f"HTTP错误: {e.response.status_code}") from e + raise _map_caption_error( + e.response.status_code, f"HTTP错误: {e.response.status_code}" + ) from e except (httpx.NetworkError, httpx.TimeoutException) as e: raise PlatformError( - f"字幕服务网络错误: {e}", platform="volcengine_caption", - retryable=True, error_type=PlatformErrorType.TIMEOUT, + f"字幕服务网络错误: {e}", + platform="volcengine_caption", + retryable=True, + error_type=PlatformErrorType.TIMEOUT, ) from e except Exception as e: raise _map_caption_error(500, f"查询任务失败: {str(e)}") from e @@ -201,7 +213,7 @@ class VolcengineCaptionProvider: sta_punc_mode: int = 3, ) -> dict[str, Any]: """提交自动字幕打轴任务,返回 {id: task_id}""" - params = { + params: dict[str, str | int] = { "appid": self.appid, "caption_type": caption_type, "sta_punc_mode": sta_punc_mode, @@ -218,7 +230,9 @@ class VolcengineCaptionProvider: response.raise_for_status() data = response.json() if "id" not in data: - raise _map_caption_error(500, f"提交打轴任务失败: {data.get('message', '未知错误')}") + raise _map_caption_error( + 500, f"提交打轴任务失败: {data.get('message', '未知错误')}" + ) return data except PlatformError: raise diff --git a/python-api/app/ai/providers/volcengine_provider.py b/python-api/app/ai/providers/volcengine_provider.py index 0e2a5d5..9f029a0 100644 --- a/python-api/app/ai/providers/volcengine_provider.py +++ b/python-api/app/ai/providers/volcengine_provider.py @@ -291,27 +291,40 @@ class VolcengineProvider(LLMProvider): if status == 429 or "rate limit" in message.lower(): return PlatformError( - message, platform="volcengine_ark", retryable=True, - error_type=PlatformErrorType.RATE_LIMIT, status_code=status, + message, + platform="volcengine_ark", + retryable=True, + error_type=PlatformErrorType.RATE_LIMIT, + status_code=status, ) elif status in (401, 403) or "authentication" in message.lower(): return PlatformError( - message, platform="volcengine_ark", retryable=False, - error_type=PlatformErrorType.AUTH_FAILED, status_code=status, + message, + platform="volcengine_ark", + retryable=False, + error_type=PlatformErrorType.AUTH_FAILED, + status_code=status, ) elif status and status >= 500: return PlatformError( - message, platform="volcengine_ark", retryable=True, - error_type=PlatformErrorType.SERVER_ERROR, status_code=status, + message, + platform="volcengine_ark", + retryable=True, + error_type=PlatformErrorType.SERVER_ERROR, + status_code=status, ) elif "timeout" in message.lower() or isinstance(e, TimeoutError): return PlatformError( - message, platform="volcengine_ark", retryable=True, + message, + platform="volcengine_ark", + retryable=True, error_type=PlatformErrorType.TIMEOUT, ) else: return PlatformError( - message, platform="volcengine_ark", retryable=False, + message, + platform="volcengine_ark", + retryable=False, error_type=PlatformErrorType.UNKNOWN, )