import { describe, expect, it } from "vitest"; import { ACTION_WINDOW_FRAMES, AVATAR_PRESETS, createStableActionState, getAvatarAnchors, getAvatarPreset, resolveAvatarKeyFromPrompt, stabilizeActionStream, type FrameActionSample, } from "./liveCamera"; function feedSamples(samples: Array>, intervalMs = 33) { const history: FrameActionSample[] = []; const state = createStableActionState(); let lastResult = null as ReturnType | null; samples.forEach((sample, index) => { lastResult = stabilizeActionStream( { ...sample, timestamp: index * intervalMs, }, history, state, ); }); return { history, state, lastResult }; } describe("live camera action stabilizer", () => { it("locks a dominant action after a full temporal window", () => { const samples = Array.from({ length: ACTION_WINDOW_FRAMES * 2 }, () => ({ action: "forehand" as const, confidence: 0.84, })); const { lastResult } = feedSamples(samples); expect(lastResult?.stableAction).toBe("forehand"); expect(lastResult?.windowAction).toBe("forehand"); expect(lastResult?.pending).toBe(false); expect(lastResult?.windowShare).toBeGreaterThan(0.9); }); it("ignores brief action spikes and keeps the stable action", () => { const stableFrames = Array.from({ length: ACTION_WINDOW_FRAMES * 2 }, () => ({ action: "forehand" as const, confidence: 0.82, })); const noisyFrames = Array.from({ length: 5 }, () => ({ action: "backhand" as const, confidence: 0.88, })); const { lastResult } = feedSamples([...stableFrames, ...noisyFrames]); expect(lastResult?.stableAction).toBe("forehand"); expect(lastResult?.pending).toBe(false); }); it("switches only after the next action persists long enough", () => { const forehandFrames = Array.from({ length: ACTION_WINDOW_FRAMES * 2 }, () => ({ action: "forehand" as const, confidence: 0.8, })); const backhandFrames = Array.from({ length: ACTION_WINDOW_FRAMES * 2 }, () => ({ action: "backhand" as const, confidence: 0.85, })); const { lastResult, state } = feedSamples([...forehandFrames, ...backhandFrames]); expect(lastResult?.stableAction).toBe("backhand"); expect(state.switchCount).toBeGreaterThanOrEqual(2); }); it("requires a longer delay before falling back to unknown", () => { const forehandFrames = Array.from({ length: ACTION_WINDOW_FRAMES * 2 }, () => ({ action: "forehand" as const, confidence: 0.83, })); const unknownFrames = Array.from({ length: 10 }, () => ({ action: "unknown" as const, confidence: 0.4, })); const { lastResult } = feedSamples([...forehandFrames, ...unknownFrames]); expect(lastResult?.stableAction).toBe("forehand"); }); }); describe("live camera avatar helpers", () => { it("maps prompt keywords into avatar presets", () => { expect(resolveAvatarKeyFromPrompt("切换成猩猩形象", "gorilla")).toBe("gorilla"); expect(resolveAvatarKeyFromPrompt("dog mascot", "gorilla")).toBe("dog"); expect(resolveAvatarKeyFromPrompt("狐狸风格", "gorilla")).toBe("fox"); expect(resolveAvatarKeyFromPrompt("兔子教练", "gorilla")).toBe("rabbit"); expect(resolveAvatarKeyFromPrompt("BeachKing 3D 替身", "gorilla")).toBe("beachKing"); expect(resolveAvatarKeyFromPrompt("Juanita avatar", "gorilla")).toBe("juanita3d"); expect(resolveAvatarKeyFromPrompt("", "pig")).toBe("pig"); }); it("exposes full-body 3d avatar examples with CC0 metadata", () => { const presets = AVATAR_PRESETS.filter((preset) => preset.category === "full-body-3d"); expect(presets).toHaveLength(4); expect(presets.every((preset) => preset.license === "CC0")).toBe(true); expect(getAvatarPreset("sportTv")?.modelUrl).toContain("arweave.net"); }); it("builds avatar anchors from pose landmarks", () => { const landmarks = Array.from({ length: 33 }, () => ({ x: 0.5, y: 0.5, visibility: 0.95 })); landmarks[0] = { x: 0.5, y: 0.16, visibility: 0.99 }; landmarks[11] = { x: 0.4, y: 0.3, visibility: 0.99 }; landmarks[12] = { x: 0.6, y: 0.3, visibility: 0.99 }; landmarks[15] = { x: 0.28, y: 0.44, visibility: 0.99 }; landmarks[16] = { x: 0.72, y: 0.44, visibility: 0.99 }; landmarks[23] = { x: 0.44, y: 0.58, visibility: 0.99 }; landmarks[24] = { x: 0.56, y: 0.58, visibility: 0.99 }; landmarks[27] = { x: 0.43, y: 0.92, visibility: 0.99 }; landmarks[28] = { x: 0.57, y: 0.92, visibility: 0.99 }; const anchors = getAvatarAnchors(landmarks, 1280, 720); expect(anchors).not.toBeNull(); expect(anchors?.headRadius).toBeGreaterThan(30); expect(anchors?.bodyHeight).toBeGreaterThan(120); expect(anchors?.rightHandX).toBeGreaterThan(anchors?.leftHandX || 0); }); });