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) { 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; 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 { return typeof value === "object" && value !== null && !Array.isArray(value) ? value as Record : {}; } 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; }