271 lines
9.6 KiB
Python
271 lines
9.6 KiB
Python
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)
|