minimax-video-adapter.ts 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232
  1. /**
  2. * MiniMax Video Generation Adapter
  3. * Supports: text-to-video with camera control commands
  4. * API: POST /v1/video_generation (submit) + GET /v1/query/video_generation?task_id=xxx (poll)
  5. * Docs: https://platform.minimaxi.com/docs/api-reference/video-generation-t2v
  6. */
  7. import type {
  8. VideoGenerationConfig,
  9. VideoGenerationOptions,
  10. VideoGenerationResult,
  11. } from '../types';
  12. const BASE_URL = 'https://api.minimaxi.com';
  13. const POLL_INTERVAL_MS = 5000;
  14. const MAX_POLL_ATTEMPTS = 120; // ~10 minutes max
  15. interface MiniMaxSubmitResponse {
  16. task_id: string;
  17. base_resp: {
  18. status_code: number;
  19. status_msg: string;
  20. };
  21. }
  22. interface MiniMaxQueryResponse {
  23. task_id: string;
  24. status: 'Preparing' | 'Queueing' | 'Processing' | 'Success' | 'Fail';
  25. file_id?: string;
  26. video_width?: number;
  27. video_height?: number;
  28. base_resp: {
  29. status_code: number;
  30. status_msg: string;
  31. };
  32. }
  33. interface MiniMaxFileRetrieveResponse {
  34. file?: {
  35. file_id: string | number;
  36. download_url?: string;
  37. filename?: string;
  38. };
  39. base_resp?: {
  40. status_code: number;
  41. status_msg: string;
  42. };
  43. }
  44. async function submitTask(
  45. config: VideoGenerationConfig,
  46. options: VideoGenerationOptions,
  47. ): Promise<string> {
  48. const baseUrl = (config.baseUrl || BASE_URL).replace(/\/$/, '');
  49. const model = config.model || 'MiniMax-Hailuo-2.3';
  50. const duration = options.duration || 6;
  51. // Map OpenMAIC resolution to MiniMax format
  52. const resolutionMap: Record<string, string> = {
  53. '720p': '720P',
  54. '1080p': '1080P',
  55. };
  56. const resolution = resolutionMap[options.resolution || ''] || '768P';
  57. const response = await fetch(`${baseUrl}/v1/video_generation`, {
  58. method: 'POST',
  59. headers: {
  60. Authorization: `Bearer ${config.apiKey}`,
  61. 'Content-Type': 'application/json; charset=utf-8',
  62. },
  63. body: JSON.stringify({
  64. model,
  65. prompt: options.prompt,
  66. duration,
  67. resolution,
  68. prompt_optimizer: false,
  69. }),
  70. });
  71. if (!response.ok) {
  72. const errText = await response.text().catch(() => response.statusText);
  73. throw new Error(`MiniMax Video submit error: ${errText}`);
  74. }
  75. const data: MiniMaxSubmitResponse = await response.json();
  76. if (data.base_resp?.status_code !== 0) {
  77. const code = data.base_resp?.status_code;
  78. const msg = data.base_resp?.status_msg || 'unknown error';
  79. throw new Error(`MiniMax Video API error ${code}: ${msg}`);
  80. }
  81. if (!data.task_id) {
  82. throw new Error(`MiniMax Video: no task_id returned. Response: ${JSON.stringify(data)}`);
  83. }
  84. return data.task_id;
  85. }
  86. async function pollTaskStatus(
  87. config: VideoGenerationConfig,
  88. taskId: string,
  89. ): Promise<MiniMaxQueryResponse> {
  90. const baseUrl = (config.baseUrl || BASE_URL).replace(/\/$/, '');
  91. const url = `${baseUrl}/v1/query/video_generation?task_id=${encodeURIComponent(taskId)}`;
  92. const response = await fetch(url, {
  93. method: 'GET',
  94. headers: {
  95. Authorization: `Bearer ${config.apiKey}`,
  96. },
  97. });
  98. if (!response.ok) {
  99. const errText = await response.text().catch(() => response.statusText);
  100. throw new Error(`MiniMax Video poll error: ${errText}`);
  101. }
  102. return response.json() as Promise<MiniMaxQueryResponse>;
  103. }
  104. async function retrieveFileDownloadUrl(
  105. config: VideoGenerationConfig,
  106. fileId: string,
  107. ): Promise<string> {
  108. const baseUrl = (config.baseUrl || BASE_URL).replace(/\/$/, '');
  109. const url = `${baseUrl}/v1/files/retrieve?file_id=${encodeURIComponent(fileId)}`;
  110. const response = await fetch(url, {
  111. method: 'GET',
  112. headers: {
  113. Authorization: `Bearer ${config.apiKey}`,
  114. },
  115. });
  116. if (!response.ok) {
  117. const errText = await response.text().catch(() => response.statusText);
  118. throw new Error(`MiniMax Video file retrieve error: ${errText}`);
  119. }
  120. const data: MiniMaxFileRetrieveResponse = await response.json();
  121. if (data.base_resp?.status_code !== 0) {
  122. const code = data.base_resp?.status_code;
  123. const msg = data.base_resp?.status_msg || 'unknown error';
  124. throw new Error(`MiniMax Video file retrieve error ${code}: ${msg}`);
  125. }
  126. const downloadUrl = data.file?.download_url;
  127. if (!downloadUrl) {
  128. throw new Error(`MiniMax Video: no download_url returned. Response: ${JSON.stringify(data)}`);
  129. }
  130. return downloadUrl;
  131. }
  132. export async function generateWithMiniMaxVideo(
  133. config: VideoGenerationConfig,
  134. options: VideoGenerationOptions,
  135. ): Promise<VideoGenerationResult> {
  136. // Step 1: Submit task
  137. const taskId = await submitTask(config, options);
  138. // Step 2: Poll until complete
  139. let lastStatus = '';
  140. let attempts = 0;
  141. while (attempts < MAX_POLL_ATTEMPTS) {
  142. await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
  143. const result = await pollTaskStatus(config, taskId);
  144. lastStatus = result.status;
  145. if (result.status === 'Success') {
  146. if (!result.file_id) {
  147. throw new Error(`MiniMax Video: task succeeded but no file_id returned`);
  148. }
  149. const videoUrl = await retrieveFileDownloadUrl(config, result.file_id);
  150. return {
  151. url: videoUrl,
  152. width: result.video_width || 1920,
  153. height: result.video_height || 1080,
  154. duration: options.duration || 6,
  155. };
  156. }
  157. if (result.status === 'Fail') {
  158. throw new Error(
  159. `MiniMax Video generation failed: ${result.base_resp?.status_msg || 'unknown'}`,
  160. );
  161. }
  162. attempts++;
  163. }
  164. throw new Error(
  165. `MiniMax Video: timeout after ${MAX_POLL_ATTEMPTS} polls, last status: ${lastStatus}`,
  166. );
  167. }
  168. export async function testMiniMaxVideoConnectivity(
  169. config: VideoGenerationConfig,
  170. ): Promise<{ success: boolean; message: string }> {
  171. try {
  172. const baseUrl = (config.baseUrl || BASE_URL).replace(/\/$/, '');
  173. // Submit a minimal task and immediately check if it returns a task_id
  174. const response = await fetch(`${baseUrl}/v1/video_generation`, {
  175. method: 'POST',
  176. headers: {
  177. Authorization: `Bearer ${config.apiKey}`,
  178. 'Content-Type': 'application/json; charset=utf-8',
  179. },
  180. body: JSON.stringify({
  181. model: 'MiniMax-Hailuo-2.3',
  182. prompt: 'test connectivity',
  183. duration: 6,
  184. resolution: '768P',
  185. }),
  186. });
  187. if (response.ok) {
  188. return { success: true, message: 'MiniMax Video API connected' };
  189. }
  190. const errData = await response.json().catch(() => ({}));
  191. const msg = errData?.base_resp?.status_msg || response.statusText;
  192. return { success: false, message: `API error: ${msg}` };
  193. } catch (err) {
  194. return { success: false, message: `Connection failed: ${(err as Error).message}` };
  195. }
  196. }