feat: add Bailian image generation support

This commit is contained in:
inman
2026-07-14 12:58:50 +08:00
parent 9a3070c749
commit f642b5e71f
18 changed files with 464 additions and 74 deletions

View File

@@ -11,12 +11,16 @@ import { importRemoteAssetAsAsset } from "@/lib/server/storage";
import { createSeedanceTask, getSeedanceConfig, querySeedanceTask, shouldMockSeedance, type SeedanceSettings } from "@/lib/seedance/client";
import type { GenerationJob } from "@/lib/types";
import { normalizeVideoDuration, normalizeVideoRatio, normalizeVideoResolution } from "@/lib/video-settings";
import { bailianResultUrls, bailianStatus, bailianTaskId, buildBailianVideoPayload, getBailianConfig, queryBailianTask, shouldMockBailian, submitBailianTask } from "@/lib/bailian/client";
export type VideoCreationEngine = "seedance" | "bailian";
export type SubmitVideoJobInput = PromptAssemblyInput & {
ownerId?: string;
externalClientId?: string;
prompt?: string;
settings?: SeedanceSettings;
engine?: VideoCreationEngine;
materials?: PromptMaterial[];
retryOf?: string;
idempotencyKey?: string;
@@ -28,6 +32,7 @@ export type SubmitVideoJobInput = PromptAssemblyInput & {
export async function submitVideoJob(input: SubmitVideoJobInput, origin: string): Promise<GenerationJob> {
const ownerId = input.ownerId || DEFAULT_OWNER_ID;
const engine: VideoCreationEngine = input.engine === "bailian" ? "bailian" : "seedance";
const config = getSeedanceConfig();
const assembled = assemblePrompt({
...input,
@@ -36,17 +41,24 @@ export async function submitVideoJob(input: SubmitVideoJobInput, origin: string)
});
const finalPrompt = input.prompt?.trim() || assembled.prompt;
const settings: SeedanceSettings = { ...(input.settings || {}) };
settings.ratio = normalizeVideoRatio(settings.ratio, config.ratio);
settings.duration = normalizeVideoDuration(settings.duration) ?? config.duration;
settings.resolution = normalizeVideoResolution(settings.resolution, config.model, config.resolution);
const mock = shouldMockSeedance();
if (engine === "bailian") {
settings.duration = normalizeVideoDuration(settings.duration) ?? 10;
settings.resolution = String(settings.resolution || "720P").toUpperCase();
buildBailianVideoPayload({ prompt: finalPrompt, materials: assembled.materials, origin, settings });
} else {
settings.ratio = normalizeVideoRatio(settings.ratio, config.ratio);
settings.duration = normalizeVideoDuration(settings.duration) ?? config.duration;
settings.resolution = normalizeVideoResolution(settings.resolution, config.model, config.resolution);
}
const mock = engine === "bailian" ? shouldMockBailian() : shouldMockSeedance();
const missingBailianKey = engine === "bailian" && !mock && !getBailianConfig().apiKey;
let job = await createGenerationJob({
ownerId,
externalClientId: input.externalClientId,
capability: "video.generate",
provider: mock ? "mock" : "seedance",
reqKey: config.model,
status: "queued",
provider: mock ? "mock" : engine,
reqKey: engine === "bailian" ? getBailianConfig().videoModel : config.model,
status: missingBailianKey ? "failed" : "queued",
prompt: finalPrompt,
inputAssetIds: input.materials?.map((material) => material.id).filter(Boolean) as string[] || [],
inputUrls: assembled.materials.map((material) => material.url),
@@ -54,8 +66,10 @@ export async function submitVideoJob(input: SubmitVideoJobInput, origin: string)
requestPayload: {
input,
assembled,
settings
settings,
engine
},
error: missingBailianKey ? { message: "缺少 BAILIAN_API_KEY请先在设置页配置阿里云百炼 API Key。", retryable: true } : undefined,
retryOf: input.retryOf,
idempotencyKey: input.idempotencyKey,
idempotencyFingerprint: input.idempotencyFingerprint,
@@ -84,17 +98,14 @@ async function dispatchVideoJob(job: GenerationJob, origin: string): Promise<Gen
const materials = Array.isArray(assembled.materials)
? assembled.materials as PromptMaterial[]
: input.materials || [];
const response = await createSeedanceTask({
prompt: job.prompt || "",
settings,
materials,
origin
});
return updateGenerationJob(job.id, {
status: "running",
providerTaskId: response.providerTaskId,
responsePayload: response.raw
});
if (job.provider === "bailian") {
const response = await submitBailianTask("video", buildBailianVideoPayload({ prompt: job.prompt || "", settings: asRecord(settings), materials, origin }));
const providerTaskId = bailianTaskId(response);
if (!providerTaskId) throw new Error("视频任务响应缺少 task_id。");
return updateGenerationJob(job.id, { status: "running", providerTaskId, responsePayload: response });
}
const response = await createSeedanceTask({ prompt: job.prompt || "", settings, materials, origin });
return updateGenerationJob(job.id, { status: "running", providerTaskId: response.providerTaskId, responsePayload: response.raw });
} catch (error) {
job = await updateGenerationJob(job.id, {
status: "failed",
@@ -115,6 +126,15 @@ export async function syncVideoJob(jobId: string, origin: string): Promise<Gener
if (!job.providerTaskId) return job;
try {
if (job.provider === "bailian") {
const response = await queryBailianTask(job.providerTaskId);
const status = bailianStatus(response);
const resultUrl = bailianResultUrls(response, "video")[0];
if (status !== "succeeded" || !resultUrl) {
return updateGenerationJob(job.id, { status, responsePayload: response, error: status === "failed" ? { message: response.message || "百炼视频任务失败。", retryable: false } : undefined });
}
return completeRemoteVideo(job, origin, resultUrl, response);
}
const result = await querySeedanceTask(job.providerTaskId);
if (result.status !== "succeeded" || !result.resultUrl) {
return updateGenerationJob(job.id, {
@@ -123,9 +143,22 @@ export async function syncVideoJob(jobId: string, origin: string): Promise<Gener
error: result.errorMessage ? { message: result.errorMessage, retryable: result.status === "failed" } : undefined
});
}
return completeRemoteVideo(job, origin, result.resultUrl, result.raw);
} catch (error) {
return updateGenerationJob(job.id, {
status: "failed",
error: {
message: error instanceof Error ? error.message : String(error),
retryable: false
}
});
}
}
async function completeRemoteVideo(job: GenerationJob, origin: string, resultUrl: string, responsePayload: Record<string, unknown>) {
const asset = await importRemoteAssetAsAsset({
ownerId: job.ownerId,
url: result.resultUrl,
url: resultUrl,
origin,
source: "generated",
capability: "video.generate",
@@ -144,17 +177,8 @@ export async function syncVideoJob(jobId: string, origin: string): Promise<Gener
return updateGenerationJob(job.id, {
status: "succeeded",
outputAssetIds: [asset.id],
responsePayload: result.raw
responsePayload
});
} catch (error) {
return updateGenerationJob(job.id, {
status: "failed",
error: {
message: error instanceof Error ? error.message : String(error),
retryable: false
}
});
}
}
export async function retryVideoJob(jobId: string, origin: string, ownerId?: string): Promise<GenerationJob> {