122 lines
4.5 KiB
TypeScript
122 lines
4.5 KiB
TypeScript
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";
|
||
}
|