UNPKG

@tsavo/creatify-api-ts

Version:

TypeScript client for the Creatify AI API

354 lines (312 loc) 10.9 kB
import { Creatify } from '../index'; import { AvatarApi } from '../api'; import { CreatifyApiOptions, Avatar, TextToSpeech, AspectRatio } from '../types'; import { apiClientFactory } from '../client-factory'; import { ICreatifyApiClientFactory } from '../types/api-client'; /** * A utility class to simplify avatar video creation */ export class VideoCreator { private creatify: Creatify; private avatarApi: AvatarApi; private avatarCache: Record<string, Avatar.AvatarInfo> = {}; private voiceCache: Record<string, TextToSpeech.Voice> = {}; private avatarsLoaded = false; private voicesLoaded = false; /** * Create a new VideoCreator instance * @param apiIdOrOptions Your Creatify API ID or options object * @param apiKey Your Creatify API Key (optional if options object is used) * @param clientFactory Optional factory for creating API clients (useful for testing) */ constructor( apiIdOrOptions: string | CreatifyApiOptions, apiKey?: string, clientFactory: ICreatifyApiClientFactory = apiClientFactory ) { const options: CreatifyApiOptions = typeof apiIdOrOptions === 'string' ? { apiId: apiIdOrOptions, apiKey: apiKey || '' } : apiIdOrOptions; this.creatify = new Creatify(options); // Create a separate AvatarApi that uses the provided clientFactory this.avatarApi = new AvatarApi(options, clientFactory); } /** * Preload avatars and voices for faster video creation */ async preload() { await Promise.all([this.loadAvatars(), this.loadVoices()]); return { avatars: Object.keys(this.avatarCache).length, voices: Object.keys(this.voiceCache).length, }; } /** * Load and cache all available avatars */ async loadAvatars() { if (this.avatarsLoaded) return Object.values(this.avatarCache); const avatars = await this.avatarApi.getAvatars(); for (const avatar of avatars) { this.avatarCache[avatar.id] = avatar; } this.avatarsLoaded = true; return avatars; } /** * Load and cache all available voices */ async loadVoices() { if (this.voicesLoaded) return Object.values(this.voiceCache); const voices = await this.avatarApi.getVoices(); for (const voice of voices) { this.voiceCache[voice.voice_id] = voice; } this.voicesLoaded = true; return voices; } /** * Find an avatar by name (partial match) * @param name Avatar name to search for */ async findAvatarByName(name: string) { if (!this.avatarsLoaded) { await this.loadAvatars(); } const normalizedName = name.toLowerCase(); return Object.values(this.avatarCache).find(avatar => avatar.name?.toLowerCase().includes(normalizedName) ); } /** * Find a voice by name (partial match) * @param name Voice name to search for */ async findVoiceByName(name: string) { if (!this.voicesLoaded) { await this.loadVoices(); } const normalizedName = name.toLowerCase(); return Object.values(this.voiceCache).find(voice => voice.name?.toLowerCase().includes(normalizedName) ); } /** * Create a video with a single avatar speaking * @param options Configuration options for the video * @returns The URL of the generated video */ async createVideo(options: { avatarName?: string; avatarId?: string; voiceName?: string; voiceId?: string; script: string; aspectRatio?: string; pollInterval?: number; maxPollingAttempts?: number; }) { try { // Default values const pollInterval = options.pollInterval || 5000; const maxPollingAttempts = options.maxPollingAttempts || 30; const aspectRatio = options.aspectRatio || '16:9'; // Resolve avatar ID let avatarId = options.avatarId; if (!avatarId && options.avatarName) { const avatar = await this.findAvatarByName(options.avatarName); if (!avatar) { throw new Error(`Avatar with name "${options.avatarName}" not found`); } avatarId = avatar.avatar_id; } if (!avatarId) { // If no avatar specified, use the first available one const avatars = await this.loadAvatars(); if (avatars.length === 0) { throw new Error('No avatars available. Please check your API credentials.'); } avatarId = avatars[0].avatar_id; } // Resolve voice ID let voiceId = options.voiceId; if (!voiceId && options.voiceName) { const voice = await this.findVoiceByName(options.voiceName); if (!voice) { throw new Error(`Voice with name "${options.voiceName}" not found`); } voiceId = voice.voice_id; } // Create the lipsync video const lipsyncResponse = await this.avatarApi.createLipsync({ text: options.script, creator: avatarId || '', aspect_ratio: aspectRatio as AspectRatio, voice_id: voiceId, }); // Ensure we have a valid response ID if (!lipsyncResponse || !lipsyncResponse.id) { throw new Error('Failed to create lipsync video: Invalid response from API'); } // Poll for completion let lipsyncResult = await this.avatarApi.getLipsync(lipsyncResponse.id); let attempts = 0; while ( lipsyncResult.status !== 'done' && lipsyncResult.status !== 'error' && attempts < maxPollingAttempts ) { // Wait between checks await new Promise(resolve => setTimeout(resolve, pollInterval)); // Check status again lipsyncResult = await this.avatarApi.getLipsync(lipsyncResponse.id); attempts++; } // Check final result if (lipsyncResult.status === 'done' && lipsyncResult.output) { return { id: lipsyncResponse.id, status: 'done', url: lipsyncResult.output, }; } else { throw new Error( lipsyncResult.error_message || `Video generation failed or timed out. Status: ${lipsyncResult.status}` ); } } catch (error: unknown) { // Ensure the error is properly thrown with the original message if (error instanceof Error) { throw error; } else if (typeof error === 'object' && error !== null && 'message' in error && typeof error.message === 'string') { throw new Error(error.message); } else { throw new Error('An error occurred with the API request'); } } } /** * Create a video with multiple avatars having a conversation * @param options Configuration options for the conversation * @returns The URL of the generated video */ async createConversation(options: { conversation: Array<{ avatarName?: string; avatarId?: string; voiceName?: string; voiceId?: string; text: string; }>; backgroundUrl?: string; aspectRatio?: string; pollInterval?: number; maxPollingAttempts?: number; }) { try { const conversations = options.conversation; // Default values const pollInterval = options.pollInterval || 5000; const maxPollingAttempts = options.maxPollingAttempts || 30; const aspectRatio = options.aspectRatio || '16:9'; const backgroundUrl = options.backgroundUrl || 'https://video.creatify.ai/bg.jpg'; // Prepare video inputs const videoInputs = []; for (const conversation of conversations) { // Resolve avatar ID let avatarId = conversation.avatarId; if (!avatarId && conversation.avatarName) { const avatar = await this.findAvatarByName(conversation.avatarName); if (!avatar) { throw new Error(`Avatar with name "${conversation.avatarName}" not found`); } avatarId = avatar.avatar_id; } if (!avatarId) { // If no avatar specified, use the first available one const avatars = await this.loadAvatars(); if (avatars.length === 0) { throw new Error('No avatars available. Please check your API credentials.'); } avatarId = avatars[0].avatar_id; } // Resolve voice ID let voiceId = conversation.voiceId; if (!voiceId && conversation.voiceName) { const voice = await this.findVoiceByName(conversation.voiceName); if (!voice) { throw new Error(`Voice with name "${conversation.voiceName}" not found`); } voiceId = voice.voice_id; } videoInputs.push({ character: { type: 'avatar' as const, avatar_id: avatarId || '', avatar_style: 'normal', offset: { x: -0.23, y: 0.35 }, }, voice: { type: 'text' as const, input_text: conversation.text, voice_id: voiceId || '', }, background: { type: 'image' as const, url: backgroundUrl, }, caption_setting: { style: 'normal-black', offset: { x: 0, y: 0.45 }, }, }); } // Create the multi-avatar lipsync video const multiAvatarResponse = await this.avatarApi.createMultiAvatarLipsync({ video_inputs: videoInputs, aspect_ratio: aspectRatio as AspectRatio, }); // Ensure we have a valid response ID if (!multiAvatarResponse || !multiAvatarResponse.id) { throw new Error('Failed to create multi-avatar lipsync video: Invalid response from API'); } // Poll for completion let result = await this.avatarApi.getLipsync(multiAvatarResponse.id); let attempts = 0; while ( result.status !== 'done' && result.status !== 'error' && attempts < maxPollingAttempts ) { // Wait between checks await new Promise(resolve => setTimeout(resolve, pollInterval)); // Check status again result = await this.avatarApi.getLipsync(multiAvatarResponse.id); attempts++; } // Check final result if (result.status === 'done' && result.output) { return { id: multiAvatarResponse.id, status: 'done', url: result.output, }; } else { throw new Error( result.error_message || `Video generation failed or timed out. Status: ${result.status}` ); } } catch (error: unknown) { // Ensure the error is properly thrown with the original message if (error instanceof Error) { throw error; } else if (typeof error === 'object' && error !== null && 'message' in error && typeof error.message === 'string') { throw new Error(error.message); } else { throw new Error('An error occurred with the API request'); } } } }