from __future__ import annotations from dataclasses import dataclass from typing import Any import httpx from app.config import settings from app.enums.video_upscale import VideoUpscaleProcessorKey class VolcMediaKitError(RuntimeError): def __init__( self, message: str, *, code: str | None = None, error_type: str | None = None, param: str | None = None, retryable: bool = True, http_status: int | None = None, request_id: str | None = None, endpoint: str | None = None, response_payload: dict[str, Any] | None = None, ): super().__init__(message) self.code = code self.error_type = error_type self.param = param self.retryable = retryable self.http_status = http_status self.request_id = request_id self.endpoint = endpoint self.response_payload = response_payload or {} def log_detail(self) -> dict[str, Any]: return { "endpoint": self.endpoint, "http_status": self.http_status, "request_id": self.request_id, "error_code": self.code, "error_type": self.error_type, "error_param": self.param, "error_message": str(self), "retryable": self.retryable, } @dataclass(slots=True) class VolcSubmitResult: task_id: str request_id: str | None request_payload: dict[str, Any] response_payload: dict[str, Any] endpoint: str @dataclass(slots=True) class VolcQueryResult: status: str request_id: str | None result: dict[str, Any] | None error: dict[str, Any] | None expires_at: int | None response_payload: dict[str, Any] endpoint: str def _headers() -> dict[str, str]: api_key = str(settings.VOLC_API_KEY or "").strip() if not api_key: raise VolcMediaKitError("VOLC_API_KEY 未配置", code="MissingApiKey", retryable=False) return {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"} def _base_url() -> str: return str(settings.VOLC_MEDIAKIT_API_BASE or "https://mediakit.cn-beijing.volces.com").rstrip("/") def _error_from_payload( payload: dict[str, Any], default_message: str, *, http_status: int | None = None, endpoint: str | None = None, ) -> VolcMediaKitError: error = payload.get("error") if isinstance(payload.get("error"), dict) else {} code = str(error.get("code") or "") or None error_type = str(error.get("type") or "") or None param = str(error.get("param") or "") or None message = str(error.get("message") or default_message) retryable = True if http_status in {400, 401, 403, 404, 422}: retryable = False if code in {"InvalidParameter", "Unauthorized", "Forbidden", "NotFound"} or error_type in {"BadRequest", "AuthError"}: retryable = False request_id = str(payload.get("request_id") or "") or None return VolcMediaKitError( message, code=code, error_type=error_type, param=param, retryable=retryable, http_status=http_status, request_id=request_id, endpoint=endpoint, response_payload=payload, ) def build_submit_payload( *, processor_key: str, video_url: str, target_resolution: str, target_width: int, target_height: int, processor: dict[str, Any], client_token: str, ) -> tuple[str, dict[str, Any]]: payload: dict[str, Any] = { "video_url": video_url, "bitrate_level": processor.get("bitrate_level") or "medium", "client_token": client_token[:64], } if processor_key == VideoUpscaleProcessorKey.VOLC_LARGE_MODEL_V1.value: endpoint = "/api/v1/tools/enhance-video-generative" normalized = str(target_resolution or "").strip().lower() if normalized not in {"720p", "1080p", "2k"}: raise VolcMediaKitError( "画质增强大模型仅支持目标分辨率 720p、1080p、2K", code="InvalidResolution", retryable=False, endpoint=endpoint, ) payload["resolution"] = normalized elif processor_key in { VideoUpscaleProcessorKey.VOLC_STANDARD_V1.value, VideoUpscaleProcessorKey.VOLC_PROFESSIONAL_V1.value, }: endpoint = "/api/v1/tools/enhance-video" payload["tool_version"] = "standard" if processor_key == VideoUpscaleProcessorKey.VOLC_STANDARD_V1.value else "professional" payload["resolution_limit"] = min(int(target_width), int(target_height)) if processor_key == VideoUpscaleProcessorKey.VOLC_STANDARD_V1.value: payload["scene"] = processor.get("scene") or "aigc" else: raise VolcMediaKitError( f"不支持的火山超分处理器: {processor_key}", code="UnsupportedProcessor", retryable=False, ) return endpoint, payload async def submit_video_enhance( *, processor_key: str, video_url: str, target_resolution: str, target_width: int, target_height: int, processor: dict[str, Any], client_token: str, ) -> VolcSubmitResult: endpoint, payload = build_submit_payload( processor_key=processor_key, video_url=video_url, target_resolution=target_resolution, target_width=target_width, target_height=target_height, processor=processor, client_token=client_token, ) timeout = max(3, int(processor.get("request_timeout_seconds") or settings.VIDEO_UPSCALE_REMOTE_REQUEST_TIMEOUT_SECONDS)) try: async with httpx.AsyncClient(timeout=timeout, follow_redirects=True) as client: response = await client.post(f"{_base_url()}{endpoint}", headers=_headers(), json=payload) try: data = response.json() except Exception: data = {"success": False, "error": {"message": response.text[:2000]}} if response.status_code >= 400: raise _error_from_payload( data, f"火山超分提交失败 HTTP {response.status_code}", http_status=response.status_code, endpoint=endpoint, ) except VolcMediaKitError: raise except (httpx.TimeoutException, httpx.NetworkError) as exc: raise VolcMediaKitError( f"火山超分提交网络异常: {exc}", code="NetworkError", retryable=True, endpoint=endpoint, ) from exc if not bool(data.get("success")) or not data.get("task_id"): raise _error_from_payload(data, "火山超分提交失败", endpoint=endpoint) return VolcSubmitResult( task_id=str(data["task_id"]), request_id=str(data.get("request_id")) if data.get("request_id") else None, request_payload=payload, response_payload=data, endpoint=endpoint, ) async def query_task(task_id: str, *, request_timeout_seconds: int | None = None) -> VolcQueryResult: endpoint = f"/api/v1/tasks/{task_id}" timeout = max(3, int(request_timeout_seconds or settings.VIDEO_UPSCALE_REMOTE_REQUEST_TIMEOUT_SECONDS)) try: async with httpx.AsyncClient(timeout=timeout, follow_redirects=True) as client: response = await client.get(f"{_base_url()}{endpoint}", headers=_headers()) try: data = response.json() except Exception: data = {"success": False, "error": {"message": response.text[:2000]}} if response.status_code >= 400: raise _error_from_payload( data, f"火山超分任务查询失败 HTTP {response.status_code}", http_status=response.status_code, endpoint=endpoint, ) except VolcMediaKitError: raise except (httpx.TimeoutException, httpx.NetworkError) as exc: raise VolcMediaKitError( f"火山超分查询网络异常: {exc}", code="NetworkError", retryable=True, endpoint=endpoint, ) from exc if not bool(data.get("success")): raise _error_from_payload(data, "火山超分任务查询失败", endpoint=endpoint) status = str(data.get("status") or "").strip().lower() if status not in {"running", "completed", "failed"}: raise VolcMediaKitError( f"火山超分返回未知任务状态: {status}", code="UnknownStatus", retryable=True, request_id=str(data.get("request_id") or "") or None, endpoint=endpoint, response_payload=data, ) expires_raw = data.get("expires_at") try: expires_at = int(expires_raw) if expires_raw is not None else None except Exception: expires_at = None return VolcQueryResult( status=status, request_id=str(data.get("request_id")) if data.get("request_id") else None, result=data.get("result") if isinstance(data.get("result"), dict) else None, error=data.get("error") if isinstance(data.get("error"), dict) else None, expires_at=expires_at, response_payload=data, endpoint=endpoint, ) def is_remote_input_access_error(error: dict[str, Any] | None) -> bool: if not error: return False text = " ".join(str(error.get(key) or "") for key in ("code", "message", "param", "type")).lower() fragments = ("403", "forbidden", "url expired", "downloadfailed", "urldownloadfail", "download file", "url无法", "下载失败") return any(fragment in text for fragment in fragments)