177 lines
7.0 KiB
Python
177 lines
7.0 KiB
Python
from __future__ import annotations
|
|
|
|
import hashlib
|
|
import json
|
|
from datetime import datetime, timezone
|
|
from typing import Any, Iterable
|
|
|
|
from fastapi import HTTPException
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from app.config import settings
|
|
from app.enums.video_upscale import (
|
|
ALL_PROCESSOR_KEYS,
|
|
VIDEO_UPSCALE_RESOLUTIONS,
|
|
VideoUpscaleProcessorKey,
|
|
normalize_video_upscale_resolution,
|
|
video_upscale_short_edge_pixels,
|
|
)
|
|
from app.services.video_upscale.config_service import get_runtime_video_upscale_config
|
|
from app.services.video_upscale.log_service import log_video_upscale_event
|
|
|
|
|
|
def normalize_resolution(value: str | None) -> str:
|
|
return normalize_video_upscale_resolution(value)
|
|
|
|
|
|
def _even(value: float) -> int:
|
|
rounded = int(round(value))
|
|
if rounded < 2:
|
|
rounded = 2
|
|
return rounded if rounded % 2 == 0 else rounded + 1
|
|
|
|
|
|
def parse_aspect_ratio(value: str | None) -> tuple[int, int]:
|
|
text = str(value or "").strip()
|
|
try:
|
|
left, right = text.split(":", 1)
|
|
width = int(left)
|
|
height = int(right)
|
|
except Exception as exc:
|
|
raise HTTPException(status_code=400, detail=f"视频比例格式不合法: {text}") from exc
|
|
if width <= 0 or height <= 0:
|
|
raise HTTPException(status_code=400, detail=f"视频比例格式不合法: {text}")
|
|
return width, height
|
|
|
|
|
|
def calculate_target_dimensions(*, aspect_ratio: str, target_resolution: str) -> tuple[int, int]:
|
|
ratio_w, ratio_h = parse_aspect_ratio(aspect_ratio)
|
|
try:
|
|
pixels = video_upscale_short_edge_pixels(target_resolution)
|
|
except ValueError as exc:
|
|
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
|
|
|
if ratio_w >= ratio_h:
|
|
height = _even(pixels)
|
|
width = _even(height * ratio_w / ratio_h)
|
|
else:
|
|
width = _even(pixels)
|
|
height = _even(width * ratio_h / ratio_w)
|
|
return width, height
|
|
|
|
|
|
def _runtime_processor_snapshot(processor_key: str) -> dict[str, Any]:
|
|
if processor_key not in ALL_PROCESSOR_KEYS:
|
|
raise HTTPException(status_code=500, detail=f"未注册的超分处理器: {processor_key}")
|
|
is_local = processor_key == VideoUpscaleProcessorKey.LOCAL_FFMPEG_CROP_V1.value
|
|
return {
|
|
"max_attempts": max(1, int(settings.VIDEO_UPSCALE_MAX_ATTEMPTS or 3)),
|
|
"timeout_seconds": int(
|
|
settings.VIDEO_UPSCALE_LOCAL_TIMEOUT_SECONDS
|
|
if is_local
|
|
else settings.VIDEO_UPSCALE_REMOTE_POLL_TIMEOUT_SECONDS
|
|
),
|
|
"request_timeout_seconds": max(3, int(settings.VIDEO_UPSCALE_REMOTE_REQUEST_TIMEOUT_SECONDS or 30)),
|
|
"poll_timeout_seconds": max(60, int(settings.VIDEO_UPSCALE_REMOTE_POLL_TIMEOUT_SECONDS or 7200)),
|
|
"poll_interval_seconds": max(5, int(settings.VIDEO_UPSCALE_REMOTE_POLL_INTERVAL_SECONDS or 30)),
|
|
"source_url_expire_seconds": max(600, int(settings.VIDEO_UPSCALE_LOCAL_SIGNED_URL_EXPIRE_SECONDS or 7200)),
|
|
"bitrate_level": "medium",
|
|
"scene": "aigc",
|
|
"fps": None,
|
|
"queue_id": None,
|
|
}
|
|
|
|
|
|
async def build_video_upscale_snapshot(
|
|
db: AsyncSession,
|
|
*,
|
|
target_resolution: str,
|
|
aspect_ratio: str,
|
|
supported_provider_resolutions: Iterable[str] | None = None,
|
|
) -> tuple[str, bool, str | None]:
|
|
config = await get_runtime_video_upscale_config(db)
|
|
original_resolution = normalize_resolution(target_resolution)
|
|
if original_resolution not in VIDEO_UPSCALE_RESOLUTIONS:
|
|
return str(target_resolution).strip(), False, None
|
|
if not config.get("enabled"):
|
|
log_video_upscale_event(
|
|
event_type="upscale_snapshot_bypassed",
|
|
event_status="bypassed",
|
|
detail={"reason": "global_disabled", "target_resolution": original_resolution, "aspect_ratio": aspect_ratio},
|
|
)
|
|
return original_resolution, False, None
|
|
|
|
matched: dict[str, Any] | None = None
|
|
for rule in config.get("rules") or []:
|
|
if not rule.get("enabled"):
|
|
continue
|
|
if normalize_resolution(rule.get("target_resolution")) == original_resolution:
|
|
matched = dict(rule)
|
|
break
|
|
if matched is None:
|
|
log_video_upscale_event(
|
|
event_type="upscale_snapshot_bypassed",
|
|
event_status="bypassed",
|
|
detail={"reason": "rule_not_matched", "target_resolution": original_resolution, "aspect_ratio": aspect_ratio},
|
|
)
|
|
return original_resolution, False, None
|
|
|
|
provider_resolution = normalize_resolution(matched.get("provider_generation_resolution"))
|
|
processor_key = str(matched.get("processor_key") or "").strip()
|
|
processor = _runtime_processor_snapshot(processor_key)
|
|
|
|
supported = {normalize_resolution(item) for item in (supported_provider_resolutions or []) if item}
|
|
if supported and provider_resolution not in supported:
|
|
raise HTTPException(
|
|
status_code=400,
|
|
detail=f"超分规则要求实际生成 {provider_resolution},但当前视频引擎不支持该分辨率",
|
|
)
|
|
|
|
target_width, target_height = calculate_target_dimensions(
|
|
aspect_ratio=aspect_ratio,
|
|
target_resolution=original_resolution,
|
|
)
|
|
target_short_edge_pixels = video_upscale_short_edge_pixels(original_resolution)
|
|
snapshot: dict[str, Any] = {
|
|
"config_version": int(config.get("version") or 1),
|
|
"target_resolution": original_resolution,
|
|
"provider_generation_resolution": provider_resolution,
|
|
"processor_key": processor_key,
|
|
"processor": processor,
|
|
"aspect_ratio": aspect_ratio,
|
|
"target_short_edge_pixels": target_short_edge_pixels,
|
|
"target_width": target_width,
|
|
"target_height": target_height,
|
|
"delete_source_after_success": bool(config.get("delete_source_after_success", True)),
|
|
"snapshot_created_at": datetime.now(timezone.utc).isoformat(),
|
|
}
|
|
canonical = json.dumps(snapshot, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
|
|
snapshot["snapshot_hash"] = hashlib.sha256(canonical.encode("utf-8")).hexdigest()
|
|
log_video_upscale_event(
|
|
event_type="upscale_snapshot_matched",
|
|
detail={
|
|
"target_resolution": original_resolution,
|
|
"provider_generation_resolution": provider_resolution,
|
|
"processor_key": processor_key,
|
|
"aspect_ratio": aspect_ratio,
|
|
"target_width": target_width,
|
|
"target_height": target_height,
|
|
"delete_source_after_success": snapshot["delete_source_after_success"],
|
|
"config_version": snapshot["config_version"],
|
|
"snapshot_hash": snapshot["snapshot_hash"],
|
|
},
|
|
)
|
|
return provider_resolution, True, json.dumps(snapshot, ensure_ascii=False, separators=(",", ":"))
|
|
|
|
|
|
def parse_video_upscale_snapshot(value: str | None) -> dict[str, Any]:
|
|
if not value:
|
|
return {}
|
|
try:
|
|
data = json.loads(value)
|
|
except Exception as exc:
|
|
raise RuntimeError(f"超分快照不是合法 JSON: {exc}") from exc
|
|
if not isinstance(data, dict):
|
|
raise RuntimeError("超分快照必须是 JSON 对象")
|
|
return data
|