from typing import Annotated, Literal from pydantic import BaseModel, ConfigDict, Field, field_validator from app.schemas.common import NaiveDatetimeOptional from app.enums.generation_history import normalize_generation_history_source class GenerationAIReference(BaseModel): """AI生成任务参考素材。""" model_config = ConfigDict( json_schema_extra={ "example": { "url": "https://example.com/reference.png", "type": "image", "name": "参考图.png", "duration": 5.0, } } ) url: str = Field( ..., description="参考素材地址,可以是图片地址、视频地址或音频地址", examples=["https://example.com/reference.png"], ) type: str = Field( ..., description="参考素材类型:image=图片,video=视频,audio=音频", examples=["image"], ) name: str | None = Field( None, description="参考素材名称,前端展示用,可为空", examples=["参考图.png"], ) duration: float | None = Field( None, ge=0, description="参考素材时长(秒)。type=video/audio 时使用,用于视频/音频素材时长校验", examples=[5.0], ) source: str | None = Field( None, description=( "参考素材来源枚举:upload_resource=历史上传素材;" "private_portrait_asset=私域真人/虚拟素材;为空表示本次普通上传素材。" ), examples=["upload_resource"], ) private_asset_id: str | None = Field( None, description="真人素材库本地素材ID。source=private_portrait_asset 时必填,后端据此解析 remote_asset_id", examples=["0019fxxx"], ) remote_asset_id: str | None = Field( None, description="后端回填的火山 Asset ID。前端传入时不可信,创建任务时以后端查库为准", examples=["asset-20260318071009-xxxxx"], ) provider_url: str | None = Field( None, description="供应商专用素材地址。真人素材生成时通常为 asset://remote_asset_id;仅用于后端排查和生成链路,不用于前端预览", examples=["asset://asset-20260318071009-xxxxx"], ) display_url: str | None = Field( None, description="前端展示用素材地址。真人素材历史响应会回填为可预览图片/视频/音频 URL", examples=["/uploads/images/2026/07/06/demo.jpg"], ) preview_url: str | None = Field( None, description="前端预览用素材地址;通常与 display_url 一致", examples=["/uploads/images/2026/07/06/demo.jpg"], ) role: str | None = Field( None, description=( "参考素材角色枚举:first_frame=首帧图;last_frame=尾帧图;" "reference_image=普通参考图;reference_video=普通参考视频;reference_audio=普通参考音频。" ), examples=["first_frame"], ) upload_resource_id: str | None = Field( None, description="历史上传素材 ID。source=upload_resource 时由 /upload-resources/history 返回,便于前端排查和去重;生成链路主要使用 url。", examples=["0019e0a448a23114888"], ) class GenerationAITaskCreate(BaseModel): """创建AI图片/视频生成任务请求体。""" model_config = ConfigDict( json_schema_extra={ "examples": [ { "project_id": None, "gen_type": "image", "original_prompt": "生成一张赛博朋克风格的城市夜景", "engine_id": None, "media_references": [ { "url": "https://example.com/reference.png", "type": "image", "name": "参考图.png", } ], "idempotency_key": "frontend-submit-uuid-001", "image_size": "2K", "image_proportion": "1:1", "image_px": "2048x2048", "duration": None, "aspect_ratio": None, "resolution": None, }, { "project_id": None, "gen_type": "video", "original_prompt": "生成一段海边日落的电影感视频", "engine_id": None, "media_references": None, "idempotency_key": "frontend-submit-uuid-002", "image_size": None, "image_proportion": None, "image_px": None, "duration": 4, "aspect_ratio": "16:9", "resolution": "480p", }, ] } ) # 新 chat 生成任务不绑定 project_id。为了兼容旧前端误传,保留可选字段但后端不使用。 project_id: str | None = Field( None, description="兼容旧前端字段。当前 /generation-ai 任务不绑定项目,后端不使用该字段,可传 null 或不传", examples=[None], ) gen_type: str = Field( ..., description="生成类型:image=图片生成,video=视频生成", examples=["image"], ) original_prompt: str = Field( ..., min_length=1, max_length=5000, description="用户原始提示词,不能为空,最长5000字符", examples=["生成一张赛博朋克风格的城市夜景"], ) engine_id: str | None = Field( None, description="图片/视频引擎ID;为空则使用当前激活且优先级最高的引擎", examples=[None], ) media_references: list[GenerationAIReference] | None = Field( None, description=( "参考素材列表。支持 image/video/audio。" "source=upload_resource 时表示来自历史上传素材;source=private_portrait_asset 时表示来自私域真人/虚拟素材。" "视频和音频素材必须携带 duration,前端和后端均按 AI 创作原规则校验数量、单段 2~15 秒、总时长不超过 15 秒。" ), ) idempotency_key: str | None = Field( None, max_length=64, description=( "幂等键,用于防止前端重复提交、网络重试导致重复创建任务和重复扣费。" "同一用户、同一 idempotency_key、同一 generation_mode 下重复请求会返回已有任务。" "建议前端每次点击生成时生成 UUID;同一次请求失败重试时复用同一个 UUID。" ), examples=["frontend-submit-uuid-001"], ) # image params image_size: str | None = Field( None, description="图片分辨率档位枚举示例:1K、2K、4K。实际可选值以 /generation-ai/engines 返回的 supported_sizes 为准。", examples=["2K"], ) image_proportion: str | None = Field( None, description="图片比例枚举示例:1:1、16:9、9:16、4:3、3:4、21:9。实际可选值以 /generation-ai/engines 返回为准。", examples=["1:1"], ) image_px: str | None = Field( None, description="图片像素尺寸,例如:2048x2048。为空时后端根据 image_size 和 image_proportion 自动匹配", examples=["2048x2048"], ) # video params duration: int | None = Field( None, description="视频生成时长,单位秒。枚举范围通常为 4~15 秒,实际可选值以视频引擎 supported_durations 为准。", examples=[4], ) aspect_ratio: str | None = Field( None, description="视频比例枚举示例:16:9、4:3、1:1、3:4、9:16、21:9。实际可选值以视频引擎 supported_ratios 为准。", examples=["16:9"], ) resolution: str | None = Field( None, description="视频分辨率枚举示例:480p、720p、1080p。实际可选值以视频引擎 supported_resolutions 为准。", examples=["480p"], ) class GenerationAIImageEngineOptionOut(BaseModel): """AI图片生成可用引擎响应项。""" id: str = Field(..., description="图片引擎ID,创建图片任务时传入 engine_id") name: str = Field(..., description="图片引擎名称,前端展示用") provider: str = Field(..., description="服务商标识,例如 ark") model_name: str | None = Field(None, description="服务商模型名称") supported_models: list[str] = Field(default_factory=list, description="该图片引擎支持的模型名称列表") supported_sizes: dict = Field( default_factory=dict, description="支持的图片尺寸映射。第一层为分辨率档位,例如 2K/4K;第二层为画布比例,例如 1:1/16:9;值为像素尺寸", ) default_size: str | None = Field(None, description="默认图片分辨率档位,例如 2K") priority: int = Field(0, description="引擎优先级,数值越大越优先") max_image_count: int = Field(0, description="最大图片数量") class GenerationAIVideoEngineOptionOut(BaseModel): """AI视频生成可用引擎响应项。""" id: str = Field(..., description="视频引擎ID,创建视频任务时传入 engine_id") name: str = Field(..., description="视频引擎名称,前端展示用") provider: str = Field(..., description="服务商标识,例如 ark") model_name: str | None = Field(None, description="服务商模型名称") supported_ratios: list[str] = Field(default_factory=list, description="支持的视频画面比例,例如 16:9、9:16、1:1") supported_resolutions: list[str] = Field(default_factory=list, description="支持的视频分辨率,例如 480p、720p、1080p") supported_durations: list[int] = Field(default_factory=list, description="支持的视频时长列表,单位秒") max_duration: int | None = Field(None, description="最大视频时长,单位秒") priority: int = Field(0, description="引擎优先级,数值越大越优先") max_image_count: int | None = Field(None, description="最大图片数量") max_video_count: int | None = Field(None, description="最大视频数量") max_audio_count: int | None = Field(None, description="最大参考音频数量,0 表示不支持音频参考") supports_first_last_frame: bool = Field(False, description="是否支持首帧和最后一帧") supports_universal_reference: bool = Field(False, description="是否支持通用参考") class GenerationAIEngineGroupOut(BaseModel): """AI图片/视频引擎分组。""" image: list[GenerationAIImageEngineOptionOut] = Field( default_factory=list, description="当前启用的图片生成引擎列表", ) video: list[GenerationAIVideoEngineOptionOut] = Field( default_factory=list, description="当前启用的视频生成引擎列表", ) class GenerationAIEngineOptionsOut(BaseModel): """AI图片/视频生成引擎列表响应体。""" model_config = ConfigDict( json_schema_extra={ "example": { "engine": { "image": [ { "id": "image_engine_xxx", "name": "豆包文生图", "provider": "ark", "model_name": "doubao-seedream-5-0-260128", "supported_models": ["doubao-seedream-5-0-260128"], "supported_sizes": { "2K": { "1:1": "2048x2048", "16:9": "2560x1440", } }, "default_size": "2K", "priority": 10, "max_image_count": 0, } ], "video": [ { "id": "video_engine_xxx", "name": "Seedance 2.0", "provider": "ark", "model_name": "doubao-seedance-2-0-260128", "supported_ratios": ["16:9", "4:3", "1:1", "3:4", "9:16", "21:9"], "supported_resolutions": ["480p", "720p", "1080p"], "supported_durations": [4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15], "max_duration": 15, "priority": 10, "max_image_count": 2, "max_video_count": 0, "max_audio_count": 0, "supports_first_last_frame": False, "supports_universal_reference": False, } ], } } } ) engine: GenerationAIEngineGroupOut = Field(..., description="图片/视频可用引擎分组") class GenerationAITaskOut(BaseModel): """AI生成任务详情响应体。""" model_config = ConfigDict( json_schema_extra={ "example": { "id": "0019e0a44895b6d837d", "source_type": "chat_task", "user_id": None, "user_name": None, "project_id": None, "generated_resource_id": "generated_resource_xxx", "gen_type": "image", "generation_mode": "chatapi_async", "pipeline_stage": "done", "status": "completed", "original_prompt": "生成一张赛博朋克风格的城市夜景", "optimized_prompt": None, "duration": None, "aspect_ratio": None, "resolution": None, "image_size": "2K", "image_proportion": "1:1", "image_px": "2048x2048", "media_references": None, "provider_task_id": "provider_task_xxx", "seedance_task_id": "provider_task_xxx", "remote_result_url": None, "image_url": "https://example.com/result.png", "video_url": None, "video_cover_url": None, "engine_id": "engine_xxx", "engine_snapshot": { "engine_type": "image", "id": "engine_xxx", "name": "图片生成引擎", "provider": "provider_name", "model_name": "model_name", "supported_models": [], "default_size": "2K", "selected_size": "2K", "selected_proportion": "1:1", "selected_px": "2048x2048", }, "credits_cost": 10.0, "text_credits_cost": 0.0, "text_tokens_used": 0, "image_tokens_used": 0, "video_tokens_used": 0, "retry_count": 0, "poll_count": 3, "error_message": None, "created_at": "2026-05-27T10:12:00", "generated_at": "2026-05-27T10:15:30", } } ) id: str = Field(..., description="生成任务ID") source_type: Literal["chat_task"] = Field( "chat_task", description="历史记录来源。ChatGenerationTask 新任务历史固定为 chat_task", ) user_id: str | None = Field( None, description="用户ID,管理后台调用存在对应值,通常为 null", ) user_name: str | None = Field( None, description="用户名称,管理后台调用存在对应值,通常为 null", ) project_id: str | None = Field( None, description="项目ID。当前 /generation-ai 任务不绑定项目,通常为 null", ) generated_resource_id: str | None = Field( None, description="关联的生成资源账本ID,来源于 generated_resources.id;历史脏数据可能为空", ) file_name: str | None = Field(None, description="文件名,来源于 generated_resources.file_name") history_source: str | None = Field( None, description=( "素材云生成历史来源:chat_task=AI创作,generation_record=项目生成," "hot_opening_replicate=爆款开头复刻,shot_replicate=拆镜复刻。" "注意:上传素材历史不在本接口返回,请使用 /upload-resources/history。" ), ) history_source_label: str | None = Field(None, description="素材云历史来源中文名称") module_project_id: str | None = Field(None, description="模块生成项目ID;非模块生成历史返回 null") module_project_title: str | None = Field(None, description="模块生成项目标题;非模块生成历史返回 null") module_step_id: str | None = Field(None, description="模块生成步骤ID;非模块生成历史返回 null") module_step_code: str | None = Field(None, description="模块生成步骤编码;非模块生成历史返回 null") hot_opening_project_id: str | None = Field(None, description="爆款开头复刻项目ID;非爆款开头复刻返回 null") hot_opening_project_title: str | None = Field(None, description="爆款开头复刻项目标题;非爆款开头复刻返回 null") shot_replicate_project_id: str | None = Field(None, description="拆镜复刻项目ID;非拆镜复刻返回 null") shot_replicate_project_title: str | None = Field(None, description="拆镜复刻项目标题;非拆镜复刻返回 null") shot_task_set_id: str | None = Field(None, description="拆镜复刻总任务ID;非拆镜复刻返回 null") shot_segment_id: str | None = Field(None, description="拆镜复刻片段ID;非拆镜复刻返回 null") shot_segment_index: int | None = Field(None, description="拆镜复刻片段序号;非拆镜复刻返回 null") shot_segment_label: str | None = Field(None, description="拆镜复刻片段展示名称,例如:拆镜复刻片段1;非拆镜复刻返回 null") gen_type: str = Field(..., description="生成类型:image=图片,video=视频") generation_mode: str | None = Field( None, description="生成模式。当前异步Chat生成任务一般为 chatapi_async", ) pipeline_stage: str | None = Field( None, description=( "任务流水线阶段,例如:queued=已入队,creating_provider_task=创建第三方任务中," "waiting_remote=等待第三方生成,result_ready=远程结果已就绪,downloading=下载中,done=完成" ), ) status: str = Field( ..., description="任务状态,例如:generating=生成中,completed=已完成,failed=失败", ) original_prompt: str = Field(..., description="用户原始提示词") optimized_prompt: str | None = Field(None, description="优化后的提示词,可能为空") duration: int | None = Field(None, description="视频时长,单位秒。图片任务通常为空") aspect_ratio: str | None = Field(None, description="视频比例,例如 16:9。图片任务通常为空") resolution: str | None = Field(None, description="视频分辨率,例如 480p。图片任务通常为空") image_size: str | None = Field(None, description="图片分辨率档位,例如 2K") image_proportion: str | None = Field(None, description="图片比例,例如 1:1") image_px: str | None = Field(None, description="图片像素尺寸,例如 2048x2048") media_references: list[dict] | None = Field( None, description="参考素材列表。由创建任务时传入的 media_references 转换而来", ) provider_task_id: str | None = Field(None, description="第三方服务商任务ID") seedance_task_id: str | None = Field( None, description="兼容旧前端/旧服务命名的第三方任务ID字段", ) remote_result_url: str | None = Field( None, description="第三方远程结果地址。当前接口可能隐藏或为空,最终展示优先使用 image_url/video_url", ) image_url: str | None = Field(None, description="最终图片地址。图片任务完成后通常有值") video_url: str | None = Field(None, description="最终视频地址。视频任务完成后通常有值") video_cover_url: str | None = Field(None, description="视频封面图片地址。视频任务完成且封面截帧成功后通常有值") engine_id: str | None = Field(None, description="本次任务使用的生成引擎ID") engine_snapshot: dict | None = Field( None, description="生成任务创建时的引擎快照,用于前端展示当时使用的模型、比例、尺寸等信息", ) credits_cost: float = Field(0.0, description="本次任务总消耗积分") text_credits_cost: float = Field(0.0, description="文本优化或文本处理消耗积分") text_tokens_used: int = Field(0, description="文本 token 使用量") image_tokens_used: int = Field(0, description="图片 token 使用量") video_tokens_used: int = Field(0, description="视频 token 使用量") retry_count: int = Field(0, description="任务重试次数") poll_count: int = Field(0, description="轮询第三方任务状态次数") error_message: str | None = Field(None, description="错误信息。成功任务一般为 null") created_at: NaiveDatetimeOptional = Field(None, description="任务创建时间") generated_at: NaiveDatetimeOptional = Field(None, description="任务生成完成时间") class GenerationAITaskListOut(BaseModel): """AI生成任务列表响应体。""" model_config = ConfigDict( json_schema_extra={ "example": { "total": 1, "items": [ { "id": "0019e0a44895b6d837d", "source_type": "chat_task", "user_id": None, "user_name": None, "project_id": None, "gen_type": "image", "generation_mode": "chatapi_async", "pipeline_stage": "done", "status": "completed", "original_prompt": "生成一张赛博朋克风格的城市夜景", "optimized_prompt": None, "duration": None, "aspect_ratio": None, "resolution": None, "image_size": "2K", "image_proportion": "1:1", "image_px": "2048x2048", "media_references": None, "provider_task_id": "provider_task_xxx", "seedance_task_id": "provider_task_xxx", "remote_result_url": None, "image_url": "https://example.com/result.png", "video_url": None, "video_cover_url": None, "engine_id": "engine_xxx", "engine_snapshot": {}, "credits_cost": 10.0, "text_credits_cost": 0.0, "text_tokens_used": 0, "image_tokens_used": 0, "video_tokens_used": 0, "retry_count": 0, "poll_count": 3, "error_message": None, "created_at": "2026-05-27T10:12:00", "generated_at": "2026-05-27T10:15:30", } ], } } ) total: int = Field(..., description="符合筛选条件的任务总数") items: list[GenerationAITaskOut] = Field( default_factory=list, description="当前分页的任务列表", ) class GenerationAITaskDeleteOut(BaseModel): """AI生成任务删除响应体。""" model_config = ConfigDict( json_schema_extra={ "example": { "message": "任务已删除", "task_id": "0019e0a44895b6d837d", "deleted": True, "freed_size_bytes": 123456, } } ) message: str = Field(..., description="操作结果提示信息") task_id: str = Field(..., description="被软删除的AI生成任务ID") deleted: bool = Field(..., description="是否已完成软删除") freed_size_bytes: int = Field(0, description="本次软删联动释放的有效资源空间字节数") class GenerationAIHistoryBatchDeleteRequest(BaseModel): """素材云历史批量删除请求体。""" model_config = ConfigDict( json_schema_extra={ "example": { "history_source": "shot_replicate", "ids": ["shot_segment_id_1", "shot_segment_id_2"], } } ) history_source: str = Field( ..., description=( "素材云历史来源。" "generation_record=项目生成,chat_task=AI创作," "hot_opening_replicate=爆款开头复刻,shot_replicate=拆镜复刻。" "注意:项目生成/AI创作传记录ID;爆款开头复刻传 module_project_id;拆镜复刻传 shot_segment_id。" ), examples=["shot_replicate"], ) ids: list[str] = Field( ..., min_length=1, max_length=30, description="需要删除的ID数组,最多30个;不允许重复或空字符串", examples=[["shot_segment_id_1", "shot_segment_id_2"]], ) @field_validator("history_source") @classmethod def validate_history_source(cls, value: str) -> str: try: return normalize_generation_history_source(value).value except ValueError as exc: raise ValueError("history_source 不支持") from exc @field_validator("ids") @classmethod def validate_ids(cls, values: list[str]) -> list[str]: normalized = [str(item).strip() for item in values if str(item or "").strip()] if not normalized: raise ValueError("ids 不能为空") if len(normalized) > 30: raise ValueError("单次最多删除30条记录") if len(normalized) != len(set(normalized)): raise ValueError("ids 不允许重复") return normalized class GenerationAIHistoryBatchDeleteOut(BaseModel): """素材云历史批量删除响应体。""" model_config = ConfigDict( json_schema_extra={ "example": { "message": "删除成功", "history_source": "shot_replicate", "history_source_label": "拆镜复刻", "requested_count": 2, "deleted_count": 2, "requested_ids": ["shot_segment_id_1", "shot_segment_id_2"], "deleted_ids": ["shot_segment_id_1", "shot_segment_id_2"], "generation_record_ids": [], "chat_task_ids": ["chat_task_id_1", "chat_task_id_2"], "module_project_ids": ["module_project_id_1", "module_project_id_2"], "shot_segment_ids": ["shot_segment_id_1", "shot_segment_id_2"], "deleted": True, "freed_size_bytes": 123456, } } ) message: str = Field(..., description="操作结果提示信息") history_source: str = Field(..., description="素材云历史来源") history_source_label: str | None = Field(None, description="素材云历史来源中文名称") requested_count: int = Field(..., description="请求删除数量") deleted_count: int = Field(..., description="实际删除数量") requested_ids: list[str] = Field(default_factory=list, description="请求删除的原始ID列表") deleted_ids: list[str] = Field(default_factory=list, description="已删除的原始ID列表") generation_record_ids: list[str] = Field(default_factory=list, description="联动软删除的 GenerationRecord ID") chat_task_ids: list[str] = Field(default_factory=list, description="联动软删除的 ChatGenerationTask ID") module_project_ids: list[str] = Field(default_factory=list, description="联动软删除的 ModuleGenerationProject ID") shot_segment_ids: list[str] = Field(default_factory=list, description="联动软删除的 ShotReplicateSegment ID") deleted: bool = Field(..., description="是否已完成软删除") freed_size_bytes: int = Field(0, description="本次软删联动释放的有效资源空间字节数") class GenerationAIRetryOut(BaseModel): """AI生成任务重试响应体。""" model_config = ConfigDict( json_schema_extra={ "example": { "id": "0019e0a44895b6d837d", "status": "generating", "pipeline_stage": "queued", "message": "任务已重新投递", } } ) id: str = Field(..., description="被重试的任务ID") status: str = Field(..., description="重试后的任务状态") pipeline_stage: str | None = Field(None, description="重试后的任务流水线阶段") message: str = Field(..., description="操作结果提示信息") class GenerationAIRecordHistoryItemOut(BaseModel): """旧 generation_records 历史记录详情响应体。""" model_config = ConfigDict( json_schema_extra={ "example": { "id": "0019e0a44895b6d837d", "source_type": "generation_record", "project_id": "project_xxx", "project_name": "默认项目", "generated_resource_id": "generated_resource_xxx", "gen_type": "image", "generation_mode": "generation_record", "pipeline_stage": None, "status": "completed", "original_prompt": "生成一张赛博朋克风格的城市夜景", "optimized_prompt": "赛博朋克城市夜景,霓虹灯,电影感,高细节", "duration": None, "aspect_ratio": None, "resolution": None, "image_size": "2K", "image_proportion": "1:1", "image_px": "2048x2048", "references": None, "media_references": None, "provider_task_id": "provider_task_xxx", "seedance_task_id": "provider_task_xxx", "remote_result_url": None, "image_url": "https://example.com/result.png", "video_url": None, "video_cover_url": None, "engine_id": None, "engine_snapshot": None, "credits_cost": 10.0, "text_credits_cost": 1.0, "text_tokens_used": 100, "image_tokens_used": 0, "video_tokens_used": 0, "retry_count": 0, "poll_count": 0, "error_message": None, "created_at": "2026-05-27T10:12:00", "generated_at": "2026-05-27T10:15:30", } } ) id: str = Field(..., description="旧生成记录ID") source_type: Literal["generation_record"] = Field( "generation_record", description="历史记录来源固定为 generation_record,用于和 chat_task 历史区分", ) project_id: str | None = Field(None, description="旧项目ID,来源于 generation_records.project_id") project_name: str | None = Field(None, description="旧项目名称,来源于 projects.name;项目不存在时为空") generated_resource_id: str | None = Field( None, description="关联的生成资源账本ID,来源于 generated_resources.id;历史脏数据可能为空", ) file_name: str | None = Field(None, description="文件名,来源于 generated_resources.file_name") history_source: str | None = Field( None, description=( "素材云生成历史来源:chat_task=AI创作,generation_record=项目生成," "hot_opening_replicate=爆款开头复刻,shot_replicate=拆镜复刻。" "注意:上传素材历史不在本接口返回,请使用 /upload-resources/history。" ), ) history_source_label: str | None = Field(None, description="素材云历史来源中文名称") module_project_id: str | None = Field(None, description="模块生成项目ID;非模块生成历史返回 null") module_project_title: str | None = Field(None, description="模块生成项目标题;非模块生成历史返回 null") module_step_id: str | None = Field(None, description="模块生成步骤ID;非模块生成历史返回 null") module_step_code: str | None = Field(None, description="模块生成步骤编码;非模块生成历史返回 null") hot_opening_project_id: str | None = Field(None, description="爆款开头复刻项目ID;非爆款开头复刻返回 null") hot_opening_project_title: str | None = Field(None, description="爆款开头复刻项目标题;非爆款开头复刻返回 null") shot_replicate_project_id: str | None = Field(None, description="拆镜复刻项目ID;非拆镜复刻返回 null") shot_replicate_project_title: str | None = Field(None, description="拆镜复刻项目标题;非拆镜复刻返回 null") shot_task_set_id: str | None = Field(None, description="拆镜复刻总任务ID;非拆镜复刻返回 null") shot_segment_id: str | None = Field(None, description="拆镜复刻片段ID;非拆镜复刻返回 null") shot_segment_index: int | None = Field(None, description="拆镜复刻片段序号;非拆镜复刻返回 null") shot_segment_label: str | None = Field(None, description="拆镜复刻片段展示名称,例如:拆镜复刻片段1;非拆镜复刻返回 null") gen_type: str = Field(..., description="生成类型:image=图片,video=视频") generation_mode: str | None = Field( "generation_record", description="兼容新历史结构的生成模式字段。旧表历史固定返回 generation_record", ) pipeline_stage: str | None = Field( None, description="兼容新历史结构的流水线阶段字段。旧 generation_records 无该字段,固定为 null", ) status: str = Field(..., description="记录状态,例如 completed=已完成,failed=失败") original_prompt: str = Field(..., description="用户原始提示词") optimized_prompt: str | None = Field(None, description="优化后的提示词,可能为空") duration: int | None = Field(None, description="视频时长,单位秒。图片记录通常为空") aspect_ratio: str | None = Field(None, description="视频比例,例如 16:9。图片记录通常为空") resolution: str | None = Field(None, description="视频分辨率,例如 480p。图片记录通常为空") image_size: str | None = Field(None, description="图片分辨率档位,例如 2K") image_proportion: str | None = Field(None, description="图片比例,例如 1:1") image_px: str | None = Field(None, description="图片像素尺寸,例如 2048x2048") references: list[dict] | None = Field( None, description="旧接口字段名,来源于 generation_records.media_references 解析后的参考素材列表", ) media_references: list[dict] | None = Field( None, description="兼容新历史结构字段名,和 references 内容一致", ) provider_task_id: str | None = Field(None, description="第三方服务商任务ID,旧表使用 seedance_task_id 兼容填充") seedance_task_id: str | None = Field(None, description="旧服务命名的第三方任务ID字段") remote_result_url: str | None = Field( None, description="第三方远程结果地址。旧 generation_records 未保存该字段,固定为 null", ) image_url: str | None = Field(None, description="最终图片地址。图片记录完成后通常有值") video_url: str | None = Field(None, description="最终视频地址。视频记录完成后通常有值") video_cover_url: str | None = Field(None, description="视频封面图片地址。视频记录完成且封面截帧成功后通常有值") engine_id: str | None = Field( None, description="兼容新历史结构的引擎ID字段。旧 generation_records 未保存该字段,固定为 null", ) engine_snapshot: dict | None = Field( None, description="兼容新历史结构的引擎快照字段。旧 generation_records 未保存该字段,固定为 null", ) credits_cost: float = Field(0.0, description="本次记录总消耗积分") text_credits_cost: float = Field(0.0, description="文本优化或文本处理消耗积分") text_tokens_used: int = Field(0, description="文本 token 使用量") image_tokens_used: int = Field(0, description="图片 token 使用量") video_tokens_used: int = Field(0, description="视频 token 使用量") retry_count: int = Field( 0, description="兼容新历史结构的重试次数字段。旧 generation_records 未保存该字段,固定为 0", ) poll_count: int = Field( 0, description="兼容新历史结构的轮询次数字段。旧 generation_records 未保存该字段,固定为 0", ) error_message: str | None = Field(None, description="错误信息。成功记录一般为 null") created_at: NaiveDatetimeOptional = Field(None, description="记录创建时间") generated_at: NaiveDatetimeOptional = Field(None, description="生成完成时间") GenerationAIHistoryItemOut = Annotated[ GenerationAITaskOut | GenerationAIRecordHistoryItemOut, Field(discriminator="source_type"), ] class GenerationAIHistoryDayGroupOut(BaseModel): """AI生成历史按天分组响应项。""" model_config = ConfigDict( json_schema_extra={ "example": { "generated_date": "2026-05-27", "total": 18, "items": [], } } ) generated_date: str = Field(..., description="生成日期,格式:YYYY-MM-DD") total: int = Field(..., description="当前生成日期下的生成成功记录总数") items: list[GenerationAIHistoryItemOut] = Field( default_factory=list, description=( "当前生成日期下倒序前10条生成记录详情。" "history_source=chat_task/hot_opening_replicate/shot_replicate 时 item 为 GenerationAITaskOut;" "history_source=generation_record 时 item 为 GenerationAIRecordHistoryItemOut" ), ) class GenerationAIHistoryGroupedOut(BaseModel): """AI生成历史日期分组列表响应体。""" model_config = ConfigDict( json_schema_extra={ "example": { "total_days": 2, "page": 1, "page_size": 10, "groups": [ { "generated_date": "2026-05-27", "total": 18, "items": [], }, { "generated_date": "2026-05-26", "total": 6, "items": [], }, ], } } ) total_days: int = Field(..., description="当前生成类型下,用户一共有多少个生成日期分组") page: int = Field(..., description="当前日期分组分页页码") page_size: int = Field(..., description="当前每页返回的日期分组数量,最大10") groups: list[GenerationAIHistoryDayGroupOut] = Field( default_factory=list, description="按生成日期倒序排列的历史记录分组列表", ) class GenerationAIHistoryDayItemsOut(BaseModel): """指定日期下AI生成历史分页响应体。""" model_config = ConfigDict( json_schema_extra={ "example": { "generated_date": "2026-05-27", "total": 18, "page": 2, "page_size": 10, "items": [], } } ) generated_date: str = Field(..., description="当前查询的生成日期,格式:YYYY-MM-DD") total: int = Field(..., description="当前日期下的生成成功记录总数") page: int = Field(..., description="当前日期下的记录分页页码") page_size: int = Field(..., description="当前日期下每页返回的生成记录数量") items: list[GenerationAIHistoryItemOut] = Field( default_factory=list, description=( "当前日期下的生成记录详情列表,按 generated_at 倒序排列。" "history_source=chat_task/hot_opening_replicate/shot_replicate 时 item 为 GenerationAITaskOut;" "history_source=generation_record 时 item 为 GenerationAIRecordHistoryItemOut" ), )