from __future__ import annotations from dataclasses import dataclass import time import uuid from typing import Any import httpx from app.config import settings from app.enums.video_upscale import VideoUpscaleProcessorKey from app.services.operation_log_service import log_ai_model_event 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, ) call_id = uuid.uuid4().hex started = time.perf_counter() log_ai_model_event( event_type="REQUEST", event_phase="REQUEST", event_status="started", module="video_upscale", step_code="provider_submit", call_id=call_id, source="app.services.video_upscale.volc_service", remote_action=endpoint, provider="volcengine_mediakit", api_base=_base_url(), request=payload, ) timeout = max(3, int(processor.get("request_timeout_seconds") or settings.VIDEO_UPSCALE_REMOTE_REQUEST_TIMEOUT_SECONDS)) response: httpx.Response | None = None data: dict[str, Any] = {} 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]}} log_ai_model_event( event_type="RESPONSE", event_phase="RESPONSE", event_status="success" if response.status_code < 400 else "failed", module="video_upscale", step_code="provider_submit", call_id=call_id, source="app.services.video_upscale.volc_service", remote_action=endpoint, remote_request_id=str(data.get("request_id") or "") or None, provider="volcengine_mediakit", api_base=_base_url(), http_status=response.status_code, latency_ms=int((time.perf_counter() - started) * 1000), response=data, error=None if response.status_code < 400 else str((data.get("error") or {}).get("message") or "remote error"), ) if response.status_code >= 400: raise _error_from_payload( data, f"火山超分提交失败 HTTP {response.status_code}", http_status=response.status_code, endpoint=endpoint, ) if not bool(data.get("success")) or not data.get("task_id"): raise _error_from_payload(data, "火山超分提交失败", endpoint=endpoint) except VolcMediaKitError as exc: log_ai_model_event( event_type="ERROR", event_phase="ERROR", event_status="failed", module="video_upscale", step_code="provider_submit", call_id=call_id, source="app.services.video_upscale.volc_service", remote_action=endpoint, remote_request_id=exc.request_id, provider="volcengine_mediakit", api_base=_base_url(), http_status=exc.http_status or (response.status_code if response is not None else None), latency_ms=int((time.perf_counter() - started) * 1000), detail=exc.log_detail(), error=str(exc), ) raise except (httpx.TimeoutException, httpx.NetworkError) as exc: wrapped = VolcMediaKitError( f"火山超分提交网络异常: {exc}", code="NetworkError", retryable=True, endpoint=endpoint, ) log_ai_model_event( event_type="ERROR", event_phase="ERROR", event_status="failed", module="video_upscale", step_code="provider_submit", call_id=call_id, source="app.services.video_upscale.volc_service", remote_action=endpoint, provider="volcengine_mediakit", api_base=_base_url(), latency_ms=int((time.perf_counter() - started) * 1000), detail=wrapped.log_detail(), error=str(wrapped), ) raise wrapped from exc 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}" call_id = uuid.uuid4().hex started = time.perf_counter() request_payload = {"task_id": task_id} log_ai_model_event( event_type="REQUEST", event_phase="REQUEST", event_status="started", module="video_upscale", step_code="provider_poll", call_id=call_id, source="app.services.video_upscale.volc_service", remote_action=endpoint, remote_request_id=task_id, provider="volcengine_mediakit", api_base=_base_url(), request=request_payload, ) timeout = max(3, int(request_timeout_seconds or settings.VIDEO_UPSCALE_REMOTE_REQUEST_TIMEOUT_SECONDS)) response: httpx.Response | None = None data: dict[str, Any] = {} 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]}} log_ai_model_event( event_type="RESPONSE", event_phase="RESPONSE", event_status="success" if response.status_code < 400 else "failed", module="video_upscale", step_code="provider_poll", call_id=call_id, source="app.services.video_upscale.volc_service", remote_action=endpoint, remote_request_id=str(data.get("request_id") or task_id), provider="volcengine_mediakit", api_base=_base_url(), http_status=response.status_code, latency_ms=int((time.perf_counter() - started) * 1000), response=data, error=None if response.status_code < 400 else str((data.get("error") or {}).get("message") or "remote error"), ) if response.status_code >= 400: raise _error_from_payload( data, f"火山超分任务查询失败 HTTP {response.status_code}", http_status=response.status_code, endpoint=endpoint, ) 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, ) except VolcMediaKitError as exc: log_ai_model_event( event_type="ERROR", event_phase="ERROR", event_status="failed", module="video_upscale", step_code="provider_poll", call_id=call_id, source="app.services.video_upscale.volc_service", remote_action=endpoint, remote_request_id=exc.request_id or task_id, provider="volcengine_mediakit", api_base=_base_url(), http_status=exc.http_status or (response.status_code if response is not None else None), latency_ms=int((time.perf_counter() - started) * 1000), detail=exc.log_detail(), error=str(exc), ) raise except (httpx.TimeoutException, httpx.NetworkError) as exc: wrapped = VolcMediaKitError( f"火山超分查询网络异常: {exc}", code="NetworkError", retryable=True, endpoint=endpoint, ) log_ai_model_event( event_type="ERROR", event_phase="ERROR", event_status="failed", module="video_upscale", step_code="provider_poll", call_id=call_id, source="app.services.video_upscale.volc_service", remote_action=endpoint, remote_request_id=task_id, provider="volcengine_mediakit", api_base=_base_url(), latency_ms=int((time.perf_counter() - started) * 1000), detail=wrapped.log_detail(), error=str(wrapped), ) raise wrapped from exc 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)