diff --git a/video-gen-api/app/services/private_portrait/reference_resolver.py b/video-gen-api/app/services/private_portrait/reference_resolver.py index 7d55b879..3e8573fc 100644 --- a/video-gen-api/app/services/private_portrait/reference_resolver.py +++ b/video-gen-api/app/services/private_portrait/reference_resolver.py @@ -7,13 +7,31 @@ from fastapi import HTTPException from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession -from app.enums.private_portrait import PRIVATE_PORTRAIT_ASSET_URI_PREFIX, PrivatePortraitAssetStatus, PrivatePortraitEventSource, PrivatePortraitEventStatus, PrivatePortraitEventType, PrivatePortraitReferenceSource +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_event +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) @@ -27,33 +45,109 @@ def _ref_set(ref: Any, key: str, value: Any) -> None: setattr(ref, key, value) -async def resolve_private_portrait_references(db: AsyncSession, *, user_id: str, media_references: list[Any] | None) -> list[Any] | None: +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() + + +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 = [ + 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}) - 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 "") - 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") - _ref_set(ref, "remote_asset_id", asset.remote_asset_id) - _ref_set(ref, "url", f"{PRIVATE_PORTRAIT_ASSET_URI_PREFIX}{asset.remote_asset_id}") - 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)}) - 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}") + + _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", f"{PRIVATE_PORTRAIT_ASSET_URI_PREFIX}{asset.remote_asset_id}") + 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