角色库导入打标签并支持筛选;模特库与全能创作角色/商品选择改为每页 20 条分页。临时限制成片 ≤60 秒,过滤对话里的超长时长选项,并收拢本地全能创作与后台用户相关修复。
256 lines
9.7 KiB
Python
256 lines
9.7 KiB
Python
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",
|
|
]
|