from rest_framework import serializers from .models import ( AITask, CreationConversation, CreationMessage, ImageConversation, ModelConfig, ModelProvider, ) # 仅把可解释的阶段名返回给创作页。模型原始 reasoning 可能冗长、跑题或包含内部工作草稿, # 不能直接作为用户可见内容;前端据此展示「正在做什么」,避免异步规划看起来像卡住。 _PUBLIC_AGENT_PROGRESS = { "starting": { "label": "正在读取创作要求", "detail": "已收到素材、角色与时长设定", }, "reasoning": { "label": "正在分析素材与创作方向", "detail": "正在结合商品、角色和目标时长梳理方案", }, "search_library": { "label": "正在查找可用素材", "detail": "正在核对当前创作需要的素材信息", }, "write_strategy": { "label": "正在确定创作策略", "detail": "正在整理受众、核心卖点和整体表达方向", }, "write_plan": { "label": "正在编排视频方案", "detail": "正在安排分段节奏,并锁定角色和商品的一致性", }, "write_prompt": { "label": "正在整理出片指令", "detail": "正在把方案转成可直接生成的视频脚本", }, "generate_image": { "label": "正在准备生成画面", "detail": "正在整理画面参考与生成条件", }, "ask_user": { "label": "正在核对关键设定", "detail": "正在确认会影响成片效果的信息", }, "responding": { "label": "正在整理回复内容", "detail": "马上把下一步呈现给你", }, } _PUBLIC_REASONING_DETAILS = { "brief": "正在理解这次创作的重点和限制", "product": "正在提炼商品卖点与可呈现的真实证据", "cast": "正在安排角色出镜方式与人物关系", "structure": "正在检查时长、分段节奏和前后衔接", "consistency": "正在锁定人物与商品在各段的一致性", "shots": "正在细化镜头、动作和出片表达", } def _public_agent_progress(phase: str, detail_key: str = "") -> dict[str, str]: """把持久化的阶段键转换成用户可见的受控摘要。""" progress = dict(_PUBLIC_AGENT_PROGRESS.get(phase, _PUBLIC_AGENT_PROGRESS["starting"])) if phase == "reasoning": progress["detail"] = _PUBLIC_REASONING_DETAILS.get(detail_key, progress["detail"]) return progress class ModelProviderSerializer(serializers.ModelSerializer): class Meta: model = ModelProvider fields = ["id", "name", "display_name", "status", "base_url", "metadata"] read_only_fields = fields class ModelConfigSerializer(serializers.ModelSerializer): provider = ModelProviderSerializer(read_only=True) class Meta: model = ModelConfig fields = ["id", "provider", "name", "display_name", "capability", "endpoint", "unit_price", "status", "is_default", "metadata"] read_only_fields = fields class ImageConversationSerializer(serializers.ModelSerializer): """图片创作对话:左栏列表用。title 可写(重命名),mode/product 创建时可指定。""" task_count = serializers.SerializerMethodField() class Meta: model = ImageConversation fields = [ "id", "title", "mode", "product", "task_count", "is_deleted", "purged_at", "last_active_at", "created_at", "updated_at", ] read_only_fields = ["id", "task_count", "is_deleted", "purged_at", "last_active_at", "created_at", "updated_at"] def get_task_count(self, obj) -> int: # list 接口已 annotate;无 annotate 时回落实时 count(详情/创建场景) cached = getattr(obj, "_task_count", None) return cached if cached is not None else obj.tasks.count() class ImageConversationTrashSerializer(ImageConversationSerializer): """垃圾桶专用对话契约:只额外暴露一张展示封面,不影响普通对话列表查询。""" cover_preview_url = serializers.SerializerMethodField() class Meta(ImageConversationSerializer.Meta): fields = [*ImageConversationSerializer.Meta.fields, "cover_preview_url"] read_only_fields = [*ImageConversationSerializer.Meta.read_only_fields, "cover_preview_url"] def get_cover_preview_url(self, obj) -> str: return str(getattr(obj, "_trash_cover_preview_url", "") or "") class AITaskSerializer(serializers.ModelSerializer): model_config = ModelConfigSerializer(read_only=True) # 从 request_payload 抽出的分组/标签信息(由 AITaskViewSet annotate 提供;其它调用处无此注解则为 None)。 # 不直接读 obj.request_payload —— 那列已 defer,读了会触发懒加载把几 MB payload 整列拉回。 batch_id = serializers.SerializerMethodField() mode = serializers.SerializerMethodField() def get_batch_id(self, obj): return getattr(obj, "rp_batch_id", None) def get_mode(self, obj): return getattr(obj, "rp_mode", None) class Meta: model = AITask fields = [ "id", "project", "task_type", "batch_id", "mode", "status", "model_config", "provider_task_id", "estimated_cost", "actual_cost", "error_code", "error_message", "submitted_at", "completed_at", "is_deleted", "purged_at", "created_at", "updated_at", ] # batch_id / mode 是显式声明的 SerializerMethodField(本就只读),不能再列进 read_only_fields(DRF 会报错) read_only_fields = [f for f in fields if f not in ("batch_id", "mode")] class CreationMessageSerializer(serializers.ModelSerializer): """全能创作对话流里的一条消息。前端**按 kind 分发到不同卡片组件**, 结构化内容一律在 payload 里(契约 §2),不要从 text 里解析。""" class Meta: model = CreationMessage fields = ["id", "role", "kind", "text", "payload", "refs", "task", "seq", "created_at"] read_only_fields = fields class CreationConversationSerializer(serializers.ModelSerializer): """会话列表 / 详情。title 可写(重命名);params 创建后也可改(对话页改模型/比例后立刻生效); mode 创建后不可改。""" message_count = serializers.SerializerMethodField() cover_url = serializers.SerializerMethodField() agent_progress = serializers.SerializerMethodField() class Meta: model = CreationConversation fields = [ "id", "title", "mode", "preset", "params", "status", "agent_status", "agent_started_at", "agent_progress", "message_count", "cover_url", "last_active_at", "created_at", "updated_at", ] read_only_fields = [ "id", "status", "agent_status", "agent_started_at", "agent_progress", "message_count", "cover_url", "last_active_at", "created_at", "updated_at", ] def get_message_count(self, obj) -> int: cached = getattr(obj, "_message_count", None) return cached if cached is not None else obj.messages.count() def get_cover_url(self, obj) -> str: """历史页封面 = **最新一版**结果(重生成是往下叠加,所以取最后一条 RESULT)。""" last = ( obj.messages.filter(kind=CreationMessage.Kind.RESULT) .order_by("-seq") .values_list("payload", flat=True) .first() ) if not last: return "" assets = (last or {}).get("assets") or [] if not assets: return "" first = assets[0] or {} return first.get("cover") or first.get("url") or "" def get_agent_progress(self, obj): """规划期间只返回受控阶段文案,绝不把模型原始 thinking 暴露给页面。""" if obj.agent_status != CreationConversation.AgentStatus.PLANNING: return None memory = obj.memory if isinstance(obj.memory, dict) else {} phase = str(memory.get("agent_progress_phase") or "") if not phase: return None detail_key = str(memory.get("agent_progress_detail_key") or "") progress = _public_agent_progress(phase, detail_key) history: list[dict[str, str]] = [] for item in memory.get("agent_progress_history") or []: if not isinstance(item, dict): continue history.append( _public_agent_progress( str(item.get("phase") or ""), str(item.get("detail_key") or ""), ) ) # 兼容服务端升级前已开始的轮次:至少显示当前一条进度。 if not history or history[-1] != progress: history.append(progress) progress["history"] = history[-12:] return progress def update(self, instance, validated_data): # mode 定死:允许传但忽略,避免前端误改后顶栏参数与已生成内容对不上 validated_data.pop("mode", None) return super().update(instance, validated_data) class CreationConversationDetailSerializer(CreationConversationSerializer): """详情:带全量消息,进对话页一次性回填。""" messages = CreationMessageSerializer(many=True, read_only=True) pinned_refs = serializers.JSONField(read_only=True) class Meta(CreationConversationSerializer.Meta): fields = [*CreationConversationSerializer.Meta.fields, "messages", "pinned_refs"] read_only_fields = [ *CreationConversationSerializer.Meta.read_only_fields, "messages", "pinned_refs", ]