Files
waooplus/tests/unit/lipsync-bailian.test.ts
2026-03-08 17:10:06 +08:00

225 lines
7.2 KiB
TypeScript

import { afterAll, beforeEach, describe, expect, it, vi } from 'vitest'
const resolveModelSelectionOrSingleMock = vi.hoisted(() => vi.fn())
const getProviderConfigMock = vi.hoisted(() => vi.fn())
const getProviderKeyMock = vi.hoisted(() => vi.fn((providerId: string) => {
const marker = providerId.indexOf(':')
return marker === -1 ? providerId : providerId.slice(0, marker)
}))
const submitFalTaskMock = vi.hoisted(() => vi.fn())
const normalizeToOriginalMediaUrlMock = vi.hoisted(() => vi.fn(async (input: string) => {
if (input.startsWith('/')) {
return `http://localhost:3000${input}`
}
return input
}))
vi.mock('@/lib/api-config', () => ({
resolveModelSelectionOrSingle: resolveModelSelectionOrSingleMock,
getProviderConfig: getProviderConfigMock,
getProviderKey: getProviderKeyMock,
}))
vi.mock('@/lib/async-submit', () => ({
submitFalTask: submitFalTaskMock,
}))
vi.mock('@/lib/media/outbound-image', () => ({
normalizeToBase64ForGeneration: vi.fn(async (input: string) => input),
normalizeToOriginalMediaUrl: normalizeToOriginalMediaUrlMock,
}))
vi.mock('@/lib/logging/core', () => ({
logInfo: vi.fn(),
logError: vi.fn(),
createScopedLogger: vi.fn(() => ({
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
debug: vi.fn(),
})),
}))
import { generateLipSync } from '@/lib/lipsync'
const POLICY_ENDPOINT = 'https://dashscope.aliyuncs.com/api/v1/uploads'
const SUBMIT_ENDPOINT = 'https://dashscope.aliyuncs.com/api/v1/services/aigc/image2video/video-synthesis'
const UPLOAD_HOST = 'https://upload.example.com'
function buildJsonResponse(payload: unknown, status = 200): Response {
return {
ok: status >= 200 && status < 300,
status,
headers: new Headers({
'content-type': 'application/json',
}),
text: async () => JSON.stringify(payload),
} as unknown as Response
}
function buildBinaryResponse(contentType: string, data: string): Response {
const bytes = new TextEncoder().encode(data)
return {
ok: true,
status: 200,
headers: new Headers({
'content-type': contentType,
}),
arrayBuffer: async () => bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength),
text: async () => '',
} as unknown as Response
}
describe('lip-sync bailian submit', () => {
const originalNextauthUrl = process.env.NEXTAUTH_URL
beforeEach(() => {
vi.clearAllMocks()
process.env.NEXTAUTH_URL = originalNextauthUrl
resolveModelSelectionOrSingleMock.mockResolvedValue({
provider: 'bailian',
modelId: 'videoretalk',
modelKey: 'bailian::videoretalk',
mediaType: 'lipsync',
})
getProviderConfigMock.mockResolvedValue({
id: 'bailian',
apiKey: 'bl-key',
})
})
afterAll(() => {
process.env.NEXTAUTH_URL = originalNextauthUrl
})
it('uploads local media to bailian temp storage then submits oss urls', async () => {
const fetchMock = vi.fn(async (input: RequestInfo | URL) => {
const url = String(input)
if (url.startsWith(`${POLICY_ENDPOINT}?action=getPolicy&model=videoretalk`)) {
return buildJsonResponse({
data: {
upload_host: UPLOAD_HOST,
upload_dir: 'dashscope-instant/upload-dir',
oss_access_key_id: 'ak',
policy: 'policy',
signature: 'sig',
},
})
}
if (url === 'http://localhost:3000/api/storage/sign?key=images%2Fdemo.mp4') {
return buildBinaryResponse('video/mp4', 'video-bytes')
}
if (url === 'http://localhost:3000/api/storage/sign?key=voice%2Fdemo.wav') {
return buildBinaryResponse('audio/wav', 'audio-bytes')
}
if (url === UPLOAD_HOST) {
return {
ok: true,
status: 200,
text: async () => '',
} as unknown as Response
}
if (url === SUBMIT_ENDPOINT) {
return buildJsonResponse({
output: {
task_id: 'task-123',
task_status: 'PENDING',
},
})
}
throw new Error(`unexpected fetch: ${url}`)
})
vi.stubGlobal('fetch', fetchMock as unknown as typeof fetch)
const result = await generateLipSync(
{
videoUrl: '/api/storage/sign?key=images%2Fdemo.mp4',
audioUrl: '/api/storage/sign?key=voice%2Fdemo.wav',
audioDurationMs: 3000,
videoDurationMs: 5000,
},
'user-1',
'bailian::videoretalk',
)
expect(resolveModelSelectionOrSingleMock).toHaveBeenCalledWith('user-1', 'bailian::videoretalk', 'lipsync')
expect(getProviderConfigMock).toHaveBeenCalledWith('user-1', 'bailian')
expect(normalizeToOriginalMediaUrlMock).toHaveBeenCalledWith('/api/storage/sign?key=images%2Fdemo.mp4')
expect(normalizeToOriginalMediaUrlMock).toHaveBeenCalledWith('/api/storage/sign?key=voice%2Fdemo.wav')
const submitCall = fetchMock.mock.calls.find(([input]) => String(input) === SUBMIT_ENDPOINT) as
| [RequestInfo | URL, RequestInit?]
| undefined
expect(submitCall).toBeDefined()
const submitInit = submitCall?.[1]
expect(submitInit).toBeDefined()
if (!submitInit) throw new Error('missing submit init')
expect(submitInit.method).toBe('POST')
expect(submitInit.headers).toEqual({
Authorization: 'Bearer bl-key',
'Content-Type': 'application/json',
'X-DashScope-Async': 'enable',
'X-DashScope-OssResourceResolve': 'enable',
})
const submitBody = JSON.parse(String(submitInit.body)) as {
model: string
input: { video_url: string; audio_url: string }
}
expect(submitBody.model).toBe('videoretalk')
expect(submitBody.input.video_url).toMatch(/^oss:\/\/dashscope-instant\/upload-dir\/video-/)
expect(submitBody.input.audio_url).toMatch(/^oss:\/\/dashscope-instant\/upload-dir\/audio-/)
const uploadCalls = fetchMock.mock.calls.filter(([input]) => String(input) === UPLOAD_HOST)
expect(uploadCalls.length).toBe(2)
expect(result).toEqual({
requestId: 'task-123',
externalId: 'BAILIAN:VIDEO:task-123',
async: true,
})
})
it('throws explicit error when bailian task id is missing', async () => {
const fetchMock = vi.fn(async (input: RequestInfo | URL) => {
const url = String(input)
if (url.startsWith(`${POLICY_ENDPOINT}?action=getPolicy&model=videoretalk`)) {
return buildJsonResponse({
data: {
upload_host: UPLOAD_HOST,
upload_dir: 'dashscope-instant/upload-dir',
oss_access_key_id: 'ak',
policy: 'policy',
signature: 'sig',
},
})
}
if (url === UPLOAD_HOST) {
return {
ok: true,
status: 200,
text: async () => '',
} as unknown as Response
}
if (url === SUBMIT_ENDPOINT) {
return buildJsonResponse({
output: {
task_status: 'PENDING',
},
})
}
throw new Error(`unexpected fetch: ${url}`)
})
vi.stubGlobal('fetch', fetchMock as unknown as typeof fetch)
await expect(generateLipSync(
{
videoUrl: 'data:video/mp4;base64,dmk=',
audioUrl: 'data:audio/wav;base64,YXU=',
audioDurationMs: 3000,
videoDurationMs: 5000,
},
'user-1',
'bailian::videoretalk',
)).rejects.toThrow('BAILIAN_LIPSYNC_TASK_ID_MISSING')
})
})