Initial 智念AIGC platform

This commit is contained in:
inman
2026-05-29 10:26:02 +08:00
commit f9c3393f84
86 changed files with 14741 additions and 0 deletions

121
lib/seedance/client.ts Normal file
View 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";
}