Files
video-gen/video-gen-api/app/services/private_portrait/reference_resolver.py
T
2026-07-07 10:48:52 +08:00

316 lines
13 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from __future__ import annotations
from copy import deepcopy
from typing import Any
from fastapi import HTTPException
from sqlalchemy import or_, select
from sqlalchemy.ext.asyncio import AsyncSession
from app.enums.private_portrait import (
PRIVATE_PORTRAIT_ASSET_URI_PREFIX,
PrivatePortraitAssetStatus,
PrivatePortraitAssetType,
PrivatePortraitEventSource,
PrivatePortraitEventStatus,
PrivatePortraitEventType,
PrivatePortraitReferenceSource,
)
from app.models.private_portrait import PrivatePortraitAsset
from app.services.operation_log_service import log_operation_error, log_operation_event
DOMAIN = "private_portrait"
_ASSET_TYPE_TO_REFERENCE_TYPE = {
PrivatePortraitAssetType.IMAGE.value: "image",
PrivatePortraitAssetType.VIDEO.value: "video",
PrivatePortraitAssetType.AUDIO.value: "audio",
}
_SUPPORTED_GEN_TYPES = {"image", "video"}
def _ref_get(ref: Any, key: str) -> Any:
if isinstance(ref, dict):
return ref.get(key)
return getattr(ref, key, None)
def _ref_set(ref: Any, key: str, value: Any) -> None:
if isinstance(ref, dict):
ref[key] = value
else:
setattr(ref, key, value)
def _normalize_gen_type(gen_type: str | None) -> str | None:
value = (gen_type or "").strip().lower()
if not value:
return None
if value not in _SUPPORTED_GEN_TYPES:
raise HTTPException(status_code=400, detail="gen_type 仅支持 image 或 video")
return value
def _normalize_ref_type(value: Any) -> str:
return str(value or "").strip().lower()
def _remote_asset_id_from_asset_uri(url: Any) -> str | None:
value = str(url or "").strip()
if not value.startswith(PRIVATE_PORTRAIT_ASSET_URI_PREFIX):
return None
remote_asset_id = value[len(PRIVATE_PORTRAIT_ASSET_URI_PREFIX):].strip()
return remote_asset_id or None
def _asset_display_url(asset: PrivatePortraitAsset) -> str | None:
# preview_url 是本地上传预览,remote_url 是火山 GetAsset 返回的远程资源 URLsource_url 是兜底公网上传地址。
return asset.preview_url or asset.remote_url or asset.source_url or None
def _fill_private_portrait_reference_display_fields(ref: Any, asset: PrivatePortraitAsset) -> None:
provider_url = str(_ref_get(ref, "provider_url") or _ref_get(ref, "url") or "").strip()
if provider_url.startswith(PRIVATE_PORTRAIT_ASSET_URI_PREFIX):
_ref_set(ref, "provider_url", provider_url)
display_url = _asset_display_url(asset)
if display_url:
# 返回给前端的 url 必须可预览;供应商专用 asset:// 保留到 provider_url,避免管理后台和客户端展示黑图。
_ref_set(ref, "url", display_url)
_ref_set(ref, "display_url", display_url)
_ref_set(ref, "preview_url", display_url)
_ref_set(ref, "source", PrivatePortraitReferenceSource.PRIVATE_PORTRAIT_ASSET.value)
_ref_set(ref, "private_asset_id", asset.id)
if asset.remote_asset_id:
_ref_set(ref, "remote_asset_id", asset.remote_asset_id)
expected_ref_type = _ASSET_TYPE_TO_REFERENCE_TYPE.get(asset.asset_type)
if expected_ref_type:
_ref_set(ref, "type", expected_ref_type)
if not _ref_get(ref, "name") and asset.name:
_ref_set(ref, "name", asset.name)
async def resolve_private_portrait_reference_display_urls(
db: AsyncSession,
media_references: list[Any] | None,
*,
user_id: str | None = None,
) -> list[Any] | None:
"""把历史响应里的 asset:// 引用补成前端可预览 URL。
生成任务入库时 url 使用 asset://remote_asset_id 传给供应商;但客户端/管理后台展示不能直接用
asset://。这里批量根据 private_asset_id 或 asset://remote_asset_id 查本地素材,并把响应中的 url
改成 preview_url/remote_url/source_url,同时保留 provider_url=asset://... 供排查。
"""
if not media_references:
return media_references
refs = deepcopy(media_references)
private_asset_ids: list[str] = []
remote_asset_ids: list[str] = []
for ref in refs:
source = _ref_get(ref, "source")
private_asset_id = _ref_get(ref, "private_asset_id")
remote_asset_id = _ref_get(ref, "remote_asset_id") or _remote_asset_id_from_asset_uri(_ref_get(ref, "url"))
if source == PrivatePortraitReferenceSource.PRIVATE_PORTRAIT_ASSET.value or remote_asset_id:
if private_asset_id:
private_asset_ids.append(str(private_asset_id))
if remote_asset_id:
remote_asset_ids.append(str(remote_asset_id))
private_asset_ids = list(dict.fromkeys(private_asset_ids))
remote_asset_ids = list(dict.fromkeys(remote_asset_ids))
if not private_asset_ids and not remote_asset_ids:
return refs
filters = []
if private_asset_ids:
filters.append(PrivatePortraitAsset.id.in_(private_asset_ids))
if remote_asset_ids:
filters.append(PrivatePortraitAsset.remote_asset_id.in_(remote_asset_ids))
stmt = select(PrivatePortraitAsset).where(or_(*filters))
if user_id is not None:
stmt = stmt.where(PrivatePortraitAsset.user_id == user_id)
rows = await db.execute(stmt)
assets = list(rows.scalars().all())
by_id = {asset.id: asset for asset in assets}
by_remote_id = {asset.remote_asset_id: asset for asset in assets if asset.remote_asset_id}
for ref in refs:
private_asset_id = str(_ref_get(ref, "private_asset_id") or "").strip()
remote_asset_id = str(_ref_get(ref, "remote_asset_id") or _remote_asset_id_from_asset_uri(_ref_get(ref, "url")) or "").strip()
asset = by_id.get(private_asset_id) or by_remote_id.get(remote_asset_id)
if not asset:
continue
_fill_private_portrait_reference_display_fields(ref, asset)
return refs
async def batch_resolve_private_portrait_reference_display_urls(
db: AsyncSession,
references_by_key: dict[Any, list[Any] | None],
*,
user_id: str | None = None,
) -> dict[Any, list[Any] | None]:
if not references_by_key:
return {}
copied: dict[Any, list[Any] | None] = {
key: deepcopy(refs) if refs else refs
for key, refs in references_by_key.items()
}
private_asset_ids: list[str] = []
remote_asset_ids: list[str] = []
for refs in copied.values():
if not refs:
continue
for ref in refs:
source = _ref_get(ref, "source")
private_asset_id = _ref_get(ref, "private_asset_id")
remote_asset_id = _ref_get(ref, "remote_asset_id") or _remote_asset_id_from_asset_uri(_ref_get(ref, "url"))
if source == PrivatePortraitReferenceSource.PRIVATE_PORTRAIT_ASSET.value or remote_asset_id:
if private_asset_id:
private_asset_ids.append(str(private_asset_id))
if remote_asset_id:
remote_asset_ids.append(str(remote_asset_id))
private_asset_ids = list(dict.fromkeys(private_asset_ids))
remote_asset_ids = list(dict.fromkeys(remote_asset_ids))
if not private_asset_ids and not remote_asset_ids:
return copied
filters = []
if private_asset_ids:
filters.append(PrivatePortraitAsset.id.in_(private_asset_ids))
if remote_asset_ids:
filters.append(PrivatePortraitAsset.remote_asset_id.in_(remote_asset_ids))
stmt = select(PrivatePortraitAsset).where(or_(*filters))
if user_id is not None:
stmt = stmt.where(PrivatePortraitAsset.user_id == user_id)
rows = await db.execute(stmt)
assets = list(rows.scalars().all())
by_id = {asset.id: asset for asset in assets}
by_remote_id = {asset.remote_asset_id: asset for asset in assets if asset.remote_asset_id}
for refs in copied.values():
if not refs:
continue
for ref in refs:
private_asset_id = str(_ref_get(ref, "private_asset_id") or "").strip()
remote_asset_id = str(_ref_get(ref, "remote_asset_id") or _remote_asset_id_from_asset_uri(_ref_get(ref, "url")) or "").strip()
asset = by_id.get(private_asset_id) or by_remote_id.get(remote_asset_id)
if not asset:
continue
_fill_private_portrait_reference_display_fields(ref, asset)
return copied
async def resolve_private_portrait_references(
db: AsyncSession,
*,
user_id: str,
media_references: list[Any] | None,
gen_type: str | None = None,
) -> list[Any] | None:
"""Resolve private portrait references before dispatching a generation task.
generation_ai_service.py 和 generation_task_factory_service.py 都会传 gen_type。
这里保留该参数用于兼容调用方,并做基础校验,避免接口因函数签名不一致直接 500。
"""
normalized_gen_type = _normalize_gen_type(gen_type)
if not media_references:
return media_references
refs = deepcopy(media_references)
ids = [
str(_ref_get(ref, "private_asset_id"))
for ref in refs
if _ref_get(ref, "source") == PrivatePortraitReferenceSource.PRIVATE_PORTRAIT_ASSET.value
and _ref_get(ref, "private_asset_id")
]
ids = list(dict.fromkeys(ids))
if not ids:
return refs
log_operation_event(
domain=DOMAIN,
event_type=PrivatePortraitEventType.REFERENCE_RESOLVE_START.value,
event_status=PrivatePortraitEventStatus.PENDING.value,
source=PrivatePortraitEventSource.SERVICE.value,
user_id=user_id,
detail={"private_asset_ids": ids, "gen_type": normalized_gen_type},
)
try:
rows = await db.execute(select(PrivatePortraitAsset).where(PrivatePortraitAsset.id.in_(ids)))
asset_map = {asset.id: asset for asset in rows.scalars().all()}
for ref in refs:
if _ref_get(ref, "source") != PrivatePortraitReferenceSource.PRIVATE_PORTRAIT_ASSET.value:
continue
asset_id = str(_ref_get(ref, "private_asset_id") or "")
if not asset_id:
raise HTTPException(status_code=400, detail="真人素材引用缺少 private_asset_id")
asset = asset_map.get(asset_id)
if not asset:
raise HTTPException(status_code=400, detail="真人素材不存在")
if asset.user_id != user_id:
raise HTTPException(status_code=403, detail="真人素材不属于当前用户")
if asset.deleted_at is not None:
raise HTTPException(status_code=400, detail="真人素材已删除")
if asset.status != PrivatePortraitAssetStatus.ACTIVE.value:
raise HTTPException(status_code=400, detail=f"真人素材状态为 {asset.status}Active 后才可用于生成")
if not asset.remote_asset_id:
raise HTTPException(status_code=400, detail="真人素材缺少远程 AssetId")
expected_ref_type = _ASSET_TYPE_TO_REFERENCE_TYPE.get(asset.asset_type)
ref_type = _normalize_ref_type(_ref_get(ref, "type"))
if expected_ref_type and ref_type and ref_type != expected_ref_type:
raise HTTPException(status_code=400, detail=f"真人素材类型不匹配:引用为 {ref_type},素材为 {expected_ref_type}")
provider_url = f"{PRIVATE_PORTRAIT_ASSET_URI_PREFIX}{asset.remote_asset_id}"
_ref_set(ref, "source", PrivatePortraitReferenceSource.PRIVATE_PORTRAIT_ASSET.value)
_ref_set(ref, "private_asset_id", asset.id)
_ref_set(ref, "remote_asset_id", asset.remote_asset_id)
_ref_set(ref, "url", provider_url)
_ref_set(ref, "provider_url", provider_url)
display_url = _asset_display_url(asset)
if display_url:
_ref_set(ref, "display_url", display_url)
_ref_set(ref, "preview_url", display_url)
if expected_ref_type:
_ref_set(ref, "type", expected_ref_type)
if not _ref_get(ref, "name") and asset.name:
_ref_set(ref, "name", asset.name)
log_operation_event(
domain=DOMAIN,
event_type=PrivatePortraitEventType.REFERENCE_RESOLVE_SUCCESS.value,
event_status=PrivatePortraitEventStatus.SUCCESS.value,
source=PrivatePortraitEventSource.SERVICE.value,
user_id=user_id,
detail={"count": len(ids), "gen_type": normalized_gen_type},
)
return refs
except Exception as exc:
log_operation_error(
domain=DOMAIN,
event_type=PrivatePortraitEventType.REFERENCE_RESOLVE_FAILED.value,
source=PrivatePortraitEventSource.SERVICE.value,
user_id=user_id,
detail={"private_asset_ids": ids, "gen_type": normalized_gen_type},
exc=exc,
)
raise