아바타 v3 세션 연결 — P1 서연 리노컷 아바타와 음성 립싱크
Some checks are pending
API contract / OpenAPI type drift (push) Waiting to run

- ClientAvatar가 리그 레지스트리(v3/rigs)로 P1을 판정해 그림 영역을 ClientAvatarV3로 그린다(로드 중·실패 시 기존 SVG, VITE_AVATAR_V3=0이면 끔)
- 발화 구동 공용 모듈 speechDriver: Lab과 세션이 함께 쓰고, TTS AudioBuffer를 오디오 시계로 표본해 립싱크·발화 동반층·지문 cue를 돌린다
- Session: TTS 재생 시작 시 발화(원문·버퍼·시작 시각) 전달, 음성 실패 시 텍스트 타이밍 발화, 정지 경로에서 해제, 개방도·겉표정 강도 전달
- 정지 화면(시작 전·썸네일)도 한 프레임을 그리고, 박스 실측 폭으로 상반신/얼굴 크롭을 고른다
- 세션 발화 E2E(오디오·실패 경로), P1 레이아웃 게이트, P1 표정 시험을 v3 기대로 갱신
This commit is contained in:
Yun Chan 2026-10-01 16:12:24 +09:00
parent ecb36d123f
commit 613bcb603e
14 changed files with 1000 additions and 182 deletions

View file

@ -666,6 +666,62 @@ test.describe("learner session full-screen layout", () => {
}
});
// P1 서연은 v3(리노컷) 아바타로 그린다(결정문 §8.5). 위 "dense viewport" 스윕을 P1로도
// 돌려 v3 래퍼(절대 위치·원형 클립)가 기존 SVG와 같은 레이아웃 게이트를 통과하는지 본다.
test("keeps critical session controls visible across dense viewport sizes for the v3 avatar (P1)", async ({
page,
}) => {
await signInAsLearner(page);
const viewports = [
{ width: 1366, height: 768 },
{ width: 1180, height: 768 },
{ width: 1024, height: 768 },
{ width: 881, height: 768 },
{ width: 820, height: 1180 },
{ width: 390, height: 844 },
{ width: 320, height: 568 },
];
await page.setViewportSize(viewports[0]);
await page.goto("/learn/session/P1");
await page.getByRole("button", { name: "회기 시작" }).click();
await completeAlliancePreCheckpoint(page);
await expect(page.locator(".sx-page.sx-page--active")).toBeVisible({ timeout: 15_000 });
for (const viewport of viewports) {
await page.setViewportSize(viewport);
await page.evaluate(() => new Promise(requestAnimationFrame));
await expect(page.locator(".sx-page.sx-page--active")).toBeVisible({ timeout: 15_000 });
await expectNoDocumentOverflow(page);
await expectNoHorizontalOverflow(page);
await expectSessionControlsInsideViewport(page);
await expectNoVisibleSessionPanelOverlap(page);
await expectMainControlsUnclipped(page);
await expectSessionPageHeightToMatchViewport(page);
await expectActiveSessionUsableLayout(page);
/* 320×568 저높이 폰은 session.css가 아바타 오브(.sx-orb-wrap)를 숨긴다(기존 SVG도 같다).
보일 때는 v3 래퍼가 스테이지 원 자리를 정확히 차지해야 한다. */
const orbShown = !(viewport.width === 320 && viewport.height === 568);
const stage = page.locator(".sx-page--active .vg-avatar__stage").first();
if (orbShown) {
await expect(stage).toBeVisible();
const v3 = stage.locator('[data-avatar-renderer="linocut"]');
await expect(v3).toHaveAttribute("data-load-state", "ready", { timeout: 15_000 });
const [stageBox, v3Box] = await Promise.all([stage.boundingBox(), v3.boundingBox()]);
expect(stageBox, "아바타 스테이지 좌표").not.toBeNull();
expect(v3Box, "v3 래퍼 좌표").not.toBeNull();
for (const key of ["x", "y", "width", "height"] as const) {
expect(Math.abs(stageBox![key] - v3Box![key]), `v3 래퍼 ${key}`).toBeLessThanOrEqual(1);
}
} else {
await expect(page.locator(".sx-page--active .sx-orb-wrap")).toBeHidden();
}
}
});
test("does not leave an unsaved local transcript when a text turn is rejected", async ({ page }) => {
await signInAsLearner(page);
const persona = await fetchAvailablePersona(page, 1);