

























@@ -55,6 +55,14 @@ const SEEDANCE_REFERENCE_MAX_AUDIOS_BY_MODEL = Object.fromEntries(
5555const DEFAULT_HTTP_TIMEOUT_MS = 30_000;
5656const DEFAULT_OPERATION_TIMEOUT_MS = 1_200_000;
5757const POLL_INTERVAL_MS = 5_000;
58+const FAL_VIDEO_MALFORMED_RESPONSE = "fal video generation response malformed";
59+const FAL_VIDEO_PENDING_STATUSES = new Set([
60+"IN_QUEUE",
61+"IN_PROGRESS",
62+"PROCESSING",
63+"QUEUED",
64+"STARTED",
65+]);
58665967type FalVideoResponse = {
6068video?: {
@@ -89,6 +97,74 @@ export function _setFalVideoFetchGuardForTesting(impl: typeof fetchWithSsrFGuard
8997falFetchGuard = impl ?? fetchWithSsrFGuard;
9098}
9199100+function isRecord(value: unknown): value is Record<string, unknown> {
101+return Boolean(value && typeof value === "object" && !Array.isArray(value));
102+}
103+104+function normalizeFalVideoUrl(value: unknown): string | undefined {
105+const normalized = normalizeOptionalString(value);
106+if (!normalized && value !== undefined && value !== null) {
107+throw new Error(FAL_VIDEO_MALFORMED_RESPONSE);
108+}
109+return normalized;
110+}
111+112+function readFalVideoPayload(payload: unknown): FalVideoResponse {
113+if (!isRecord(payload)) {
114+throw new Error(FAL_VIDEO_MALFORMED_RESPONSE);
115+}
116+const video = payload.video;
117+const videos = payload.videos;
118+if (video !== undefined && video !== null && !isRecord(video)) {
119+throw new Error(FAL_VIDEO_MALFORMED_RESPONSE);
120+}
121+if (videos !== undefined && videos !== null && !Array.isArray(videos)) {
122+throw new Error(FAL_VIDEO_MALFORMED_RESPONSE);
123+}
124+return {
125+video: isRecord(video)
126+ ? {
127+url: normalizeFalVideoUrl(video.url),
128+content_type: normalizeOptionalString(video.content_type),
129+}
130+ : undefined,
131+videos: Array.isArray(videos)
132+ ? videos.map((entry) => {
133+if (!isRecord(entry)) {
134+throw new Error(FAL_VIDEO_MALFORMED_RESPONSE);
135+}
136+return {
137+url: normalizeFalVideoUrl(entry.url),
138+content_type: normalizeOptionalString(entry.content_type),
139+};
140+})
141+ : undefined,
142+prompt: normalizeOptionalString(payload.prompt),
143+seed: typeof payload.seed === "number" ? payload.seed : undefined,
144+};
145+}
146+147+function readFalQueueResponse(payload: unknown): FalQueueResponse {
148+if (!isRecord(payload)) {
149+throw new Error(FAL_VIDEO_MALFORMED_RESPONSE);
150+}
151+const error = payload.error;
152+if (error !== undefined && error !== null && !isRecord(error)) {
153+throw new Error(FAL_VIDEO_MALFORMED_RESPONSE);
154+}
155+return {
156+status: normalizeOptionalString(payload.status),
157+request_id: normalizeOptionalString(payload.request_id),
158+response_url: normalizeOptionalString(payload.response_url),
159+status_url: normalizeOptionalString(payload.status_url),
160+cancel_url: normalizeOptionalString(payload.cancel_url),
161+detail: normalizeOptionalString(payload.detail),
162+response: payload.response === undefined ? undefined : readFalVideoPayload(payload.response),
163+prompt: normalizeOptionalString(payload.prompt),
164+error: isRecord(error) ? { message: normalizeOptionalString(error.message) } : undefined,
165+};
166+}
167+92168function toDataUrl(buffer: Buffer, mimeType: string): string {
93169return `data:${mimeType};base64,${buffer.toString("base64")}`;
94170}
@@ -355,7 +431,11 @@ async function fetchFalJson(params: {
355431});
356432try {
357433await assertOkOrThrowHttpError(response, params.errorContext);
358-return await response.json();
434+try {
435+return await response.json();
436+} catch {
437+throw new Error(FAL_VIDEO_MALFORMED_RESPONSE);
438+}
359439} finally {
360440await release();
361441}
@@ -372,35 +452,40 @@ async function waitForFalQueueResult(params: {
372452const deadline = Date.now() + params.timeoutMs;
373453let lastStatus = "unknown";
374454while (Date.now() < deadline) {
375-const payload = (await fetchFalJson({
376-url: params.statusUrl,
377-init: {
378-method: "GET",
379-headers: params.headers,
380-},
381-timeoutMs: DEFAULT_HTTP_TIMEOUT_MS,
382-policy: params.policy,
383-dispatcherPolicy: params.dispatcherPolicy,
384-auditContext: "fal-video-status",
385-errorContext: "fal video status request failed",
386-})) as FalQueueResponse;
387-const status = normalizeOptionalString(payload.status)?.toUpperCase();
388-if (status) {
389-lastStatus = status;
390-}
391-if (status === "COMPLETED") {
392-return (await fetchFalJson({
393-url: params.responseUrl,
455+const payload = readFalQueueResponse(
456+await fetchFalJson({
457+url: params.statusUrl,
394458init: {
395459method: "GET",
396460headers: params.headers,
397461},
398462timeoutMs: DEFAULT_HTTP_TIMEOUT_MS,
399463policy: params.policy,
400464dispatcherPolicy: params.dispatcherPolicy,
401-auditContext: "fal-video-result",
402-errorContext: "fal video result request failed",
403-})) as FalQueueResponse;
465+auditContext: "fal-video-status",
466+errorContext: "fal video status request failed",
467+}),
468+);
469+const status = normalizeOptionalString(payload.status)?.toUpperCase();
470+if (!status) {
471+throw new Error(FAL_VIDEO_MALFORMED_RESPONSE);
472+}
473+lastStatus = status;
474+if (status === "COMPLETED") {
475+return readFalQueueResponse(
476+await fetchFalJson({
477+url: params.responseUrl,
478+init: {
479+method: "GET",
480+headers: params.headers,
481+},
482+timeoutMs: DEFAULT_HTTP_TIMEOUT_MS,
483+policy: params.policy,
484+dispatcherPolicy: params.dispatcherPolicy,
485+auditContext: "fal-video-result",
486+errorContext: "fal video result request failed",
487+}),
488+);
404489}
405490if (status === "FAILED" || status === "CANCELLED") {
406491throw new Error(
@@ -409,16 +494,19 @@ async function waitForFalQueueResult(params: {
409494`fal video generation ${normalizeLowercaseStringOrEmpty(status)}`,
410495);
411496}
497+if (!FAL_VIDEO_PENDING_STATUSES.has(status)) {
498+throw new Error(FAL_VIDEO_MALFORMED_RESPONSE);
499+}
412500await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
413501}
414502throw new Error(`fal video generation did not finish in time (last status: ${lastStatus})`);
415503}
416504417505function extractFalVideoPayload(payload: FalQueueResponse): FalVideoResponse {
418-if (payload.response && typeof payload.response === "object") {
506+if (payload.response) {
419507return payload.response;
420508}
421-return payload as FalVideoResponse;
509+return readFalVideoPayload(payload);
422510}
423511424512export function buildFalVideoGenerationProvider(): VideoGenerationProvider {
@@ -509,19 +597,21 @@ export function buildFalVideoGenerationProvider(): VideoGenerationProvider {
509597const requestBody = buildFalVideoRequestBody({ req, model });
510598const policy = buildPolicy(allowPrivateNetwork);
511599const queueBaseUrl = resolveFalQueueBaseUrl(baseUrl);
512-const submitted = (await fetchFalJson({
513-url: `${queueBaseUrl}/${model}`,
514-init: {
515-method: "POST",
516- headers,
517-body: JSON.stringify(requestBody),
518-},
519-timeoutMs: DEFAULT_HTTP_TIMEOUT_MS,
520- policy,
521- dispatcherPolicy,
522-auditContext: "fal-video-submit",
523-errorContext: "fal video generation failed",
524-})) as FalQueueResponse;
600+const submitted = readFalQueueResponse(
601+await fetchFalJson({
602+url: `${queueBaseUrl}/${model}`,
603+init: {
604+method: "POST",
605+ headers,
606+body: JSON.stringify(requestBody),
607+},
608+timeoutMs: DEFAULT_HTTP_TIMEOUT_MS,
609+ policy,
610+ dispatcherPolicy,
611+auditContext: "fal-video-submit",
612+errorContext: "fal video generation failed",
613+}),
614+);
525615const statusUrl = normalizeOptionalString(submitted.status_url);
526616const responseUrl = normalizeOptionalString(submitted.response_url);
527617if (!statusUrl || !responseUrl) {
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。