@tsavo/creatify-api-ts
Version:
TypeScript client for the Creatify AI API
354 lines (312 loc) • 10.9 kB
text/typescript
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');
}
}
}
}