Files
video-gen/video-gen-api/app/services/generation/media_reference_service.py
T

149 lines
5.1 KiB
Python

from __future__ import annotations
import json
from dataclasses import dataclass
from typing import Any, Iterable
from fastapi import HTTPException
@dataclass(frozen=True, slots=True)
class MediaReferenceUsage:
image_count: int = 0
video_count: int = 0
audio_count: int = 0
input_video_duration: float = 0.0
input_audio_duration: float = 0.0
def parse_media_references(value: str | list[dict] | None) -> list[dict]:
if not value:
return []
data: Any = value
if isinstance(value, str):
try:
data = json.loads(value)
except (TypeError, json.JSONDecodeError):
return []
if not isinstance(data, list):
return []
return [item for item in data if isinstance(item, dict)]
def _reference_type(item: dict) -> str:
value = str(item.get("type") or item.get("media_type") or "").strip().lower()
if value in {"image", "video", "audio"}:
return value
mime = str(item.get("mime_type") or item.get("content_type") or "").lower()
if mime.startswith("image/"):
return "image"
if mime.startswith("video/"):
return "video"
if mime.startswith("audio/"):
return "audio"
url = str(item.get("url") or item.get("file_url") or "").lower().split("?", 1)[0]
if url.endswith((".png", ".jpg", ".jpeg", ".webp", ".gif", ".bmp")):
return "image"
if url.endswith((".mp4", ".mov", ".webm", ".mkv", ".avi")):
return "video"
if url.endswith((".mp3", ".wav", ".m4a", ".aac", ".ogg", ".flac")):
return "audio"
return ""
def _duration(item: dict) -> float:
for key in ("duration", "duration_seconds", "video_duration", "audio_duration"):
try:
value = float(item.get(key) or 0)
except (TypeError, ValueError):
continue
if value > 0:
return value
return 0.0
def calculate_media_reference_usage(
references: str | list[dict] | None,
*,
include: bool,
) -> MediaReferenceUsage:
if not include:
return MediaReferenceUsage()
image_count = video_count = audio_count = 0
video_duration = audio_duration = 0.0
for item in parse_media_references(references):
media_type = _reference_type(item)
if media_type == "image":
image_count += 1
elif media_type == "video":
video_count += 1
video_duration += _duration(item)
elif media_type == "audio":
audio_count += 1
audio_duration += _duration(item)
return MediaReferenceUsage(
image_count=image_count,
video_count=video_count,
audio_count=audio_count,
input_video_duration=round(video_duration, 3),
input_audio_duration=round(audio_duration, 3),
)
def filter_references_by_type(
references: str | list[dict] | None,
*,
allowed_types: Iterable[str],
max_count: int | None = None,
) -> list[dict]:
allowed = {str(item).lower() for item in allowed_types}
result = [item for item in parse_media_references(references) if _reference_type(item) in allowed]
if max_count is not None:
return result[: max(0, int(max_count))]
return result
def validate_media_reference_usage_for_engine(
usage: MediaReferenceUsage,
*,
gen_type: str,
engine: Any,
) -> None:
"""按所选引擎能力校验最终实际会发送的附件。"""
normalized = str(gen_type or "").strip().lower()
if normalized == "video":
if not bool(getattr(engine, "supports_universal_reference", True)) and (
usage.image_count or usage.video_count or usage.audio_count
):
raise HTTPException(status_code=400, detail="当前视频引擎不支持参考附件")
limits = {
"图片": int(getattr(engine, "max_image_count", 0) or 0),
"视频": int(getattr(engine, "max_video_count", 0) or 0),
"音频": int(getattr(engine, "max_audio_count", 0) or 0),
}
counts = {"图片": usage.image_count, "视频": usage.video_count, "音频": usage.audio_count}
for label, count in counts.items():
limit = limits[label]
if count > limit:
raise HTTPException(
status_code=400,
detail=f"当前视频引擎最多支持 {limit}{label}附件,当前为 {count} 个",
)
return
if normalized == "image":
if usage.video_count or usage.audio_count:
raise HTTPException(status_code=400, detail="图片生成只能携带图片附件")
limit = int(
getattr(engine, "max_reference_image_count", None)
if getattr(engine, "max_reference_image_count", None) is not None
else getattr(engine, "max_image_count", 0)
or 0
)
if usage.image_count > limit:
raise HTTPException(
status_code=400,
detail=f"当前图片引擎最多支持 {limit} 张参考图,当前为 {usage.image_count} 张",
)
return
raise HTTPException(status_code=400, detail="不支持的生成类型")