功能增加

This commit is contained in:
andy committed 2026-09-11 15:33:18 +08:00
1 parent b340fc7ad7
commit f1993eb388
69 files changed
+2243 -162

No files matched your search

+161
View File
@@ -0,0 +1,161 @@
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;
}