Files
openmaic/OpenMAIC/lib/media/adapters/minimax-video-adapter.ts
T

237 lines
7.0 KiB
TypeScript

/**
* MiniMax Video Generation Adapter
* Supports: text-to-video with camera control commands
* API: POST /v1/video_generation (submit) + GET /v1/query/video_generation?task_id=xxx (poll)
* Docs: https://platform.minimaxi.com/docs/api-reference/video-generation-t2v
*/
import type {
VideoGenerationConfig,
VideoGenerationOptions,
VideoGenerationResult,
} from '../types';
import { runPolledTask } from '../polled-task';
const BASE_URL = 'https://api.minimaxi.com';
const POLL_INTERVAL_MS = 5000;
const MAX_POLL_ATTEMPTS = 120; // ~10 minutes max
interface MiniMaxSubmitResponse {
task_id: string;
base_resp: {
status_code: number;
status_msg: string;
};
}
interface MiniMaxQueryResponse {
task_id: string;
status: 'Preparing' | 'Queueing' | 'Processing' | 'Success' | 'Fail';
file_id?: string;
video_width?: number;
video_height?: number;
base_resp: {
status_code: number;
status_msg: string;
};
}
interface MiniMaxFileRetrieveResponse {
file?: {
file_id: string | number;
download_url?: string;
filename?: string;
};
base_resp?: {
status_code: number;
status_msg: string;
};
}
async function submitTask(
config: VideoGenerationConfig,
options: VideoGenerationOptions,
): Promise<string> {
const baseUrl = (config.baseUrl || BASE_URL).replace(/\/$/, '');
const model = config.model || 'MiniMax-Hailuo-2.3';
const duration = options.duration || 6;
// Map OpenMAIC resolution to MiniMax format. MiniMax's mid tier is 768P, not
// 720P — Hailuo 2.3 rejects 720P with "2013 ... does not support resolution
// 720P". Our shared resolution enum has no 768p, so the UI's "720p" maps to
// MiniMax 768P here (and 768P is also the safe fallback for any other value).
const resolutionMap: Record<string, string> = {
'720p': '768P',
'1080p': '1080P',
};
const resolution = resolutionMap[options.resolution || ''] || '768P';
const response = await fetch(`${baseUrl}/v1/video_generation`, {
method: 'POST',
headers: {
Authorization: `Bearer ${config.apiKey}`,
'Content-Type': 'application/json; charset=utf-8',
},
body: JSON.stringify({
model,
prompt: options.prompt,
duration,
resolution,
prompt_optimizer: false,
}),
});
if (!response.ok) {
const errText = await response.text().catch(() => response.statusText);
throw new Error(`MiniMax Video submit error: ${errText}`);
}
const data: MiniMaxSubmitResponse = await response.json();
if (data.base_resp?.status_code !== 0) {
const code = data.base_resp?.status_code;
const msg = data.base_resp?.status_msg || 'unknown error';
throw new Error(`MiniMax Video API error ${code}: ${msg}`);
}
if (!data.task_id) {
throw new Error(`MiniMax Video: no task_id returned. Response: ${JSON.stringify(data)}`);
}
return data.task_id;
}
async function pollTaskStatus(
config: VideoGenerationConfig,
taskId: string,
): Promise<MiniMaxQueryResponse> {
const baseUrl = (config.baseUrl || BASE_URL).replace(/\/$/, '');
const url = `${baseUrl}/v1/query/video_generation?task_id=${encodeURIComponent(taskId)}`;
const response = await fetch(url, {
method: 'GET',
headers: {
Authorization: `Bearer ${config.apiKey}`,
},
});
if (!response.ok) {
const errText = await response.text().catch(() => response.statusText);
throw new Error(`MiniMax Video poll error: ${errText}`);
}
return response.json() as Promise<MiniMaxQueryResponse>;
}
async function retrieveFileDownloadUrl(
config: VideoGenerationConfig,
fileId: string,
): Promise<string> {
const baseUrl = (config.baseUrl || BASE_URL).replace(/\/$/, '');
const url = `${baseUrl}/v1/files/retrieve?file_id=${encodeURIComponent(fileId)}`;
const response = await fetch(url, {
method: 'GET',
headers: {
Authorization: `Bearer ${config.apiKey}`,
},
});
if (!response.ok) {
const errText = await response.text().catch(() => response.statusText);
throw new Error(`MiniMax Video file retrieve error: ${errText}`);
}
const data: MiniMaxFileRetrieveResponse = await response.json();
if (data.base_resp?.status_code !== 0) {
const code = data.base_resp?.status_code;
const msg = data.base_resp?.status_msg || 'unknown error';
throw new Error(`MiniMax Video file retrieve error ${code}: ${msg}`);
}
const downloadUrl = data.file?.download_url;
if (!downloadUrl) {
throw new Error(`MiniMax Video: no download_url returned. Response: ${JSON.stringify(data)}`);
}
return downloadUrl;
}
export async function generateWithMiniMaxVideo(
config: VideoGenerationConfig,
options: VideoGenerationOptions,
): Promise<VideoGenerationResult> {
return runPolledTask<VideoGenerationResult>({
submit: async () => ({
status: 'submitted',
taskId: await submitTask(config, options),
}),
poll: async (taskId) => {
const result = await pollTaskStatus(config, taskId);
if (result.status === 'Success') {
if (!result.file_id) {
throw new Error(`MiniMax Video: task succeeded but no file_id returned`);
}
return {
status: 'done',
result: {
url: await retrieveFileDownloadUrl(config, result.file_id),
width: result.video_width || 1920,
height: result.video_height || 1080,
duration: options.duration || 6,
},
};
}
if (result.status === 'Fail') {
return {
status: 'failed',
message: `MiniMax Video generation failed: ${result.base_resp?.status_msg || 'unknown'}`,
};
}
return { status: 'pending', detail: result.status };
},
intervalMs: POLL_INTERVAL_MS,
maxAttempts: MAX_POLL_ATTEMPTS,
label: 'MiniMax Video',
formatTimeout: ({ attempts, lastPendingDetail }) =>
`MiniMax Video: timeout after ${attempts} polls, last status: ${lastPendingDetail ?? ''}`,
});
}
export async function testMiniMaxVideoConnectivity(
config: VideoGenerationConfig,
): Promise<{ success: boolean; message: string }> {
try {
const baseUrl = (config.baseUrl || BASE_URL).replace(/\/$/, '');
// Submit a minimal task and immediately check if it returns a task_id
const response = await fetch(`${baseUrl}/v1/video_generation`, {
method: 'POST',
redirect: 'manual',
headers: {
Authorization: `Bearer ${config.apiKey}`,
'Content-Type': 'application/json; charset=utf-8',
},
body: JSON.stringify({
model: 'MiniMax-Hailuo-2.3',
prompt: 'test connectivity',
duration: 6,
resolution: '768P',
}),
});
if (response.ok) {
return { success: true, message: 'MiniMax Video API connected' };
}
const errData = await response.json().catch(() => ({}));
const msg = errData?.base_resp?.status_msg || response.statusText;
return { success: false, message: `API error: ${msg}` };
} catch (err) {
return { success: false, message: `Connection failed: ${(err as Error).message}` };
}
}