Files
NianAIGC/lib/minimax/client.ts
2026-09-11 15:33:18 +08:00

162 lines
6.3 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 type { PromptMaterial } from "@/lib/prompt/assembler";
import { toAbsoluteUrl } from "@/lib/server/runtime";
import type { GenerationStatus } from "@/lib/types";
export const MINIMAX_H3_MODEL = "MiniMax-H3";
export const MINIMAX_DEFAULT_BASE_URL = "https://api.minimax.cn";
export type MinimaxH3Settings = {
ratio?: string;
duration?: number;
resolution?: string;
};
export type MinimaxH3Usage = {
totalSeconds: number;
inputSeconds: number;
outputSeconds: number;
inputImageCount: number;
};
export function getMinimaxConfig() {
return {
apiKey: process.env.MINIMAX_API_KEY?.trim(),
baseUrl: (process.env.MINIMAX_BASE_URL || MINIMAX_DEFAULT_BASE_URL).replace(/\/+$/, ""),
model: MINIMAX_H3_MODEL
};
}
export function buildMinimaxH3Payload(input: {
prompt: string;
materials: PromptMaterial[];
settings: MinimaxH3Settings;
origin: string;
}) {
const prompt = input.prompt.trim();
if (!prompt) throw new Error("MiniMax H3 视频提示词不能为空。");
if (input.materials.length > 1 || input.materials.some((material) => material.type !== "image")) {
throw new Error("MiniMax H3 第一版支持纯文字生成,或只上传 1 张图片作为首帧。");
}
const duration = Number(input.settings.duration ?? 5);
if (!Number.isInteger(duration) || duration < 4 || duration > 15) {
throw new Error("MiniMax H3 视频时长须为 4–15 秒整数。");
}
const resolution = String(input.settings.resolution || "768P").toUpperCase();
if (resolution !== "768P" && resolution !== "2K") {
throw new Error("MiniMax H3 分辨率仅支持 768P 或 2K。");
}
const ratio = input.materials.length === 1 ? "adaptive" : String(input.settings.ratio || "16:9");
if (input.materials.length === 0 && !["21:9", "16:9", "4:3", "1:1", "3:4", "9:16"].includes(ratio)) {
throw new Error("MiniMax H3 文生视频须选择固定画面比例。");
}
return {
model: MINIMAX_H3_MODEL,
content: [
{ type: "text", text: prompt },
...input.materials.map((material) => ({
type: "image_url",
image_url: { url: toAbsoluteUrl(material.url, input.origin) },
role: "first_frame"
}))
],
resolution,
duration,
ratio,
aigc_watermark: false
};
}
export async function createMinimaxH3Task(payload: ReturnType<typeof buildMinimaxH3Payload>) {
const config = requiredConfig();
const response = await minimaxFetch(`${config.baseUrl}/v2/video_generation`, config.apiKey, {
method: "POST",
body: JSON.stringify(payload)
});
const providerTaskId = text(response.task_id);
if (!providerTaskId) throw new Error("MiniMax H3 响应中缺少任务 ID。");
return { providerTaskId, raw: response };
}
export async function queryMinimaxH3Task(providerTaskId: string) {
const config = requiredConfig();
const response = await minimaxFetch(`${config.baseUrl}/v2/query/video_generation/${encodeURIComponent(providerTaskId)}`, config.apiKey);
const task = record(response.task);
const content = record(task.content);
const error = record(task.error);
return {
status: normalizeStatus(task.status),
resultUrl: text(content.url),
errorMessage: minimaxTaskError(text(error.code), text(error.message)),
usage: extractMinimaxH3Usage(task.usage),
raw: response
};
}
export function extractMinimaxH3Usage(value: unknown): MinimaxH3Usage | undefined {
const usage = record(value);
const outputSeconds = nonNegativeInteger(usage.output_seconds);
if (!outputSeconds) return undefined;
return {
totalSeconds: nonNegativeInteger(usage.total_seconds),
inputSeconds: nonNegativeInteger(usage.input_seconds),
outputSeconds,
inputImageCount: nonNegativeInteger(usage.input_image_count)
};
}
async function minimaxFetch(url: string, apiKey: string, init: RequestInit = {}) {
const response = await fetch(url, {
...init,
headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json", ...(init.headers || {}) }
});
const json = await response.json().catch(() => ({})) as Record<string, unknown>;
if (!response.ok) throw new Error(minimaxHTTPError(response.status));
return json;
}
function requiredConfig() {
const config = getMinimaxConfig();
if (!config.apiKey) throw new Error("缺少 MINIMAX_API_KEY,请先配置 MiniMax 凭据。");
return { ...config, apiKey: config.apiKey };
}
function minimaxHTTPError(status: number) {
if (status === 401) return "MiniMax 鉴权失败,请检查 API Key 是否正确且已开通 H3。";
if (status === 403) return "MiniMax API Key 无 H3 调用权限,请确认已购买并开通该模型。";
if (status === 402) return "MiniMax API 账户余额不足,请充值后重试。";
if (status === 404) return "MiniMax H3 接口或模型不可用,请检查 Base URL 和账号模型权限。";
if (status === 422) return "视频描述或输入素材触发内容安全审核,请调整后重试。";
if (status === 429) return "MiniMax 请求频率过高,请稍后重试。";
if (status >= 500) return "MiniMax 服务暂时不可用,请稍后重试。";
return "MiniMax 请求参数不符合要求,请检查提示词、素材和生成参数。";
}
function minimaxTaskError(code?: string, message?: string) {
if (code === "1008") return "MiniMax API 账户余额不足,请充值后重试。";
if (code === "1026") return "视频描述或输入素材触发内容安全审核,请调整后重试。";
if (code === "1002") return "MiniMax 请求频率过高,请稍后重试。";
return message ? "MiniMax 视频生成失败,请调整提示词或素材后重试。" : undefined;
}
function normalizeStatus(value: unknown): GenerationStatus {
const status = String(value || "").toLowerCase();
if (status === "succeeded") return "succeeded";
if (status === "failed") return "failed";
if (status === "cancelled") return "cancelled";
if (status === "running") return "running";
return "queued";
}
function record(value: unknown): Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value) ? value as Record<string, unknown> : {};
}
function text(value: unknown) {
return typeof value === "string" && value.trim() ? value.trim() : undefined;
}
function nonNegativeInteger(value: unknown) {
const number = Number(value);
return Number.isInteger(number) && number >= 0 ? number : 0;
}