P2~P7 리노컷 캐스트 원화·리그와 세션 활성화

- 추천 원화(P2 b·P3 v2b·P4 v2b·P5 v2a·P6 v2b·P7 v2b)로 얼굴 없는 기본형·몸 원본 codex 편집, 정렬·정규화 후 공통 파이프라인 실행
- 게시 자산 apps/web/public/avatar/v3/p2~p7, 리그 rigs/p2Rig~p7Rig
- 세션 레지스트리 P1~P7 전원 활성, Lab은 생성된 리그 자동 수집
- P3·P6 기본 눈 감김을 줄여 중립 렌더를 원화 눈매에 맞춤
- 표정 E2E를 v3 렌더러 기대로 갱신(검사 의도 유지)
This commit is contained in:
Yun Chan 2026-10-01 22:38:45 +09:00
parent 98577cf4ae
commit ded312acdc
258 changed files with 18463 additions and 32 deletions

View file

@ -53,26 +53,35 @@ test.describe("avatar expression lab", () => {
);
await expect(panel.locator('[data-expression-card="true"]')).toHaveCount(28);
const cardRenderers = panel.locator(
'.axl__card .vg-avatar [data-avatar-renderer="linocut"]',
);
await expect(cardRenderers).toHaveCount(28);
await expect
.poll(
async () =>
cardRenderers.evaluateAll(
(els) => els.filter((el) => el.getAttribute("data-load-state") === "ready").length,
),
{ timeout: 20_000 },
)
.toBe(28);
const sectionMetrics = await panel.evaluate((el) => {
const avatars = Array.from(el.querySelectorAll<HTMLElement>(".axl__card .vg-avatar"));
const invalid = avatars
.map((avatar) => {
const neck = avatar.querySelector<SVGGraphicsElement>('[data-avatar-neck="true"]');
const svg = avatar.querySelector("svg");
const renderer = avatar.querySelector<HTMLElement>('[data-avatar-renderer="linocut"]');
const svg = avatar.querySelector(".linocut-avatar");
return {
affect: avatar.getAttribute("data-affect"),
primitives: avatar.querySelectorAll("svg path, svg ellipse, svg circle, svg line, svg rect")
.length,
neckBox: neck?.getBoundingClientRect().toJSON(),
loadState: renderer?.getAttribute("data-load-state"),
svgBox: svg?.getBoundingClientRect().toJSON(),
};
})
.filter((item) => {
return (
item.primitives < 12 ||
!item.neckBox ||
item.neckBox.width <= 8 ||
item.neckBox.height <= 14 ||
item.loadState !== "ready" ||
!item.svgBox ||
item.svgBox.width <= 0 ||
item.svgBox.height <= 0

View file

@ -197,6 +197,10 @@ test.describe("persona avatar expression rig", () => {
await expect(avatar).toBeVisible();
await expect(avatar).toHaveAttribute("data-affect", persona.expectedExpression);
const v3 = avatar.locator('[data-avatar-renderer="linocut"]');
await expect(v3).toHaveAttribute("data-load-state", "ready", { timeout: 15_000 });
await expect(v3.locator(".linocut-avatar")).toBeVisible();
const metrics = await avatar.evaluate((el) => ({
expressionCount: Number(el.getAttribute("data-expression-count")),
live2dSchema: el.getAttribute("data-live2d-schema"),
@ -207,10 +211,7 @@ test.describe("persona avatar expression rig", () => {
live2dExpressionCount: Number(el.getAttribute("data-live2d-expression-count")),
renderMode: el.getAttribute("data-render-mode"),
rasterCount: el.querySelectorAll(".vg-raster").length,
primitiveCount: el.querySelectorAll("svg path, svg ellipse, svg circle, svg line, svg rect")
.length,
neckBox: el.querySelector<SVGGraphicsElement>('[data-avatar-neck="true"]')?.getBoundingClientRect().toJSON(),
svgBox: el.querySelector("svg")?.getBoundingClientRect().toJSON(),
v3Box: el.querySelector(".linocut-avatar")?.getBoundingClientRect().toJSON(),
}));
expect(metrics.expressionCount, `${persona.code} expression count`).toBeGreaterThanOrEqual(20);
expect(metrics.live2dSchema, `${persona.code} Live2D schema`).toBe("vignette.live2d.v1");
@ -225,10 +226,8 @@ test.describe("persona avatar expression rig", () => {
expect(metrics.live2dExpressionCount, `${persona.code} Live2D expressions`).toBeGreaterThanOrEqual(20);
expect(metrics.renderMode, `${persona.code} render mode`).toBe("svg");
expect(metrics.rasterCount, `${persona.code} raster rig should be disabled`).toBe(0);
expect(metrics.primitiveCount, `${persona.code} SVG primitives`).toBeGreaterThan(10);
expect(metrics.neckBox?.width, `${persona.code} SVG neck`).toBeGreaterThan(0);
expect(metrics.svgBox?.width, `${persona.code} SVG rig`).toBeGreaterThan(0);
expect(metrics.svgBox?.height, `${persona.code} SVG rig`).toBeGreaterThan(0);
expect(metrics.v3Box?.width, `${persona.code} linocut SVG`).toBeGreaterThan(0);
expect(metrics.v3Box?.height, `${persona.code} linocut SVG`).toBeGreaterThan(0);
}
});
@ -261,9 +260,10 @@ test.describe("persona avatar expression rig", () => {
const activeAvatar = page.locator(".sx-page--active .vg-avatar").first();
await expect(activeAvatar).toBeVisible();
await expect(activeAvatar).toHaveAttribute("data-render-mode", "svg");
const activeV3 = activeAvatar.locator('[data-avatar-renderer="linocut"]');
await expect(activeV3).toHaveAttribute("data-load-state", "ready", { timeout: 15_000 });
await expect(activeV3.locator(".linocut-avatar")).toBeVisible();
await expect(activeAvatar.locator(".vg-raster")).toHaveCount(0);
await expect(activeAvatar.locator(".vg-avatar__svg")).toBeVisible();
await expect(activeAvatar).toHaveAttribute("data-expression-count", "28");
await expect(activeAvatar).toHaveAttribute("data-live2d-expression-count", "28");
await expect(activeAvatar).toHaveAttribute("data-live2d-model", "vignette-p4-live2d");
@ -316,9 +316,10 @@ test.describe("persona avatar expression rig", () => {
await page.getByRole("button", { name: "회기 시작" }).click();
await expect(page.locator(".sx-page--active")).toBeVisible();
const activeAvatar = page.locator(".sx-page--active .vg-avatar").first();
await expect(activeAvatar).toHaveAttribute("data-render-mode", "svg");
const activeV3 = activeAvatar.locator('[data-avatar-renderer="linocut"]');
await expect(activeV3).toHaveAttribute("data-load-state", "ready", { timeout: 15_000 });
await expect(activeV3.locator(".linocut-avatar")).toBeVisible();
await expect(activeAvatar.locator(".vg-raster")).toHaveCount(0);
await expect(activeAvatar.locator(".vg-avatar__svg")).toBeVisible();
await expect(activeAvatar).toHaveAttribute("data-live2d-motion", "anxious");
await setLearnerUtterance(page, "조금 안정된 것 같아요.");