诉求:生图/视频提示词全写死在代码里,产品方看不到也改不了(之前那版只做成「质量后缀」不是正文)。
现新增可编辑「提示词模板」体系,覆盖视频项目生产线 6 条:人物立绘 / 人物三视图 / 商品三视图 /
场景图 / 分镜图 / 视频。
后端:
- 新模型 PromptTemplate(key 固定 6 条 + template 正文 + ratio 比例 + enabled);迁移 0012/0013
种入默认正文(= 各 builder 现写死文本,零回归)。
- render_prompt(key, default, **字段):读 DB 模板,留空/停用回落默认;安全替换 {占位符}(中文名),
未知占位符原样保留(改错不致命)。prompt_ratio_size:比例→gpt-image 尺寸。
- 6 个 builder/调用点改走模板;5 条图比例可改(gpt-image 仅 1:1/竖/横),视频固定 Seedance 9:16。
场景图新增可编辑「场景风格」包装(默认仅 {场景描述},零回归)。人物/分镜/视频原质量后缀并入模板。
- adminpanel CRUD:GET /prompt-templates、PATCH /<id>(改正文/比例/启用,key 不可改、不增删,带审计)。
前端:admin 新增「提示词」侧栏页(/admin/prompts):6 卡,每卡=占位符提示+正文文本框+比例按钮+
启用开关+保存。复用 .btn/.card-hard 共享类,仅 token 配色,单橙锚点。
验证:Django 套件 102 全绿(含新模型/API/生成侧立即生效 + 人物提示词改读模板);tsc+vite build 绿;
无头(独立 sqlite 栈)真开 /admin/prompts:6 卡齐、文本框/比例/占位符齐、0 控制台错误。
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
203 lines
7.9 KiB
Python
203 lines
7.9 KiB
Python
from decimal import Decimal
|
|
|
|
from rest_framework import serializers
|
|
|
|
from apps.accounts.models import Team, TeamMember, User
|
|
from apps.ai.models import AITask, ModelConfig, ModelProvider, PromptTemplate, QualityWord
|
|
from apps.assets.models import Asset
|
|
from apps.billing.models import CreditLedger, QuotaPolicy
|
|
from apps.projects.models import Project
|
|
|
|
# 成本异常阈值:实际成本 > 预估 × 此倍数(且预估 > 0)即标异常
|
|
COST_ANOMALY_RATIO = Decimal("1.5")
|
|
|
|
|
|
def is_cost_anomaly(estimated, actual) -> bool:
|
|
est = estimated or Decimal("0")
|
|
act = actual or Decimal("0")
|
|
return bool(est > 0 and act > est * COST_ANOMALY_RATIO)
|
|
|
|
|
|
class QualityWordSerializer(serializers.ModelSerializer):
|
|
class Meta:
|
|
model = QualityWord
|
|
fields = ["id", "stage", "slot", "text", "sort", "enabled", "created_at"]
|
|
read_only_fields = ["id", "created_at"]
|
|
|
|
|
|
# 每条提示词可用的 {占位符}(给 admin 提示;运行时由对应 builder 注入,改错会原样保留不致命)
|
|
PROMPT_PLACEHOLDERS = {
|
|
"person_portrait": ["描述"],
|
|
"person_triview": [],
|
|
"product_triview": ["商品", "补充"],
|
|
"scene": ["场景描述"],
|
|
"storyboard_frame": ["设定", "场景上下文", "时长", "脚本", "补充"],
|
|
"video_segment": ["设定", "分镜", "脚本", "时长"],
|
|
}
|
|
|
|
|
|
class PromptTemplateSerializer(serializers.ModelSerializer):
|
|
label = serializers.CharField(source="get_key_display", read_only=True)
|
|
placeholders = serializers.SerializerMethodField()
|
|
|
|
class Meta:
|
|
model = PromptTemplate
|
|
fields = ["id", "key", "label", "template", "ratio", "enabled", "placeholders", "updated_at"]
|
|
read_only_fields = ["id", "key", "label", "placeholders", "updated_at"] # key 固定,只改正文/比例/启用
|
|
|
|
def get_placeholders(self, obj) -> list:
|
|
return PROMPT_PLACEHOLDERS.get(obj.key, [])
|
|
|
|
|
|
class AdminReviewAssetSerializer(serializers.ModelSerializer):
|
|
"""跨团队人像审核队列行:复用 AssetFileSerializer 的首图 preview_url。"""
|
|
|
|
team_name = serializers.CharField(source="team.name", read_only=True, default=None)
|
|
preview_url = serializers.SerializerMethodField()
|
|
|
|
class Meta:
|
|
model = Asset
|
|
fields = ["id", "name", "category", "review_status", "review_error", "team", "team_name", "preview_url", "created_at"]
|
|
read_only_fields = fields
|
|
|
|
def get_preview_url(self, obj):
|
|
from apps.assets.serializers import AssetFileSerializer
|
|
|
|
# 用 prefetch 缓存(list(...))而非 .first():后者会另发查询,列表页 N+1 拖慢到数秒
|
|
files = list(obj.files.all())
|
|
return AssetFileSerializer(files[0]).data.get("preview_url", "") if files else ""
|
|
|
|
|
|
class AdminTeamSerializer(serializers.ModelSerializer):
|
|
owner_username = serializers.CharField(source="owner.username", read_only=True, default=None)
|
|
member_count = serializers.SerializerMethodField()
|
|
balance = serializers.SerializerMethodField()
|
|
|
|
class Meta:
|
|
model = Team
|
|
fields = ["id", "name", "status", "owner", "owner_username", "member_count", "balance", "created_at"]
|
|
read_only_fields = fields
|
|
|
|
def get_member_count(self, obj):
|
|
anno = getattr(obj, "member_count_anno", None)
|
|
return anno if anno is not None else obj.members.count()
|
|
|
|
def get_balance(self, obj):
|
|
acct = getattr(obj, "credit_account", None)
|
|
return str(acct.balance) if acct is not None else "0"
|
|
|
|
|
|
class AdminTeamMemberSerializer(serializers.ModelSerializer):
|
|
username = serializers.CharField(source="user.username", read_only=True)
|
|
user_status = serializers.CharField(source="user.status", read_only=True)
|
|
|
|
class Meta:
|
|
model = TeamMember
|
|
fields = ["id", "username", "role", "status", "user_status", "monthly_credit_limit"]
|
|
read_only_fields = fields
|
|
|
|
|
|
class AdminUserSerializer(serializers.ModelSerializer):
|
|
teams = serializers.SerializerMethodField()
|
|
|
|
class Meta:
|
|
model = User
|
|
fields = ["id", "username", "status", "is_platform_admin", "date_joined", "teams"]
|
|
read_only_fields = fields
|
|
|
|
def get_teams(self, obj):
|
|
return [
|
|
{"team_id": str(m.team_id), "team_name": m.team.name, "role": m.role}
|
|
for m in obj.team_memberships.select_related("team").all()
|
|
]
|
|
|
|
|
|
class AdminTaskSerializer(serializers.ModelSerializer):
|
|
team_name = serializers.CharField(source="team.name", read_only=True, default=None)
|
|
model_name = serializers.CharField(source="model_config.name", read_only=True, default=None)
|
|
cost_anomaly = serializers.SerializerMethodField()
|
|
|
|
class Meta:
|
|
model = AITask
|
|
fields = [
|
|
"id", "task_type", "status", "team", "team_name", "model_name",
|
|
"estimated_cost", "actual_cost", "cost_anomaly", "error_code", "created_at",
|
|
]
|
|
read_only_fields = fields
|
|
|
|
def get_cost_anomaly(self, obj) -> bool:
|
|
return is_cost_anomaly(obj.estimated_cost, obj.actual_cost)
|
|
|
|
|
|
class AdminTaskDetailSerializer(AdminTaskSerializer):
|
|
class Meta(AdminTaskSerializer.Meta):
|
|
fields = AdminTaskSerializer.Meta.fields + [
|
|
"project", "idempotency_key", "request_payload", "response_payload",
|
|
"error_message", "submitted_at", "completed_at",
|
|
]
|
|
read_only_fields = fields
|
|
|
|
|
|
class AdminLedgerSerializer(serializers.ModelSerializer):
|
|
team_name = serializers.CharField(source="team.name", read_only=True, default=None)
|
|
username = serializers.CharField(source="user.username", read_only=True, default=None)
|
|
|
|
class Meta:
|
|
model = CreditLedger
|
|
fields = ["id", "team", "team_name", "username", "ledger_type", "amount", "balance_after", "reason", "created_at"]
|
|
read_only_fields = fields
|
|
|
|
|
|
class AdminQuotaPolicySerializer(serializers.ModelSerializer):
|
|
team_name = serializers.CharField(source="team.name", read_only=True, default=None)
|
|
|
|
class Meta:
|
|
model = QuotaPolicy
|
|
fields = [
|
|
"id", "team", "team_name", "user", "project",
|
|
"monthly_limit", "project_limit", "per_task_limit", "is_active", "created_at",
|
|
]
|
|
read_only_fields = ["id", "team_name", "created_at"]
|
|
|
|
|
|
class AdminModelProviderSerializer(serializers.ModelSerializer):
|
|
has_api_key = serializers.SerializerMethodField()
|
|
model_count = serializers.SerializerMethodField()
|
|
|
|
class Meta:
|
|
model = ModelProvider
|
|
fields = ["id", "name", "display_name", "status", "base_url", "api_key", "metadata", "has_api_key", "model_count", "created_at"]
|
|
# api_key 只写不读(密钥不回传前端)
|
|
extra_kwargs = {"api_key": {"write_only": True, "required": False}}
|
|
read_only_fields = ["id", "has_api_key", "model_count", "created_at"]
|
|
|
|
def get_has_api_key(self, obj) -> bool:
|
|
return bool(obj.api_key)
|
|
|
|
def get_model_count(self, obj) -> int:
|
|
anno = getattr(obj, "model_count_anno", None)
|
|
return anno if anno is not None else obj.models.count()
|
|
|
|
|
|
class AdminModelConfigSerializer(serializers.ModelSerializer):
|
|
provider_name = serializers.CharField(source="provider.name", read_only=True, default=None)
|
|
|
|
class Meta:
|
|
model = ModelConfig
|
|
fields = [
|
|
"id", "provider", "provider_name", "name", "display_name", "capability",
|
|
"endpoint", "unit_price", "status", "is_default", "rate_limit_per_minute", "metadata", "created_at",
|
|
]
|
|
# is_default 只经 set-default 端点改,不在普通编辑里直接写
|
|
read_only_fields = ["id", "provider_name", "is_default", "created_at"]
|
|
|
|
|
|
class AdminProjectSerializer(serializers.ModelSerializer):
|
|
team_name = serializers.CharField(source="team.name", read_only=True, default=None)
|
|
product_title = serializers.CharField(source="product.title", read_only=True, default=None)
|
|
|
|
class Meta:
|
|
model = Project
|
|
fields = ["id", "name", "team", "team_name", "product_title", "status", "current_stage", "created_at"]
|
|
read_only_fields = fields
|