All checks were successful
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 1m6s
501 lines
12 KiB
TypeScript
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,
|
|
}),
|
|
);
|
|
});
|
|
});
|