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; }; 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, payload }; } export async function querySeedanceTask(providerTaskId: string): Promise { 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 }; } 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"; }