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:
co-authored by
Claude Opus 4.8
parent
304a083184
commit
9c2a3bc365
@@ -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。"""
|
||||
|
||||
|
||||
@@ -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。"""
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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"}
|
||||
|
||||
Reference in New Issue
Block a user