超时时间
All checks were successful
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 1m7s

This commit is contained in:
Song367 2026-03-19 21:02:22 +08:00
parent 04072dc94b
commit e3c4b0f697
6 changed files with 131 additions and 3 deletions

View File

@ -13,6 +13,7 @@ import {
resolveMiniMaxTtsConfig, resolveMiniMaxTtsConfig,
} from './src/server/minimaxTts'; } from './src/server/minimaxTts';
import { generateSubtitlePipeline } from './src/server/subtitleGeneration'; import { generateSubtitlePipeline } from './src/server/subtitleGeneration';
import { resolveLlmProviderConfig } from './src/server/llmProvider';
import { parseSubtitleRequest } from './src/server/subtitleRequest'; import { parseSubtitleRequest } from './src/server/subtitleRequest';
import { import {
buildAssSubtitleContent, buildAssSubtitleContent,
@ -262,6 +263,9 @@ async function startServer() {
}); });
const { provider, targetLanguage, ttsLanguage, fileId } = parseSubtitleRequest(req.body); 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) { if (!videoPath && !fileId) {
logEvent({ logEvent({
level: 'warn', level: 'warn',
@ -286,6 +290,7 @@ async function startServer() {
provider, provider,
targetLanguage, targetLanguage,
ttsLanguage, ttsLanguage,
pollTimeoutMs,
fileId, fileId,
filePath: videoPath, filePath: videoPath,
}); });

View File

@ -56,4 +56,18 @@ describe('subtitleJobs', () => {
expect(toSubtitleJobResponse(job)).not.toHaveProperty('filePath'); 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,
});
});
}); });

View File

@ -22,6 +22,7 @@ export interface SubtitleJob {
provider?: string | null; provider?: string | null;
targetLanguage: string; targetLanguage: string;
ttsLanguage?: string; ttsLanguage?: string;
pollTimeoutMs?: number;
fileId?: string; fileId?: string;
filePath?: string; filePath?: string;
error?: string; error?: string;
@ -73,6 +74,7 @@ export const createSubtitleJob = (
provider, provider,
targetLanguage, targetLanguage,
ttsLanguage, ttsLanguage,
pollTimeoutMs,
fileId, fileId,
filePath, filePath,
}: { }: {
@ -80,6 +82,7 @@ export const createSubtitleJob = (
provider?: string | null; provider?: string | null;
targetLanguage: string; targetLanguage: string;
ttsLanguage?: string; ttsLanguage?: string;
pollTimeoutMs?: number;
fileId?: string; fileId?: string;
filePath?: string; filePath?: string;
}, },
@ -91,6 +94,7 @@ export const createSubtitleJob = (
provider, provider,
targetLanguage, targetLanguage,
ttsLanguage, ttsLanguage,
pollTimeoutMs,
fileId, fileId,
filePath, filePath,
status: 'queued', status: 'queued',
@ -159,6 +163,7 @@ export const toSubtitleGenerationProgress = (job: SubtitleJob): SubtitleGenerati
stage: job.stage, stage: job.stage,
progress: job.progress, progress: job.progress,
message: job.message, message: job.message,
...(job.pollTimeoutMs ? { pollTimeoutMs: job.pollTimeoutMs } : {}),
}); });
export const toSubtitleJobResponse = (job: SubtitleJob) => ({ export const toSubtitleJobResponse = (job: SubtitleJob) => ({

View File

@ -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;
});
}); });

View File

@ -11,6 +11,11 @@ const ARK_FILE_STATUS_TIMEOUT_MS = 120000;
const SUBTITLE_JOB_POLL_INTERVAL_MS = 5000; const SUBTITLE_JOB_POLL_INTERVAL_MS = 5000;
const SUBTITLE_JOB_TIMEOUT_MS = 20 * 60 * 1000; 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 { interface SubtitleJobResponse extends SubtitleGenerationProgress {
error?: string; error?: string;
result?: Partial<SubtitlePipelineResult>; result?: Partial<SubtitlePipelineResult>;
@ -109,10 +114,11 @@ const pollSubtitleJob = async (
jobId: string, jobId: string,
targetLanguage: string, targetLanguage: string,
ttsLanguage: string, ttsLanguage: string,
pollTimeoutMs: number,
fetchImpl: typeof fetch, fetchImpl: typeof fetch,
onProgress?: (progress: SubtitleGenerationProgress) => void, onProgress?: (progress: SubtitleGenerationProgress) => void,
): Promise<SubtitlePipelineResult> => { ): Promise<SubtitlePipelineResult> => {
const deadline = Date.now() + SUBTITLE_JOB_TIMEOUT_MS; const deadline = Date.now() + resolvePollTimeoutMs(pollTimeoutMs);
while (true) { while (true) {
const resp = await fetchImpl(apiUrl(`/generate-subtitles/${jobId}`), { const resp = await fetchImpl(apiUrl(`/generate-subtitles/${jobId}`), {
@ -226,7 +232,14 @@ export const generateSubtitlePipeline = async (
} }
const job = parsed.data as unknown as SubtitleJobResponse; const job = parsed.data as unknown as SubtitleJobResponse;
onProgress?.(job); 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(); const formData = new FormData();
@ -250,5 +263,12 @@ export const generateSubtitlePipeline = async (
throw error; throw error;
} }
onProgress?.(parsed.data); 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,
);
}; };

View File

@ -66,6 +66,7 @@ export interface SubtitleGenerationProgress {
stage: SubtitleJobStage; stage: SubtitleJobStage;
progress: number; progress: number;
message: string; message: string;
pollTimeoutMs?: number;
} }
export interface TextStyles { export interface TextStyles {