Initial 智念AIGC platform
This commit is contained in:
121
lib/seedance/client.ts
Normal file
121
lib/seedance/client.ts
Normal file
@@ -0,0 +1,121 @@
|
||||
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";
|
||||
}
|
||||
Reference in New Issue
Block a user