grok-video-adapter.ts 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209
  1. /**
  2. * Grok (xAI) Video Generation Adapter
  3. *
  4. * Async task pattern: submit → poll → return video URL.
  5. *
  6. * REST endpoints:
  7. * - Submit: POST /v1/videos/generations
  8. * - Poll: GET /v1/videos/{request_id}
  9. *
  10. * Supported models:
  11. * - grok-imagine-video ($0.05/sec)
  12. *
  13. * Authentication: Bearer token via Authorization header
  14. *
  15. * API docs: https://docs.x.ai/developers/rest-api-reference/inference/videos
  16. */
  17. import type {
  18. VideoGenerationConfig,
  19. VideoGenerationOptions,
  20. VideoGenerationResult,
  21. } from '../types';
  22. const DEFAULT_MODEL = 'grok-imagine-video';
  23. const DEFAULT_BASE_URL = 'https://api.x.ai/v1';
  24. const POLL_INTERVAL_MS = 10_000; // 10 seconds
  25. const MAX_POLL_ATTEMPTS = 60; // 10 minutes max
  26. function delay(ms: number): Promise<void> {
  27. return new Promise((resolve) => setTimeout(resolve, ms));
  28. }
  29. /** Dimension defaults per aspect ratio */
  30. function getDimensions(aspectRatio?: string): {
  31. width: number;
  32. height: number;
  33. } {
  34. switch (aspectRatio) {
  35. case '9:16':
  36. return { width: 720, height: 1280 };
  37. case '1:1':
  38. return { width: 1080, height: 1080 };
  39. case '4:3':
  40. return { width: 1024, height: 768 };
  41. default:
  42. return { width: 1280, height: 720 }; // 16:9
  43. }
  44. }
  45. /** Common headers for all Grok Video API calls */
  46. function apiHeaders(apiKey: string): Record<string, string> {
  47. return {
  48. 'Content-Type': 'application/json',
  49. Authorization: `Bearer ${apiKey}`,
  50. };
  51. }
  52. // ---------------------------------------------------------------------------
  53. // REST types
  54. // ---------------------------------------------------------------------------
  55. interface GrokVideoSubmitResponse {
  56. request_id: string;
  57. }
  58. interface GrokVideoPollResponse {
  59. status: string; // "pending" | "done" | "failed"
  60. progress?: number; // 0-100
  61. video?: {
  62. url: string;
  63. duration: number;
  64. respect_moderation?: boolean;
  65. };
  66. model?: string;
  67. }
  68. // ---------------------------------------------------------------------------
  69. // Connectivity test
  70. // ---------------------------------------------------------------------------
  71. /**
  72. * Lightweight connectivity test — validates API key by making a minimal
  73. * request that triggers auth check. 401/403 means key invalid.
  74. */
  75. export async function testGrokVideoConnectivity(
  76. config: VideoGenerationConfig,
  77. ): Promise<{ success: boolean; message: string }> {
  78. const baseUrl = config.baseUrl || DEFAULT_BASE_URL;
  79. try {
  80. const response = await fetch(`${baseUrl}/videos/generations`, {
  81. method: 'POST',
  82. headers: apiHeaders(config.apiKey),
  83. body: JSON.stringify({
  84. model: config.model || DEFAULT_MODEL,
  85. prompt: '',
  86. }),
  87. });
  88. if (response.status === 401 || response.status === 403) {
  89. const text = await response.text();
  90. return {
  91. success: false,
  92. message: `Grok Video auth failed (${response.status}): ${text}`,
  93. };
  94. }
  95. return { success: true, message: 'Connected to Grok Video' };
  96. } catch (err) {
  97. return { success: false, message: `Grok Video connectivity error: ${err}` };
  98. }
  99. }
  100. // ---------------------------------------------------------------------------
  101. // Submit
  102. // ---------------------------------------------------------------------------
  103. async function submitVideoGeneration(
  104. baseUrl: string,
  105. apiKey: string,
  106. model: string,
  107. options: VideoGenerationOptions,
  108. ): Promise<string> {
  109. const body: Record<string, unknown> = {
  110. model,
  111. prompt: options.prompt,
  112. };
  113. if (options.duration) body.duration = options.duration;
  114. const response = await fetch(`${baseUrl}/videos/generations`, {
  115. method: 'POST',
  116. headers: apiHeaders(apiKey),
  117. body: JSON.stringify(body),
  118. });
  119. if (!response.ok) {
  120. const text = await response.text();
  121. throw new Error(`Grok video submit failed (${response.status}): ${text}`);
  122. }
  123. const data = (await response.json()) as GrokVideoSubmitResponse;
  124. if (!data.request_id) {
  125. throw new Error('Grok video returned empty request_id');
  126. }
  127. return data.request_id;
  128. }
  129. // ---------------------------------------------------------------------------
  130. // Poll
  131. // ---------------------------------------------------------------------------
  132. async function pollVideoStatus(
  133. baseUrl: string,
  134. apiKey: string,
  135. requestId: string,
  136. ): Promise<GrokVideoPollResponse> {
  137. const response = await fetch(`${baseUrl}/videos/${requestId}`, {
  138. method: 'GET',
  139. headers: apiHeaders(apiKey),
  140. });
  141. if (!response.ok) {
  142. const text = await response.text();
  143. throw new Error(`Grok video poll failed (${response.status}): ${text}`);
  144. }
  145. return response.json() as Promise<GrokVideoPollResponse>;
  146. }
  147. // ---------------------------------------------------------------------------
  148. // Public entry point
  149. // ---------------------------------------------------------------------------
  150. export async function generateWithGrokVideo(
  151. config: VideoGenerationConfig,
  152. options: VideoGenerationOptions,
  153. ): Promise<VideoGenerationResult> {
  154. const model = config.model || DEFAULT_MODEL;
  155. const baseUrl = config.baseUrl || DEFAULT_BASE_URL;
  156. // 1. Submit
  157. const requestId = await submitVideoGeneration(baseUrl, config.apiKey, model, options);
  158. // 2. Poll until done
  159. for (let attempt = 0; attempt < MAX_POLL_ATTEMPTS; attempt++) {
  160. await delay(POLL_INTERVAL_MS);
  161. const result = await pollVideoStatus(baseUrl, config.apiKey, requestId);
  162. if (result.status === 'done') {
  163. if (!result.video?.url) {
  164. throw new Error('Grok video task completed but no video URL returned');
  165. }
  166. const { width, height } = getDimensions(options.aspectRatio);
  167. return {
  168. url: result.video.url,
  169. duration: result.video.duration || options.duration || 6,
  170. width,
  171. height,
  172. };
  173. }
  174. if (result.status === 'failed') {
  175. throw new Error(`Grok video generation failed: ${JSON.stringify(result)}`);
  176. }
  177. }
  178. throw new Error(
  179. `Grok video generation timed out after ${(MAX_POLL_ATTEMPTS * POLL_INTERVAL_MS) / 1000}s (request: ${requestId})`,
  180. );
  181. }