from __future__ import annotations from typing import Literal from pydantic import BaseModel, ConfigDict, Field, field_validator from app.enums.upload_resource import ( UploadResourceBindStatusEnum, UploadResourceDeletePolicyEnum, UploadResourceModuleEnum, UploadResourceTypeEnum, ) from app.schemas.common import NaiveDatetimeOptional class UploadResourceMediaReferenceOut(BaseModel): """AI 创作可直接复用的上传素材参考结构。""" name: str | None = Field(None, description="参考素材名称,前端展示用") type: Literal["image", "video", "audio"] = Field(..., description="参考素材类型:image=图片,video=视频,audio=音频") url: str = Field(..., description="参考素材 URL,创建 AI 创作任务时传入 media_references[].url") label: str | None = Field("", description="前端展示标签,默认空,由 AI 创作页重新计算") duration: float | None = Field(None, ge=0, description="素材时长秒数,type=video/audio 时必须有有效值") source: Literal["upload_resource"] = Field("upload_resource", description="参考素材来源固定为 upload_resource") upload_resource_id: str = Field(..., description="UploadResource 本地资源账本 ID") class UploadResourceHistoryItemOut(BaseModel): """上传资源历史素材项。""" model_config = ConfigDict( json_schema_extra={ "example": { "id": "0019e0a448a23114888", "source_type": "upload_resource", "history_source": "upload_resource", "history_source_label": "历史上传素材", "module": "common", "module_label": "普通上传", "resource_type": "video", "resource_type_label": "视频", "resource_url": "/uploads/videos/2026/07/08/video_ref_xxx.mp4", "display_url": "/uploads/videos/2026/07/08/video_ref_xxx.mp4", "preview_url": "/uploads/videos/2026/07/08/video_ref_xxx.mp4", "video_url": "/uploads/videos/2026/07/08/video_ref_xxx.mp4", "duration_seconds": 5.2, "bind_status": "pending", "delete_policy": "user_deletable", "deletable": True, "media_reference": { "name": "video_ref_xxx.mp4", "type": "video", "url": "/uploads/videos/2026/07/08/video_ref_xxx.mp4", "duration": 5.2, "source": "upload_resource", "upload_resource_id": "0019e0a448a23114888", }, "created_at": "2026-07-08T13:20:00", } } ) id: str = Field(..., description="UploadResource.id") source_type: Literal["upload_resource"] = Field("upload_resource", description="前端判定来源固定值") history_source: Literal["upload_resource"] = Field("upload_resource", description="素材云历史来源固定为 upload_resource") history_source_label: str = Field("历史上传素材", description="素材云历史来源中文名称") module: str = Field(..., description="上传资源所属模块:common=普通上传,hot_opening_replicate=爆款开头复刻,shot_replicate=拆镜复刻") module_label: str = Field(..., description="上传资源所属模块中文名称") resource_type: Literal["image", "video", "audio"] = Field(..., description="资源类型:image=图片,video=视频,audio=音频") resource_type_label: str = Field(..., description="资源类型中文名称") resource_url: str = Field(..., description="原始资源 URL,可传给 AI 创作 media_references[].url") display_url: str = Field(..., description="前端展示 URL。当前普通上传资源通常与 resource_url 一致") preview_url: str = Field(..., description="前端预览 URL。当前普通上传资源通常与 display_url 一致") image_url: str | None = Field(None, description="图片资源 URL;resource_type=image 时有值") video_url: str | None = Field(None, description="视频资源 URL;resource_type=video 时有值") audio_url: str | None = Field(None, description="音频资源 URL;resource_type=audio 时有值") file_name: str | None = Field(None, description="文件名") file_ext: str | None = Field(None, description="文件扩展名") mime_type: str | None = Field(None, description="MIME 类型") file_size_bytes: int = Field(0, description="文件大小,单位字节") duration_seconds: float | None = Field(None, description="视频/音频时长秒数") width: int | None = Field(None, description="图片或视频宽度,未识别时为空") height: int | None = Field(None, description="图片或视频高度,未识别时为空") bind_status: str = Field(..., description="绑定状态。历史素材接口只返回 pending=未绑定") delete_policy: str = Field(..., description="删除策略。历史素材接口只返回 user_deletable=用户可删除") deletable: bool = Field(True, description="是否允许当前用户删除。历史素材接口只返回 true") media_reference: UploadResourceMediaReferenceOut = Field(..., description="AI 创作可直接复用的参考素材对象") created_at: NaiveDatetimeOptional = Field(None, description="上传时间") updated_at: NaiveDatetimeOptional = Field(None, description="更新时间") class UploadResourceHistoryDayGroupOut(BaseModel): """上传资源历史按天分组响应项。""" generated_date: str = Field(..., description="上传日期,格式 YYYY-MM-DD。字段名沿用生成历史,方便前端复用分组逻辑") total: int = Field(..., description="当前日期下可展示/可复用/可删除的上传素材总数") page: int = Field(1, description="当前日期下 items 的页码,分组接口固定为 1") items: list[UploadResourceHistoryItemOut] = Field(default_factory=list, description="当前日期倒序前若干条上传素材") class UploadResourceHistoryGroupedOut(BaseModel): """上传资源历史日期分组列表响应体。""" total_days: int = Field(..., description="符合条件的上传素材日期分组总数") page: int = Field(..., description="日期分组页码,从 1 开始") page_size: int = Field(..., description="日期分组每页数量,最大 10") groups: list[UploadResourceHistoryDayGroupOut] = Field(default_factory=list, description="上传日期倒序分组列表") class UploadResourceHistoryDayItemsOut(BaseModel): """指定日期下上传资源历史分页响应体。""" generated_date: str = Field(..., description="当前查询上传日期,格式 YYYY-MM-DD") total: int = Field(..., description="当前日期下符合条件的上传素材总数") page: int = Field(..., description="当前日期下分页页码,从 1 开始") page_size: int = Field(..., description="当前日期下每页返回数量,最大 100") items: list[UploadResourceHistoryItemOut] = Field(default_factory=list, description="当前日期下上传素材列表,按上传时间倒序排列") class UploadResourceHistoryBatchDeleteRequest(BaseModel): """批量删除上传历史素材请求体。""" resource_ids: list[str] = Field( ..., min_length=1, max_length=30, description="UploadResource.id 列表。一次最多 30 条,不允许重复", examples=[["0019e0a448a23114888", "0019e0a448a23114889"]], ) @field_validator("resource_ids") @classmethod def validate_resource_ids(cls, values: list[str]) -> list[str]: cleaned = [str(value).strip() for value in values if str(value or "").strip()] if not cleaned: raise ValueError("resource_ids 不能为空") if len(cleaned) > 30: raise ValueError("单次最多删除 30 条上传素材") if len(cleaned) != len(set(cleaned)): raise ValueError("resource_ids 不允许重复") return cleaned class UploadResourceCleanupOut(BaseModel): """真实文件清理结果。""" matched: int = Field(0, description="匹配到待清理资源数量") deleted: int = Field(0, description="真实文件删除成功数量") missing: int = Field(0, description="真实文件已不存在数量") failed: int = Field(0, description="真实文件删除失败数量") legacy_deleted: int = Field(0, description="兼容历史路径删除成功数量") legacy_missing: int = Field(0, description="兼容历史路径不存在数量") legacy_failed: int = Field(0, description="兼容历史路径删除失败数量") class UploadResourceHistoryBatchDeleteOut(BaseModel): """批量删除上传历史素材响应体。""" message: str = Field("删除成功", description="操作结果消息") requested_count: int = Field(..., description="请求删除数量") deleted_count: int = Field(..., description="成功软删数量") requested_ids: list[str] = Field(default_factory=list, description="请求删除的 UploadResource.id 列表") deleted_ids: list[str] = Field(default_factory=list, description="已软删的 UploadResource.id 列表") released_size_bytes: int = Field(0, description="已释放的上传容量字节数") cleanup: UploadResourceCleanupOut = Field(default_factory=UploadResourceCleanupOut, description="commit 成功后的真实文件清理结果") UPLOAD_RESOURCE_HISTORY_ALLOWED_RESOURCE_TYPES = { UploadResourceTypeEnum.IMAGE.value, UploadResourceTypeEnum.VIDEO.value, UploadResourceTypeEnum.AUDIO.value, } UPLOAD_RESOURCE_HISTORY_ALLOWED_BIND_STATUS = UploadResourceBindStatusEnum.PENDING.value UPLOAD_RESOURCE_HISTORY_ALLOWED_DELETE_POLICY = UploadResourceDeletePolicyEnum.USER_DELETABLE.value UPLOAD_RESOURCE_HISTORY_ALLOWED_MODULES = { UploadResourceModuleEnum.COMMON.value, UploadResourceModuleEnum.HOT_OPENING_REPLICATE.value, UploadResourceModuleEnum.SHOT_REPLICATE.value, }