From e3c4b0f6974d52770bd2d0018e48a71f67c9c1fe Mon Sep 17 00:00:00 2001 From: Song367 <601337784@qq.com> Date: Thu, 19 Mar 2026 21:02:22 +0800 Subject: [PATCH] =?UTF-8?q?=E8=B6=85=E6=97=B6=E6=97=B6=E9=97=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server.ts | 5 ++ src/server/subtitleJobs.test.ts | 14 +++++ src/server/subtitleJobs.ts | 5 ++ src/services/subtitleService.test.ts | 83 ++++++++++++++++++++++++++++ src/services/subtitleService.ts | 26 ++++++++- src/types.ts | 1 + 6 files changed, 131 insertions(+), 3 deletions(-) diff --git a/server.ts b/server.ts index 442ac67..b2df66e 100644 --- a/server.ts +++ b/server.ts @@ -13,6 +13,7 @@ import { resolveMiniMaxTtsConfig, } from './src/server/minimaxTts'; import { generateSubtitlePipeline } from './src/server/subtitleGeneration'; +import { resolveLlmProviderConfig } from './src/server/llmProvider'; import { parseSubtitleRequest } from './src/server/subtitleRequest'; import { buildAssSubtitleContent, @@ -262,6 +263,9 @@ async function startServer() { }); const { provider, targetLanguage, ttsLanguage, fileId } = parseSubtitleRequest(req.body); + const providerConfig = resolveLlmProviderConfig(provider, process.env); + const pollTimeoutMs = + providerConfig.provider === 'doubao' ? providerConfig.timeoutMs : undefined; if (!videoPath && !fileId) { logEvent({ level: 'warn', @@ -286,6 +290,7 @@ async function startServer() { provider, targetLanguage, ttsLanguage, + pollTimeoutMs, fileId, filePath: videoPath, }); diff --git a/src/server/subtitleJobs.test.ts b/src/server/subtitleJobs.test.ts index d7ca21a..34f06bb 100644 --- a/src/server/subtitleJobs.test.ts +++ b/src/server/subtitleJobs.test.ts @@ -56,4 +56,18 @@ describe('subtitleJobs', () => { expect(toSubtitleJobResponse(job)).not.toHaveProperty('filePath'); }); + + it('includes poll timeout metadata in api responses when provided', () => { + const store = createSubtitleJobStore(); + const job = createSubtitleJob(store, { + requestId: 'req-1', + provider: 'doubao', + targetLanguage: 'English', + pollTimeoutMs: 900000, + }); + + expect(toSubtitleJobResponse(job)).toMatchObject({ + pollTimeoutMs: 900000, + }); + }); }); diff --git a/src/server/subtitleJobs.ts b/src/server/subtitleJobs.ts index 7bb1855..6a2e6a0 100644 --- a/src/server/subtitleJobs.ts +++ b/src/server/subtitleJobs.ts @@ -22,6 +22,7 @@ export interface SubtitleJob { provider?: string | null; targetLanguage: string; ttsLanguage?: string; + pollTimeoutMs?: number; fileId?: string; filePath?: string; error?: string; @@ -73,6 +74,7 @@ export const createSubtitleJob = ( provider, targetLanguage, ttsLanguage, + pollTimeoutMs, fileId, filePath, }: { @@ -80,6 +82,7 @@ export const createSubtitleJob = ( provider?: string | null; targetLanguage: string; ttsLanguage?: string; + pollTimeoutMs?: number; fileId?: string; filePath?: string; }, @@ -91,6 +94,7 @@ export const createSubtitleJob = ( provider, targetLanguage, ttsLanguage, + pollTimeoutMs, fileId, filePath, status: 'queued', @@ -159,6 +163,7 @@ export const toSubtitleGenerationProgress = (job: SubtitleJob): SubtitleGenerati stage: job.stage, progress: job.progress, message: job.message, + ...(job.pollTimeoutMs ? { pollTimeoutMs: job.pollTimeoutMs } : {}), }); export const toSubtitleJobResponse = (job: SubtitleJob) => ({ diff --git a/src/services/subtitleService.test.ts b/src/services/subtitleService.test.ts index 8c1a6db..3b26963 100644 --- a/src/services/subtitleService.test.ts +++ b/src/services/subtitleService.test.ts @@ -497,4 +497,87 @@ describe('generateSubtitlePipeline', () => { }), ); }); + + it('uses the server-provided poll timeout for doubao jobs', 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: '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, + pollTimeoutMs: 1000, + }), + { + status: 202, + headers: { + 'Content-Type': 'application/json', + }, + }, + ), + ) + .mockImplementation(async () => + 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', + }, + }, + ), + ); + + const promise = generateSubtitlePipeline( + new File(['video'], 'clip.mp4', { type: 'video/mp4' }), + 'English', + 'doubao', + null, + fetchMock as unknown as typeof fetch, + ); + const rejection = expect(promise).rejects.toThrow( + /timed out while waiting for subtitle generation to complete/i, + ); + + await vi.runAllTimersAsync(); + await rejection; + }); }); diff --git a/src/services/subtitleService.ts b/src/services/subtitleService.ts index 57947b3..5072b5b 100644 --- a/src/services/subtitleService.ts +++ b/src/services/subtitleService.ts @@ -11,6 +11,11 @@ const ARK_FILE_STATUS_TIMEOUT_MS = 120000; const SUBTITLE_JOB_POLL_INTERVAL_MS = 5000; const SUBTITLE_JOB_TIMEOUT_MS = 20 * 60 * 1000; +const resolvePollTimeoutMs = (value?: number) => + Number.isFinite(value) && (value as number) > 0 + ? Math.floor(value as number) + : SUBTITLE_JOB_TIMEOUT_MS; + interface SubtitleJobResponse extends SubtitleGenerationProgress { error?: string; result?: Partial; @@ -109,10 +114,11 @@ const pollSubtitleJob = async ( jobId: string, targetLanguage: string, ttsLanguage: string, + pollTimeoutMs: number, fetchImpl: typeof fetch, onProgress?: (progress: SubtitleGenerationProgress) => void, ): Promise => { - const deadline = Date.now() + SUBTITLE_JOB_TIMEOUT_MS; + const deadline = Date.now() + resolvePollTimeoutMs(pollTimeoutMs); while (true) { const resp = await fetchImpl(apiUrl(`/generate-subtitles/${jobId}`), { @@ -226,7 +232,14 @@ export const generateSubtitlePipeline = async ( } const job = parsed.data as unknown as SubtitleJobResponse; onProgress?.(job); - return pollSubtitleJob(job.jobId, targetLanguage, resolvedTtsLanguage, fetchImpl, onProgress); + return pollSubtitleJob( + job.jobId, + targetLanguage, + resolvedTtsLanguage, + job.pollTimeoutMs ?? SUBTITLE_JOB_TIMEOUT_MS, + fetchImpl, + onProgress, + ); } const formData = new FormData(); @@ -250,5 +263,12 @@ export const generateSubtitlePipeline = async ( throw error; } onProgress?.(parsed.data); - return pollSubtitleJob(parsed.data.jobId, targetLanguage, resolvedTtsLanguage, fetchImpl, onProgress); + return pollSubtitleJob( + parsed.data.jobId, + targetLanguage, + resolvedTtsLanguage, + parsed.data.pollTimeoutMs ?? SUBTITLE_JOB_TIMEOUT_MS, + fetchImpl, + onProgress, + ); }; diff --git a/src/types.ts b/src/types.ts index 03ca427..0f77751 100644 --- a/src/types.ts +++ b/src/types.ts @@ -66,6 +66,7 @@ export interface SubtitleGenerationProgress { stage: SubtitleJobStage; progress: number; message: string; + pollTimeoutMs?: number; } export interface TextStyles {