video_translate/src/services/subtitleService.test.ts
Song367 04072dc94b
All checks were successful
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 1m6s
commit code
2026-03-19 20:13:24 +08:00

501 lines
12 KiB
TypeScript

// @vitest-environment jsdom
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { generateSubtitlePipeline } from './subtitleService';
describe('generateSubtitlePipeline', () => {
beforeEach(() => {
vi.stubEnv('VITE_ARK_API_KEY', 'ark-key');
vi.stubEnv('VITE_API_BASE_PATH', '/api');
});
afterEach(() => {
vi.unstubAllEnvs();
vi.useRealTimers();
});
it('posts the selected provider to the server for gemini', async () => {
vi.useFakeTimers();
const fetchMock = vi
.fn()
.mockResolvedValueOnce(
new Response(
JSON.stringify({
jobId: 'job-1',
requestId: 'req-1',
status: 'queued',
stage: 'queued',
progress: 5,
message: 'Queued',
}),
{
status: 202,
headers: {
'Content-Type': 'application/json',
},
},
),
)
.mockResolvedValueOnce(
new Response(
JSON.stringify({
jobId: 'job-1',
requestId: 'req-1',
status: 'succeeded',
stage: 'succeeded',
progress: 100,
result: {
subtitles: [],
speakers: [],
quality: 'fallback',
},
}),
{
status: 200,
headers: {
'Content-Type': 'application/json',
},
},
),
);
const promise = generateSubtitlePipeline(
new File(['video'], 'clip.mp4', { type: 'video/mp4' }),
'English',
'gemini',
null,
fetchMock as unknown as typeof fetch,
);
await vi.runAllTimersAsync();
await promise;
expect(fetchMock).toHaveBeenCalledWith(
'/api/generate-subtitles',
expect.objectContaining({
method: 'POST',
body: expect.any(FormData),
}),
);
const [, requestInit] = fetchMock.mock.calls[0] as unknown as [string, RequestInit];
const formData = requestInit.body as FormData;
expect(formData.get('targetLanguage')).toBe('English');
expect(formData.get('provider')).toBe('gemini');
expect(fetchMock).toHaveBeenNthCalledWith(2, '/api/generate-subtitles/job-1', { method: 'GET' });
});
it('forwards the tts language in subtitle requests', async () => {
vi.useFakeTimers();
const fetchMock = vi
.fn()
.mockResolvedValueOnce(
new Response(
JSON.stringify({
jobId: 'job-1',
requestId: 'req-1',
status: 'queued',
stage: 'queued',
progress: 5,
}),
{
status: 202,
headers: {
'Content-Type': 'application/json',
},
},
),
)
.mockResolvedValueOnce(
new Response(
JSON.stringify({
jobId: 'job-1',
requestId: 'req-1',
status: 'succeeded',
stage: 'succeeded',
progress: 100,
result: {
subtitles: [],
speakers: [],
quality: 'fallback',
},
}),
{
status: 200,
headers: {
'Content-Type': 'application/json',
},
},
),
);
const promise = generateSubtitlePipeline(
new File(['video'], 'clip.mp4', { type: 'video/mp4' }),
'English',
'gemini',
null,
fetchMock as unknown as typeof fetch,
undefined,
'fr',
);
await vi.runAllTimersAsync();
await promise;
const [, requestInit] = fetchMock.mock.calls[0] as unknown as [string, RequestInit];
const formData = requestInit.body as FormData;
expect(formData.get('ttsLanguage')).toBe('fr');
});
it('uploads doubao videos to ark files before requesting subtitles', async () => {
vi.useFakeTimers();
const fetchMock = vi
.fn()
.mockResolvedValueOnce(
new Response(
JSON.stringify({
id: 'file-123',
}),
{
status: 200,
headers: {
'Content-Type': 'application/json',
},
},
),
)
.mockResolvedValueOnce(
new Response(
JSON.stringify({
id: 'file-123',
status: 'processing',
}),
{
status: 200,
headers: {
'Content-Type': 'application/json',
},
},
),
)
.mockResolvedValueOnce(
new Response(
JSON.stringify({
id: 'file-123',
status: 'active',
}),
{
status: 200,
headers: {
'Content-Type': 'application/json',
},
},
),
)
.mockResolvedValueOnce(
new Response(
JSON.stringify({
jobId: 'job-1',
requestId: 'req-1',
status: 'queued',
stage: 'queued',
progress: 5,
}),
{
status: 202,
headers: {
'Content-Type': 'application/json',
},
},
),
)
.mockResolvedValueOnce(
new Response(
JSON.stringify({
jobId: 'job-1',
requestId: 'req-1',
status: 'succeeded',
stage: 'succeeded',
progress: 100,
result: {
subtitles: [],
speakers: [],
quality: 'fallback',
},
}),
{
status: 200,
headers: {
'Content-Type': 'application/json',
},
},
),
);
const promise = generateSubtitlePipeline(
new File(['video'], 'clip.mp4', { type: 'video/mp4' }),
'English',
'doubao',
null,
fetchMock as unknown as typeof fetch,
);
await vi.runAllTimersAsync();
await promise;
expect(fetchMock).toHaveBeenNthCalledWith(
1,
'https://ark.cn-beijing.volces.com/api/v3/files',
expect.objectContaining({
method: 'POST',
body: expect.any(FormData),
}),
);
expect(fetchMock).toHaveBeenNthCalledWith(
2,
'https://ark.cn-beijing.volces.com/api/v3/files/file-123',
expect.objectContaining({
method: 'GET',
headers: {
Authorization: 'Bearer ark-key',
},
}),
);
expect(fetchMock).toHaveBeenNthCalledWith(
3,
'https://ark.cn-beijing.volces.com/api/v3/files/file-123',
expect.objectContaining({
method: 'GET',
headers: {
Authorization: 'Bearer ark-key',
},
}),
);
const [, subtitleRequest] = fetchMock.mock.calls[3] as unknown as [string, RequestInit];
const subtitleBody = JSON.parse(String(subtitleRequest.body));
expect(fetchMock).toHaveBeenNthCalledWith(
4,
'/api/generate-subtitles',
expect.objectContaining({
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
}),
);
expect(subtitleBody).toEqual({
fileId: 'file-123',
provider: 'doubao',
targetLanguage: 'English',
ttsLanguage: 'English',
});
expect(fetchMock).toHaveBeenNthCalledWith(5, '/api/generate-subtitles/job-1', { method: 'GET' });
});
it('stops when ark reports file preprocessing failure', async () => {
const fetchMock = vi
.fn()
.mockResolvedValueOnce(
new Response(
JSON.stringify({
id: 'file-123',
}),
{
status: 200,
headers: {
'Content-Type': 'application/json',
},
},
),
)
.mockResolvedValueOnce(
new Response(
JSON.stringify({
id: 'file-123',
status: 'failed',
error: {
message: 'video preprocess failed',
},
}),
{
status: 200,
headers: {
'Content-Type': 'application/json',
},
},
),
);
await expect(
generateSubtitlePipeline(
new File(['video'], 'clip.mp4', { type: 'video/mp4' }),
'English',
'doubao',
null,
fetchMock as unknown as typeof fetch,
),
).rejects.toThrow(/video preprocess failed/i);
expect(fetchMock).toHaveBeenCalledTimes(2);
});
it('keeps multipart uploads for gemini requests', async () => {
vi.useFakeTimers();
const fetchMock = vi
.fn()
.mockResolvedValueOnce(
new Response(
JSON.stringify({
jobId: 'job-1',
requestId: 'req-1',
status: 'queued',
stage: 'queued',
progress: 5,
}),
{
status: 202,
headers: {
'Content-Type': 'application/json',
},
},
),
)
.mockResolvedValueOnce(
new Response(
JSON.stringify({
jobId: 'job-1',
requestId: 'req-1',
status: 'succeeded',
stage: 'succeeded',
progress: 100,
result: {
subtitles: [],
speakers: [],
quality: 'fallback',
},
}),
{
status: 200,
headers: {
'Content-Type': 'application/json',
},
},
),
);
const promise = generateSubtitlePipeline(
new File(['video'], 'clip.mp4', { type: 'video/mp4' }),
'English',
'gemini',
null,
fetchMock as unknown as typeof fetch,
);
await vi.runAllTimersAsync();
await promise;
expect(fetchMock).toHaveBeenNthCalledWith(
1,
'/api/generate-subtitles',
expect.objectContaining({
method: 'POST',
body: expect.any(FormData),
}),
);
});
it('polls every 5 seconds and reports progress updates', async () => {
vi.useFakeTimers();
const onProgress = vi.fn();
const fetchMock = vi
.fn()
.mockResolvedValueOnce(
new Response(
JSON.stringify({
jobId: 'job-1',
requestId: 'req-1',
status: 'queued',
stage: 'queued',
progress: 5,
message: 'Queued',
}),
{
status: 202,
headers: {
'Content-Type': 'application/json',
},
},
),
)
.mockResolvedValueOnce(
new Response(
JSON.stringify({
jobId: 'job-1',
requestId: 'req-1',
status: 'running',
stage: 'calling_provider',
progress: 70,
message: 'Calling provider',
}),
{
status: 200,
headers: {
'Content-Type': 'application/json',
},
},
),
)
.mockResolvedValueOnce(
new Response(
JSON.stringify({
jobId: 'job-1',
requestId: 'req-1',
status: 'succeeded',
stage: 'succeeded',
progress: 100,
message: 'Done',
result: {
subtitles: [],
speakers: [],
quality: 'fallback',
},
}),
{
status: 200,
headers: {
'Content-Type': 'application/json',
},
},
),
);
const promise = generateSubtitlePipeline(
new File(['video'], 'clip.mp4', { type: 'video/mp4' }),
'English',
'gemini',
null,
fetchMock as unknown as typeof fetch,
onProgress,
);
await vi.runAllTimersAsync();
await promise;
expect(onProgress).toHaveBeenCalledWith(
expect.objectContaining({
jobId: 'job-1',
status: 'queued',
stage: 'queued',
progress: 5,
}),
);
expect(onProgress).toHaveBeenCalledWith(
expect.objectContaining({
jobId: 'job-1',
status: 'running',
stage: 'calling_provider',
progress: 70,
}),
);
});
});