超分功能完成

This commit is contained in:
2026-07-16 15:02:03 +08:00
parent 51673453d8
commit 3db586cd60
49 changed files with 4423 additions and 167 deletions
@@ -0,0 +1,270 @@
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)