Files
NianAIGC/lib/seedance/client.ts
2026-05-29 10:26:02 +08:00

122 lines
4.5 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { materialContentForProvider, type PromptMaterial } from "@/lib/prompt/assembler";
import {
VIDEO_DURATION_DEFAULT,
VIDEO_RATIO_DEFAULT,
VIDEO_RESOLUTION_DEFAULT,
clampVideoDuration,
normalizeVideoDuration,
normalizeVideoRatio,
normalizeVideoResolution
} from "@/lib/video-settings";
export type SeedanceSettings = {
ratio?: string;
duration?: number;
resolution?: string;
};
export type SeedanceCreateInput = {
prompt: string;
settings: SeedanceSettings;
materials: PromptMaterial[];
origin: string;
};
export type SeedanceQueryResult = {
status: "queued" | "running" | "succeeded" | "failed" | "cancelled";
resultUrl?: string;
errorMessage?: string;
raw: Record<string, unknown>;
};
export function getSeedanceConfig() {
const model = process.env.SEEDANCE_MODEL || "doubao-seedance-2-0-260128";
return {
apiKey: process.env.SEEDANCE_API_KEY,
baseUrl: process.env.SEEDANCE_BASE_URL || "https://ark.cn-beijing.volces.com/api/v3",
model,
ratio: normalizeVideoRatio(process.env.SEEDANCE_RATIO, VIDEO_RATIO_DEFAULT),
duration: clampVideoDuration(process.env.SEEDANCE_DURATION, VIDEO_DURATION_DEFAULT),
resolution: normalizeVideoResolution(process.env.SEEDANCE_RESOLUTION, model, VIDEO_RESOLUTION_DEFAULT)
};
}
export function shouldMockSeedance() {
const flag = process.env.SEEDANCE_MOCK || "auto";
if (flag === "1" || flag === "true") return true;
if (flag === "0" || flag === "false") return false;
return !getSeedanceConfig().apiKey;
}
export async function createSeedanceTask(input: SeedanceCreateInput) {
const config = getSeedanceConfig();
if (!config.apiKey) throw new Error("缺少 SEEDANCE_API_KEY。请在 .env.local 配置火山方舟 API Key。");
const payload = {
model: config.model,
content: [
{ type: "text", text: input.prompt },
...materialContentForProvider(input.materials, input.origin)
],
generate_audio: true,
ratio: normalizeVideoRatio(input.settings.ratio, config.ratio),
duration: normalizeVideoDuration(input.settings.duration) ?? config.duration,
resolution: normalizeVideoResolution(input.settings.resolution, config.model, config.resolution),
watermark: false
};
const response = await fetch(`${config.baseUrl.replace(/\/$/, "")}/contents/generations/tasks`, {
method: "POST",
headers: {
Authorization: `Bearer ${config.apiKey}`,
"Content-Type": "application/json"
},
body: JSON.stringify(payload)
});
const json = await response.json().catch(() => ({}));
if (!response.ok) throw new Error(`Seedance 创建任务失败:${response.status} ${JSON.stringify(json)}`);
const providerTaskId = json.id || json.task_id || json.data?.id || json.data?.task_id;
if (!providerTaskId) throw new Error(`Seedance 响应中缺少任务 ID${JSON.stringify(json)}`);
return {
providerTaskId: String(providerTaskId),
raw: json as Record<string, unknown>,
payload
};
}
export async function querySeedanceTask(providerTaskId: string): Promise<SeedanceQueryResult> {
const config = getSeedanceConfig();
if (!config.apiKey) throw new Error("缺少 SEEDANCE_API_KEY无法查询真实生成任务。");
const response = await fetch(`${config.baseUrl.replace(/\/$/, "")}/contents/generations/tasks/${providerTaskId}`, {
headers: {
Authorization: `Bearer ${config.apiKey}`,
"Content-Type": "application/json"
}
});
const json = await response.json().catch(() => ({}));
if (!response.ok) throw new Error(`Seedance 查询任务失败:${response.status}`);
return {
status: normalizeSeedanceStatus(json.status || json.data?.status),
resultUrl:
json.content?.video_url ||
json.content?.file_url ||
json.video_url ||
json.url ||
json.output ||
json.data?.content?.video_url ||
json.data?.content?.file_url ||
json.data?.video_url ||
json.data?.url ||
json.data?.output,
errorMessage: json.error?.message || json.data?.error?.message,
raw: json as Record<string, unknown>
};
}
function normalizeSeedanceStatus(status: unknown): SeedanceQueryResult["status"] {
const value = String(status || "").toLowerCase();
if (["succeeded", "success", "completed"].includes(value)) return "succeeded";
if (["failed", "error", "expired", "timeout"].includes(value)) return "failed";
if (["cancelled", "canceled"].includes(value)) return "cancelled";
if (["running", "processing", "generating"].includes(value)) return "running";
return "queued";
}