Files
seaislee1209andClaude Opus 4.8 c6e9c7e77b 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>
2026-06-21 19:27:23 +08:00

65 lines
3.2 KiB
JavaScript

// 期3 走查:资产库「角色/素材」改名 + 最终成片隐藏 + pipeline 资产趴「角色」+ 演员库「模特库」tab。
import { chromium } from "playwright";
import fs from "node:fs";
import path from "node:path";
const BASE = process.env.BASE || "http://localhost:5200";
const PROJECT = process.env.PROJECT || "bdbf39d3-1a89-4135-b1b7-26d3331a7021";
const OUT = path.resolve("../../../_qa_shots/models-p3");
fs.mkdirSync(OUT, { recursive: true });
const browser = await chromium.launch({ headless: true });
const r = { consoleErrors: [], library: {}, pipeline: {} };
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } });
const p = await ctx.newPage();
p.on("console", (m) => { if (m.type() === "error") r.consoleErrors.push(m.text()); });
p.on("pageerror", (e) => r.consoleErrors.push("PAGEERR:" + e.message));
// 登录
await p.goto(BASE + "/login", { waitUntil: "load" });
await p.waitForTimeout(400);
await p.fill("#auth-username", "airshelf");
await p.fill("#auth-pwd", "Restraint2026");
await p.click("button.btn-cta");
await p.waitForFunction(() => !location.pathname.startsWith("/login"), { timeout: 12000 });
// ── 资产库:角色 / 素材 改名,人物 / 成片 消失 ──
await p.goto(BASE + "/library", { waitUntil: "load" });
await p.waitForTimeout(2600);
const libTabs = await p.locator(".tabs .tab, .tab").allInnerTexts().catch(() => []);
const tabText = libTabs.join(" | ");
r.library.tabs = tabText;
r.library.hasRoleTab = /角色/.test(tabText);
r.library.hasMaterialTab = /素材/.test(tabText);
r.library.peopleTabGone = !/(^|\|)\s*人物\s*\d/.test(tabText) && !/人物\s/.test(tabText);
r.library.finalsTabGone = !/成片/.test(tabText);
await p.screenshot({ path: path.join(OUT, "library-tabs.png"), fullPage: true });
// ── pipeline 资产趴(stage 2):KIND_LABEL person → 角色 ──
await p.goto(BASE + "/pipeline/" + PROJECT, { waitUntil: "load" });
await p.waitForTimeout(2500);
await p.locator('a[data-stage="2"]').first().click().catch(() => {});
await p.waitForTimeout(2500);
const bodyText = await p.locator('[data-stage-pane="2"]').innerText().catch(() => "");
r.pipeline.hasRole = /角色/.test(bodyText);
r.pipeline.hasPersonOld = /(新增人物|暂无人物)/.test(bodyText);
await p.screenshot({ path: path.join(OUT, "pipeline.png"), fullPage: true });
// 尝试打开演员库(点资产趴里的「新增角色」或「模特库」入口),验证「模特库」tab
let openedActorLib = false;
const trigger = p.locator("button", { hasText: /新增角色|模特库|演员库/ }).first();
if (await trigger.count()) {
await trigger.click().catch(() => {});
await p.waitForTimeout(1800);
const alText = await p.locator(".actorlib").innerText().catch(() => "");
r.pipeline.actorLibText = alText.slice(0, 120);
r.pipeline.actorLibHasModelLib = /模特库/.test(alText);
openedActorLib = await p.locator(".actorlib").count() > 0;
if (openedActorLib) await p.screenshot({ path: path.join(OUT, "actor-library.png"), fullPage: true });
}
r.pipeline.openedActorLib = openedActorLib;
await browser.close();
console.log(JSON.stringify(r, null, 2));
fs.writeFileSync(path.join(OUT, "summary.json"), JSON.stringify(r, null, 2));