UNPKG

@tanstack/ai

Version:

Core TanStack AI library - Open source AI SDK

497 lines (454 loc) 14.4 kB
/** * Video Activity (Experimental) * * Generates videos from text prompts using a jobs/polling architecture. * This is a self-contained module with implementation, types, and JSDoc. * * @experimental Video generation is an experimental feature and may change. */ import { aiEventClient } from '@tanstack/ai-event-client' import type { VideoAdapter } from './adapter' import type { StreamChunk, VideoJobResult, VideoStatusResult, VideoUrlResult, } from '../../types' // =========================== // Activity Kind // =========================== /** The adapter kind this activity handles */ export const kind = 'video' as const // =========================== // Type Extraction Helpers // =========================== /** * Extract provider options from a VideoAdapter via ~types. */ export type VideoProviderOptions<TAdapter> = TAdapter extends VideoAdapter<any, any, any, any> ? TAdapter['~types']['providerOptions'] : object /** * Extract the size type for a VideoAdapter's model via ~types. */ export type VideoSizeForAdapter<TAdapter> = TAdapter extends VideoAdapter<infer TModel, any, any, infer TSizeMap> ? TModel extends keyof TSizeMap ? TSizeMap[TModel] : string : string // =========================== // Activity Options Types function createId(prefix: string): string { return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 9)}` } // =========================== /** * Base options shared by all video activity operations. * The model is extracted from the adapter's model property. */ interface VideoActivityBaseOptions< TAdapter extends VideoAdapter<string, any, any, any>, > { /** The video adapter to use (must be created with a model) */ adapter: TAdapter & { kind: typeof kind } } /** * Options for creating a new video generation job. * The model is extracted from the adapter's model property. * * @template TAdapter - The video adapter type * @template TStream - Whether to stream the output * * @experimental Video generation is an experimental feature and may change. */ export type VideoCreateOptions< TAdapter extends VideoAdapter<string, any, any, any>, TStream extends boolean = false, > = VideoActivityBaseOptions<TAdapter> & { /** Request type - create a new job (default if not specified) */ request?: 'create' /** Text description of the desired video */ prompt: string /** Video size — format depends on the provider (e.g., "16:9", "1280x720") */ size?: VideoSizeForAdapter<TAdapter> /** Video duration in seconds */ duration?: number /** * Whether to stream the video generation lifecycle. * When true, returns an AsyncIterable<StreamChunk> that handles the full * job lifecycle: create job, poll for status, yield updates, and yield final result. * When false or not provided, returns a Promise<VideoJobResult>. * * @default false */ stream?: TStream /** Polling interval in milliseconds (stream mode only). @default 2000 */ pollingInterval?: number /** Maximum time to wait before timing out in milliseconds (stream mode only). @default 600000 */ maxDuration?: number /** Custom run ID (stream mode only) */ runId?: string } & ({} extends VideoProviderOptions<TAdapter> ? { /** Provider-specific options for video generation */ modelOptions?: VideoProviderOptions<TAdapter> } : { /** Provider-specific options for video generation */ modelOptions: VideoProviderOptions<TAdapter> }) /** * Options for polling the status of a video generation job. * * @experimental Video generation is an experimental feature and may change. */ export interface VideoStatusOptions< TAdapter extends VideoAdapter<string, any, any, any>, > extends VideoActivityBaseOptions<TAdapter> { /** Request type - get job status */ request: 'status' /** The job ID to check status for */ jobId: string } /** * Options for getting the URL of a completed video. * * @experimental Video generation is an experimental feature and may change. */ export interface VideoUrlOptions< TAdapter extends VideoAdapter<string, any, any, any>, > extends VideoActivityBaseOptions<TAdapter> { /** Request type - get video URL */ request: 'url' /** The job ID to get URL for */ jobId: string } /** * Union type for all video activity options. * Discriminated by the `request` field. * * @experimental Video generation is an experimental feature and may change. */ export type VideoActivityOptions< TAdapter extends VideoAdapter<string, any, any, any>, TRequest extends 'create' | 'status' | 'url' = 'create', TStream extends boolean = false, > = TRequest extends 'status' ? VideoStatusOptions<TAdapter> : TRequest extends 'url' ? VideoUrlOptions<TAdapter> : VideoCreateOptions<TAdapter, TStream> // =========================== // Activity Result Types // =========================== /** * Result type for the video activity, based on request type and streaming. * - If stream is true (create request): AsyncIterable<StreamChunk> * - Otherwise: Promise<VideoJobResult | VideoStatusResult | VideoUrlResult> * * @experimental Video generation is an experimental feature and may change. */ export type VideoActivityResult< TRequest extends 'create' | 'status' | 'url' = 'create', TStream extends boolean = false, > = TRequest extends 'status' ? Promise<VideoStatusResult> : TRequest extends 'url' ? Promise<VideoUrlResult> : TStream extends true ? AsyncIterable<StreamChunk> : Promise<VideoJobResult> // =========================== // Activity Implementation // =========================== /** * Generate video - creates a video generation job from a text prompt. * * Uses AI video generation models to create videos based on natural language descriptions. * Unlike image generation, video generation is asynchronous and requires polling for completion. * * When `stream: true` is passed, handles the full job lifecycle automatically: * create job → poll for status → stream updates → yield final result. * * @experimental Video generation is an experimental feature and may change. * * @example Create a video generation job * ```ts * import { generateVideo } from '@tanstack/ai' * import { openaiVideo } from '@tanstack/ai-openai' * * // Start a video generation job * const { jobId } = await generateVideo({ * adapter: openaiVideo('sora-2'), * prompt: 'A cat chasing a dog in a sunny park' * }) * * console.log('Job started:', jobId) * ``` * * @example Stream the full video generation lifecycle * ```ts * import { generateVideo, toServerSentEventsResponse } from '@tanstack/ai' * import { openaiVideo } from '@tanstack/ai-openai' * * const stream = generateVideo({ * adapter: openaiVideo('sora-2'), * prompt: 'A cat chasing a dog in a sunny park', * stream: true, * pollingInterval: 3000, * }) * * return toServerSentEventsResponse(stream) * ``` */ export function generateVideo< TAdapter extends VideoAdapter<string, any, any, any>, TStream extends boolean = false, >( options: VideoCreateOptions<TAdapter, TStream>, ): VideoActivityResult<'create', TStream> { if (options.stream) { return runStreamingVideoGeneration( options as VideoCreateOptions<TAdapter, true>, ) as VideoActivityResult<'create', TStream> } return runCreateVideoJob(options) as VideoActivityResult<'create', TStream> } /** * Internal implementation of non-streaming video job creation. */ async function runCreateVideoJob< TAdapter extends VideoAdapter<string, any, any, any>, >(options: VideoCreateOptions<TAdapter, boolean>): Promise<VideoJobResult> { const { adapter, prompt, size, duration, modelOptions } = options const model = adapter.model return adapter.createVideoJob({ model, prompt, size, duration, modelOptions, }) } function sleep(ms: number): Promise<void> { return new Promise((resolve) => setTimeout(resolve, ms)) } /** * Internal streaming implementation for video generation. * Handles the full job lifecycle: create job → poll for status → stream updates → yield final result. */ async function* runStreamingVideoGeneration< TAdapter extends VideoAdapter<string, any, any, any>, >(options: VideoCreateOptions<TAdapter, true>): AsyncIterable<StreamChunk> { const { adapter, prompt, size, duration, modelOptions } = options const model = adapter.model const runId = options.runId ?? createId('run') const pollingInterval = options.pollingInterval ?? 2000 const maxDuration = options.maxDuration ?? 600_000 yield { type: 'RUN_STARTED', runId, timestamp: Date.now(), } try { // Create the video generation job const jobResult = await adapter.createVideoJob({ model, prompt, size, duration, modelOptions, }) yield { type: 'CUSTOM', name: 'video:job:created', value: { jobId: jobResult.jobId }, timestamp: Date.now(), } // Poll for completion const startTime = Date.now() while (Date.now() - startTime < maxDuration) { await sleep(pollingInterval) const statusResult = await adapter.getVideoStatus(jobResult.jobId) yield { type: 'CUSTOM', name: 'video:status', value: { jobId: jobResult.jobId, status: statusResult.status, progress: statusResult.progress, error: statusResult.error, }, timestamp: Date.now(), } if (statusResult.status === 'completed') { const urlResult = await adapter.getVideoUrl(jobResult.jobId) yield { type: 'CUSTOM', name: 'generation:result', value: { jobId: jobResult.jobId, status: 'completed', url: urlResult.url, expiresAt: urlResult.expiresAt, }, timestamp: Date.now(), } yield { type: 'RUN_FINISHED', runId, finishReason: 'stop', timestamp: Date.now(), } return } if (statusResult.status === 'failed') { throw new Error(statusResult.error || 'Video generation failed') } } throw new Error('Video generation timed out') } catch (error: any) { yield { type: 'RUN_ERROR', runId, error: { message: error.message || 'Video generation failed', code: error.code, }, timestamp: Date.now(), } } } /** * Get video job status - returns the current status, progress, and URL if available. * * This function combines status checking and URL retrieval. If the job is completed, * it will automatically fetch and include the video URL. * * @experimental Video generation is an experimental feature and may change. * * @example Check job status * ```ts * import { getVideoJobStatus } from '@tanstack/ai' * import { openaiVideo } from '@tanstack/ai-openai' * * const result = await getVideoJobStatus({ * adapter: openaiVideo('sora-2'), * jobId: 'job-123' * }) * * console.log('Status:', result.status) * console.log('Progress:', result.progress) * if (result.url) { * console.log('Video URL:', result.url) * } * ``` */ export async function getVideoJobStatus< TAdapter extends VideoAdapter<string, any, any, any>, >(options: { adapter: TAdapter & { kind: typeof kind } jobId: string }): Promise<{ status: 'pending' | 'processing' | 'completed' | 'failed' progress?: number url?: string error?: string }> { const { adapter, jobId } = options const requestId = createId('video-status') const startTime = Date.now() aiEventClient.emit('video:request:started', { requestId, provider: adapter.name, model: adapter.model, requestType: 'status', jobId, timestamp: startTime, }) // Get status first const statusResult = await adapter.getVideoStatus(jobId) // If completed, also get the URL if (statusResult.status === 'completed') { try { const urlResult = await adapter.getVideoUrl(jobId) aiEventClient.emit('video:request:completed', { requestId, provider: adapter.name, model: adapter.model, requestType: 'status', jobId, status: statusResult.status, progress: statusResult.progress, url: urlResult.url, duration: Date.now() - startTime, timestamp: Date.now(), }) return { status: statusResult.status, progress: statusResult.progress, url: urlResult.url, } } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Failed to get video URL' aiEventClient.emit('video:request:completed', { requestId, provider: adapter.name, model: adapter.model, requestType: 'status', jobId, status: 'failed', progress: statusResult.progress, error: errorMessage, duration: Date.now() - startTime, timestamp: Date.now(), }) // Provider reported completed but result fetch failed — treat as failed return { status: 'failed' as const, progress: statusResult.progress, error: errorMessage, } } } aiEventClient.emit('video:request:completed', { requestId, provider: adapter.name, model: adapter.model, requestType: 'status', jobId, status: statusResult.status, progress: statusResult.progress, error: statusResult.error, duration: Date.now() - startTime, timestamp: Date.now(), }) // Return status for non-completed jobs return { status: statusResult.status, progress: statusResult.progress, error: statusResult.error, } } // =========================== // Options Factory // =========================== /** * Create typed options for the generateVideo() function without executing. */ export function createVideoOptions< TAdapter extends VideoAdapter<string, any, any, any>, TStream extends boolean = false, >( options: VideoCreateOptions<TAdapter, TStream>, ): VideoCreateOptions<TAdapter, TStream> { return options } // Re-export adapter types export type { VideoAdapter, VideoAdapterConfig, AnyVideoAdapter, } from './adapter' export { BaseVideoAdapter } from './adapter'