feat(video): 期3 视频趴 — 人物→角色 + 引用模特库/自动入库 + 分镜合规归类 + 资产库素材

后端:
- B 路:视频流程新生成角色立绘+三视图成套 → 自动入模特库(run_triview_task,幂等 additive)
- 合规闭合:三视图 person→tri_view、分镜图 scene→storyboard,两者均加 on_commit 送审(此前只有角色立绘送审,三视图/分镜图漏审 = 合规缺口)
- 资产库 _tab_q:成片 finals→素材 materials(所有视频排除最终成片;最终成片隐藏不列)
- 单测 TriviewAutoEnrollTests:自动入库 + 幂等 + 三视图落审核范围

前端:
- 视频项目资产趴「人物」→「角色」(KIND_LABEL,区块名/新增/空态/详情弹窗一处全改;向导人设 persona 不动)
- A 路:演员库收编模特库 —— 顶部「模特库」tab 取自模特库实体(含官方跨团队模特),选一个即用其形象图作角色参考
- 资产库 library:人物→角色、成片 tab→素材、分类标签补全(模特上身图/平台套图/自由创作/视频素材/最终成片)
- vite.config:端口与后端代理目标可用 VITE_PORT/VITE_API_TARGET 覆盖(默认不变),避免多实例撞车

验收:后端单测全绿(基线既有 7 失败零新增);tsc+build 全绿;无头 0 console error
(资产库角色/素材改名、最终成片隐藏;pipeline 资产趴显角色;演员库模特库 tab 含 5 模特)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
seaislee1209
2026-06-21 19:27:23 +08:00
co-authored by Claude Opus 4.8
parent c8f3f91d38
commit c6e9c7e77b
8 changed files with 192 additions and 27 deletions
+9 -8
View File
@@ -13,7 +13,7 @@ const LIB_PAGE_SIZE = 10;
// asset.source / asset.asset_type / asset.category → 中文标签(筛选下拉 + 卡片 meta 用,不给用户看裸枚举)
const SOURCE_LABELS: Record<string, string> = { upload: "上传", ai_generated: "AI 生成", exported: "导出", system: "系统" };
const KIND_LABELS: Record<string, string> = { image: "图片", video: "视频", audio: "音频", subtitle: "字幕", document: "文档" };
const CATEGORY_LABELS: Record<string, string> = { person: "人物", scene: "场景", product_image: "商品图", video_clip: "视频片段", final_video: "成片", upload: "上传", uncategorized: "未分类" };
const CATEGORY_LABELS: Record<string, string> = { person: "角色", scene: "场景", product_image: "商品图", model_tryon: "模特上身图", platform_kit: "平台套图", free_create: "自由创作", video_clip: "视频素材", final_video: "最终成片", upload: "上传", uncategorized: "未分类" };
// 上传文件 → 资产类型:按 MIME 推断,字幕按后缀,兜底文档(后端直接存这个值,不能一律写死 image)
function inferAssetType(file: File): string {
@@ -35,12 +35,13 @@ const UPLOAD_KINDS: Array<{ value: string; label: string; accept: string; hint:
];
// 图片趴三类(期2):tryon=模特上身图 / kits=平台套图 / creations=自由创作
type LibTab = "people" | "scenes" | "products" | "tryon" | "kits" | "creations" | "finals" | "uploads" | "unclassified";
// 期3:人物→角色;成片 tab→素材(视频素材,排除最终成片;最终成片隐藏不列)
type LibTab = "people" | "scenes" | "products" | "tryon" | "kits" | "creations" | "materials" | "uploads" | "unclassified";
const LIB_TABS: Array<{ key: LibTab; label: string }> = [
{ key: "people", label: "人物" }, { key: "scenes", label: "场景" }, { key: "products", label: "商品图" },
{ key: "people", label: "角色" }, { key: "scenes", label: "场景" }, { key: "products", label: "商品图" },
{ key: "tryon", label: "模特上身图" }, { key: "kits", label: "平台套图" }, { key: "creations", label: "自由创作" },
{ key: "finals", label: "成片" }, { key: "uploads", label: "我的上传" }, { key: "unclassified", label: "未分类" }
{ key: "materials", label: "素材" }, { key: "uploads", label: "我的上传" }, { key: "unclassified", label: "未分类" }
];
// 对齐 api-bridge:工具栏 chip 按 tab 显隐
@@ -50,8 +51,8 @@ const LIB_CHIPS: Array<{ key: string; label: string; tabs: LibTab[] }> = [
{ key: "role", label: "角色标签", tabs: ["people"] },
{ key: "sceneType", label: "场景类型", tabs: ["scenes"] },
{ key: "product", label: "关联商品", tabs: ["products", "tryon", "kits"] },
{ key: "project", label: "关联项目", tabs: ["finals"] },
{ key: "duration", label: "时长", tabs: ["finals"] },
{ key: "project", label: "关联项目", tabs: ["materials"] },
{ key: "duration", label: "时长", tabs: ["materials"] },
{ key: "kind", label: "资产类型", tabs: ["uploads"] },
{ key: "source", label: "来源", tabs: ["people", "scenes", "products", "tryon", "kits", "creations", "uploads"] }
];
@@ -427,7 +428,7 @@ export function LibraryPage({ onUpload, onDelete }: { onUpload: (formData: FormD
// ── 服务端懒加载:列表按页拉、tab 计数走 summary、筛选项走 facets(不再前端取全量再切片)──
const [items, setItems] = useState<Asset[]>([]);
const [total, setTotal] = useState(0);
const [counts, setCounts] = useState<Record<LibTab, number>>({ people: 0, scenes: 0, products: 0, tryon: 0, kits: 0, creations: 0, finals: 0, uploads: 0, unclassified: 0 });
const [counts, setCounts] = useState<Record<LibTab, number>>({ people: 0, scenes: 0, products: 0, tryon: 0, kits: 0, creations: 0, materials: 0, uploads: 0, unclassified: 0 });
const [facets, setFacets] = useState<{ sources: string[]; kinds: string[]; metadata: Record<string, string[]> }>({ sources: [], kinds: [], metadata: {} });
const [loading, setLoading] = useState(false);
const [page, setPage] = useState(1);
@@ -522,7 +523,7 @@ export function LibraryPage({ onUpload, onDelete }: { onUpload: (formData: FormD
<div className="page-head">
<div>
<h1>资产库</h1>
<div className="sub"><span className="mono">// 跨项目复用 · <span id="sub-people">{counts.people}</span> 人 · <span id="sub-scenes">{counts.scenes}</span> 景 · <span id="sub-products">{counts.products}</span> 商 · <span id="sub-finals">{counts.finals}</span> 片</span></div>
<div className="sub"><span className="mono">// 跨项目复用 · <span id="sub-people">{counts.people}</span> 角色 · <span id="sub-scenes">{counts.scenes}</span> 景 · <span id="sub-products">{counts.products}</span> 商 · <span id="sub-materials">{counts.materials}</span> 素材</span></div>
</div>
<div className="actions">
<button className={`btn btn-edit-toggle${editMode ? " active" : ""}`} type="button" id="lib-manage-btn" onClick={() => (editMode ? exitEdit() : setEditMode(true))}>
+2 -2
View File
@@ -12,8 +12,8 @@ import { IconKitSvg } from "../components/IconKitSvg";
// 真实资产缩略图注入:与全站一致用 --mock-media-url(.placeholder.has-mock-media 负责 cover 裁切 + 8px 圆角)
const mediaStyle = (url: string): CSSProperties => ({ ["--mock-media-url"]: `url(${url})` } as CSSProperties);
// 基础资产组 kind → 中文区块名(对齐 design.md 商品/人物/场景三类)
const KIND_LABEL: Record<string, string> = { product: "商品", person: "人物", scene: "场景" };
// 基础资产组 kind → 中文区块名(期3:视频项目资产趴「人物」→「角色」,可引用模特库的可复用形象)
const KIND_LABEL: Record<string, string> = { product: "商品", person: "角色", scene: "场景" };
// 脚本来源 → 「来源」brief pill 文案
const SOURCE_LABEL: Record<string, string> = { ai: "AI 全生", theme: "一句话主题", manual: "自带脚本" };
// 旁白配音音色预设(语音合成经典版试用包实测可用,与后端 VOICEOVER_VOICES 对齐)