feat(prompts): admin「提示词」页 —— 视频线 6 条生图/视频提示词正文+比例可在后台改,不再写死

诉求:生图/视频提示词全写死在代码里,产品方看不到也改不了(之前那版只做成「质量后缀」不是正文)。
现新增可编辑「提示词模板」体系,覆盖视频项目生产线 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>
This commit is contained in:
seaislee1209
2026-06-23 03:19:07 +08:00
co-authored by Claude Opus 4.8
parent 304a083184
commit 9c2a3bc365
13 changed files with 485 additions and 64 deletions
+25 -1
View File
@@ -3,7 +3,7 @@ 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, QualityWord
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
@@ -25,6 +25,30 @@ class QualityWordSerializer(serializers.ModelSerializer):
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。"""
+40 -11
View File
@@ -210,21 +210,24 @@ class AdminQualityWordTests(TestCase):
r = self.ac.post("/api/admin/quality-words/", {"stage": "bogus", "text": "x"}, format="json")
self.assertEqual(r.status_code, 400)
def test_generation_reads_config_with_fallback(self):
from apps.ai.models import QualityWord
def test_person_prompt_reads_template_with_fallback(self):
"""人物立绘提示词改由「提示词模板」(person_portrait)驱动,取代旧质量词:
种子模板 = 原默认正文(描述填进 {描述});改模板 → 生成立即用新正文。"""
from apps.ai.models import PromptTemplate
from apps.ai.services import build_person_frontal_prompt
# 无配置 → 回落写死值
self.assertIn("纯色背景", build_person_frontal_prompt("小姐姐"))
# 配置后 → 生成用新词,描述仍保留
QualityWord.objects.create(stage="person", text="赛博朋克风", sort=0)
QualityWord.objects.create(stage="person", text="霓虹光", sort=1)
# 种子模板 → 含默认尾串,{描述} 被填入
out = build_person_frontal_prompt("小姐姐")
self.assertIn("赛博朋克风", out)
self.assertIn("霓虹光", out)
self.assertIn("纯色背景", out)
self.assertIn("小姐姐", out)
self.assertNotIn("纯色背景", out)
# 后台改模板 → 生成用新正文,{描述} 仍替换
PromptTemplate.objects.filter(key="person_portrait").update(template="赛博朋克风{描述}霓虹光")
out2 = build_person_frontal_prompt("小姐姐")
self.assertIn("赛博朋克风", out2)
self.assertIn("霓虹光", out2)
self.assertIn("小姐姐", out2)
self.assertNotIn("纯色背景", out2)
def test_disabled_word_not_used(self):
from apps.ai.models import QualityWord
@@ -236,6 +239,32 @@ class AdminQualityWordTests(TestCase):
self.assertIn("启用词", words)
self.assertNotIn("停用词", words)
def test_prompt_templates_list_update_permission(self):
"""提示词模板:列表(6 条,带 label/placeholders)+ 改正文比例 + 权限 + 审计 + 生成侧立即生效。"""
from apps.ai.services import build_product_triview_prompt_refs
self.assertEqual(self.nc.get("/api/admin/prompt-templates/").status_code, 403)
lst = self.ac.get("/api/admin/prompt-templates/")
self.assertEqual(lst.status_code, 200)
self.assertEqual(len(lst.data), 6) # 视频线 6 条
row = next(r for r in lst.data if r["key"] == "product_triview")
self.assertEqual(row["label"], "商品三视图")
self.assertIn("商品", row["placeholders"])
up = self.ac.patch(
f"/api/admin/prompt-templates/{row['id']}/",
{"template": "参考图1是「{商品}」是商品。生成三视图。", "ratio": "portrait"},
format="json",
)
self.assertEqual(up.status_code, 200)
self.assertTrue(AdminAuditLog.objects.filter(action="prompt_template.update").exists())
class P:
title = "杂鱼煲"
out = build_product_triview_prompt_refs(P(), "")
self.assertEqual(out, "参考图1是「杂鱼煲」是商品。生成三视图。") # 生成侧立即用新模板,且 {商品} 已替换
class AdminAssetReviewTests(TestCase):
"""Phase 5:跨团队人像审核队列(列/筛/批量送审/轮询)+ 权限。火山调用全 mock。"""
+4
View File
@@ -19,6 +19,8 @@ from .views import (
admin_task_detail,
admin_task_retry,
admin_tasks,
admin_prompt_template_detail,
admin_prompt_templates,
admin_quality_word_detail,
admin_quality_words,
admin_revoke_invitation,
@@ -42,6 +44,8 @@ urlpatterns = [
path("users/<uuid:user_id>/reset-password/", admin_user_reset_password, name="admin-user-reset-password"),
path("quality-words/", admin_quality_words, name="admin-quality-words"),
path("quality-words/<uuid:word_id>/", admin_quality_word_detail, name="admin-quality-word-detail"),
path("prompt-templates/", admin_prompt_templates, name="admin-prompt-templates"),
path("prompt-templates/<uuid:template_id>/", admin_prompt_template_detail, name="admin-prompt-template-detail"),
path("asset-reviews/", admin_asset_reviews, name="admin-asset-reviews"),
path("asset-reviews/submit/", admin_asset_reviews_submit, name="admin-asset-reviews-submit"),
path("asset-reviews/poll/", admin_asset_reviews_poll, name="admin-asset-reviews-poll"),
+36 -1
View File
@@ -12,7 +12,7 @@ from apps.accounts.audit import log_admin_action
from apps.accounts.models import Invitation, Team, User
from apps.accounts.permissions import IsPlatformAdmin
from apps.accounts.serializers import InvitationSerializer
from apps.ai.models import AITask, ModelConfig, ModelProvider, QualityWord
from apps.ai.models import AITask, ModelConfig, ModelProvider, PromptTemplate, QualityWord
from apps.assets.models import Asset
from apps.assets.review import poll_asset_review, submit_asset_for_review
from apps.billing.models import CreditLedger, QuotaPolicy
@@ -34,6 +34,7 @@ from .serializers import (
AdminTeamMemberSerializer,
AdminTeamSerializer,
AdminUserSerializer,
PromptTemplateSerializer,
QualityWordSerializer,
)
@@ -281,6 +282,40 @@ def admin_quality_word_detail(request, word_id):
return Response(QualityWordSerializer(obj).data)
# ─────────────────────────── 生图/视频 提示词模板 ───────────────────────────
@api_view(["GET"])
@permission_classes([IsPlatformAdmin])
def admin_prompt_templates(request):
"""列视频线 6 条提示词模板(固定 key,数量小不分页,按 key 排序)。"""
qs = PromptTemplate.objects.all().order_by("key")
return Response(PromptTemplateSerializer(qs, many=True).data)
@api_view(["PATCH"])
@permission_classes([IsPlatformAdmin])
def admin_prompt_template_detail(request, template_id):
"""改一条提示词模板的正文 / 比例 / 启用(key 固定不可改,不增不删)。"""
obj = PromptTemplate.objects.filter(id=template_id).first()
if obj is None:
return Response({"detail": "not found"}, status=status.HTTP_404_NOT_FOUND)
before = {"template": obj.template, "ratio": obj.ratio, "enabled": obj.enabled}
serializer = PromptTemplateSerializer(obj, data=request.data, partial=True)
serializer.is_valid(raise_exception=True)
serializer.save()
log_admin_action(
request,
"prompt_template.update",
target_type="prompt_template",
target_id=obj.id,
target_name=obj.key,
before=before,
after={"template": obj.template, "ratio": obj.ratio, "enabled": obj.enabled},
)
return Response(PromptTemplateSerializer(obj).data)
# ─────────────────────────── 火山人像审核队列 ───────────────────────────
_REVIEW_STATUSES = {"", "processing", "active", "failed"}