feat(admin): Phase 4 质量词 Admin(平台单层) — QualityWord + 生成侧读配置(无配置回落) + 质量词页

后端:apps/ai QualityWord(stage/slot/text/sort/enabled)+ migration;services.py quality_words/quality_suffix,
4 个 builder 接入(person/model_tryon/storyboard/video)无配置回落原写死值零回归;adminpanel CRUD + 审计。
前端:adminApi quality 系列;Admin 质量词页(按 stage 分组卡 + 编辑模式开关 + 行内增改删 chip)。
测试:adminpanel 23 + accounts 22 = 45 单测过(CRUD/权限/生成读配置+回落/停用词不取);
无头 e2e _admin-p4.mjs 4 断言过 + 0 console error + live 库零残留;tsc+build 绿。
注:apps.ai 3 个 image_edit 路由测试失败为进场前既有(已 stash 比对 HEAD 同样失败),非本 Phase 引入。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
seaislee1209
2026-06-19 21:52:41 +08:00
co-authored by Claude Opus 4.8
parent 6560633959
commit 01d4f235ed
14 changed files with 535 additions and 5 deletions
+28 -4
View File
@@ -599,6 +599,26 @@ def _product_cover_url(product) -> str:
return ""
def quality_words(stage: str, slot: str = "quality") -> list[str]:
"""平台单层质量词配置(QualityWord)。无配置/表不存在 → 返回 [],调用方回落写死值,保证零回归。"""
try:
from apps.ai.models import QualityWord
return list(
QualityWord.objects.filter(stage=stage, slot=slot, enabled=True)
.order_by("sort", "created_at")
.values_list("text", flat=True)
)
except Exception: # noqa: BLE001 — 配置读取失败绝不阻断生成
return []
def quality_suffix(stage: str, fallback: str, slot: str = "quality", sep: str = "") -> str:
"""取该阶段配置的质量词拼成尾串;无配置回落到 fallback(各 builder 的原写死值)。"""
words = quality_words(stage, slot)
return sep.join(words) if words else fallback
def build_product_triview_prompt_refs(product, base_prompt: str = "") -> str:
"""商品三视图 image_edit 提示词(refs 版):参考图1=商品真实主图,锁包装一致性。"""
name = (getattr(product, "title", "") or "商品").strip()
@@ -630,7 +650,7 @@ def build_model_tryon_prompt_refs(product, has_model: bool, base_prompt: str = "
"请生成一位真人模特自然地展示/佩戴/使用参考图1中商品的电商效果图。",
"商品的外形、品牌文字、配色、Logo 必须与参考图1高度一致,不要改动或重新设计。",
]
lines.append("自然光、真实质感、干净背景、电商主图构图,人物与商品比例真实协调。")
lines.append(quality_suffix("model_tryon", "自然光、真实质感、干净背景、电商主图构图,人物与商品比例真实协调。"))
if base_prompt and base_prompt.strip():
lines.append(base_prompt.strip())
return " ".join(lines)
@@ -662,7 +682,8 @@ def build_person_frontal_prompt(description: str = "") -> str:
用户钦定格式:电商真人模特,氛围正面全身照,<描述>,自然妆容,柔和影棚光,真实质感,单人,纯色背景。"""
desc = (description or "").strip()
inner = f"{desc}" if desc else ""
return f"电商真人模特,氛围正面全身照,{inner}自然妆容,柔和影棚光,真实质感,单人,纯色背景"
tail = quality_suffix("person", "自然妆容,柔和影棚光,真实质感,单人,纯色背景")
return f"电商真人模特,氛围正面全身照,{inner}{tail}"
def generate_base_asset(*, project, user, kind: str, prompt: str, label: str = "", group_id: str | None = None) -> AITask:
@@ -896,7 +917,7 @@ def build_storyboard_frame_prompt(project, version, segment) -> str:
]
if version.prompt:
lines.append(version.prompt.strip())
lines.append("电商竖屏 9:16 导演故事板,真实清新、生活化,画面清晰,可直接指导视频生成。")
lines.append(quality_suffix("storyboard", "电商竖屏 9:16 导演故事板,真实清新、生活化,画面清晰,可直接指导视频生成。"))
return "\n".join(line for line in lines if line)
@@ -923,7 +944,10 @@ def build_video_segment_prompt(project, video_segment, scene, refs, user_prompt:
if extra:
script_text = (script_text + "\n" + extra).strip() if script_text else extra
lines.append("【脚本】" + (script_text or f"{video_segment.sort_order + 1}"))
lines.append(f"{video_segment.target_duration_seconds}s · 9:16 竖屏电商带货短视频,镜头稳定,商品露出清晰,节奏有转化感。")
lines.append(
f"{video_segment.target_duration_seconds}s · "
+ quality_suffix("video", "9:16 竖屏电商带货短视频,镜头稳定,商品露出清晰,节奏有转化感。")
)
return "\n".join(line for line in lines if line)