scrptly
Version:
Scrptly is a Video Development Kit (VDK) for programmatically generating AI-powered videos.
369 lines (323 loc) • 12.5 kB
text/typescript
import AssetUploader from './assetUploader';
import Renderer from './renderer';
import type {RenderOptions} from './renderer';
import Agent from './agent';
import type { AgentOptions } from './agent';
import { Listr, SilentRenderer } from 'listr2';
import type { Time, Id, Easing, Action, AddLayerOptions, ContextInput } from './types';
export type { Time, Id, Easing, Action, AddLayerOptions, ContextInput };
import BaseLayer from './layers/BaseLayer';
import type { BaseLayerProperties, BaseLayerSettings } from './layers/BaseLayer';
export type { BaseLayerProperties, BaseLayerSettings };
import FolderLayer from './layers/FolderLayer';
import type { FolderLayerProperties, FolderLayerSettings } from './layers/FolderLayer';
export type { FolderLayerProperties, FolderLayerSettings };
import TextLayer from './layers/TextLayer';
import type { TextLayerProperties, TextLayerSettings } from './layers/TextLayer';
export type { TextLayerProperties, TextLayerSettings };
import CaptionsLayer from './layers/CaptionsLayer';
import type { CaptionsLayerProperties, CaptionsLayerSettings } from './layers/CaptionsLayer';
export type { CaptionsLayerProperties, CaptionsLayerSettings };
import ImageLayer from './layers/ImageLayer';
import type { ImageLayerProperties, ImageLayerSettings } from './layers/ImageLayer';
export type { ImageLayerProperties, ImageLayerSettings };
import VideoLayer from './layers/VideoLayer';
import type { VideoLayerProperties, VideoLayerSettings } from './layers/VideoLayer';
export type { VideoLayerProperties, VideoLayerSettings };
import AudioLayer from './layers/AudioLayer';
import type { AudioLayerProperties, AudioLayerSettings } from './layers/AudioLayer';
export type { AudioLayerProperties, AudioLayerSettings };
import TTSLayer from './layers/TTSLayer';
import type { TTSLayerProperties, TTSLayerSettings } from './layers/TTSLayer';
export type { TTSLayerProperties, TTSLayerSettings };
import ChartLayer from './layers/ChartLayer';
import type { ChartLayerProperties, ChartLayerSettings } from './layers/ChartLayer';
export type { ChartLayerProperties, ChartLayerSettings };
export { BaseLayer, FolderLayer, TextLayer, CaptionsLayer, ImageLayer, VideoLayer, AudioLayer, TTSLayer, ChartLayer };
export {default as TextualLayer} from './layers/TextualLayer';
export type { TextualLayerProperties, TextualLayerSettings } from './layers/TextualLayer';
export {default as AuditoryLayer} from './layers/AuditoryLayer';
export type { AuditoryLayerProperties, AuditoryLayerSettings } from './layers/AuditoryLayer';
export {default as MediaLayer} from './layers/MediaLayer';
import MediaLayer from './layers/MediaLayer';
import type { MediaLayerProperties, MediaLayerSettings } from './layers/MediaLayer';
export type { MediaLayerProperties, MediaLayerSettings };
export {default as VisualLayer} from './layers/VisualLayer';
import type { VisualLayerProperties, VisualLayerSettings } from './layers/VisualLayer';
export type { VisualLayerProperties, VisualLayerSettings };
export type ProjectSettings = {
size?: { width: number; height: number };
frameRate?: number | string;
backgroundColor?: string;
defaults?: {
easing?: Easing;
fontFamily?: string;
cacheIntegrations?: boolean; // whether to cache API calls to third party integrations like TTS, AI generators, etc.
}
};
export type ScrptlySettings = {
apiKey: string | false;
apiEndpoint?: string;
};
export type AiAgentParameters = {
prompt: string;
context?: ContextInput;
approveUpTo?: number; // maximum number of tokens you are willing to approve for the AI-generated video
};
interface RenderCtx {
result?: any;
};
interface GenerateCtx {
result?: any;
};
const scriptlySettings: ScrptlySettings = {
apiKey: false,
apiEndpoint: 'https://api.scrptly.com/',
};
export default class Scrptly {
settings!: ProjectSettings;
layers: BaseLayer[] = [];
flow: Action[] = [];
private _flowPointer: Action[] = this.flow;
prepareAssetsTask: any = null;
renderVideoTask: any = null;
generateProjectTask: any = null;
renderCtx: RenderCtx = {};
generateCtx: GenerateCtx = {};
createAiProjectTask: any = null;
generateAiVideoTask: any = null;
constructor(settings: ProjectSettings = {}) {
this.settings = {
...((this.constructor as typeof Scrptly).defaultSettings),
...settings,
};
}
static get defaultSettings() : ProjectSettings {
return {
size: { width: 1920, height: 1080 },
frameRate: 30,
backgroundColor: '#00000000',
defaults: {
easing: 'easeInOut',
fontFamily: 'Noto Sans',
cacheIntegrations: true,
}
};
}
static setApiSettings(settings: ScrptlySettings) {
for (const k of Object.keys(settings) as (keyof ScrptlySettings)[]) {
scriptlySettings[k] = settings[k]as any;
}
}
// Flow control
pushAction(action: Action) {
this._flowPointer.push(action);
}
wait(time: Time) {
this.pushAction({ statement: 'wait', duration: time });
return this;
}
parallel(funcs: Array<() => Action>, settings?: any) {
let initialPointer = this._flowPointer;
let actions: Action[][] = [];
funcs.forEach(fn => {
this._flowPointer = [];
actions.push(this._flowPointer);
fn();
});
this._flowPointer = initialPointer;
this.pushAction({ statement: 'parallel', actions });
}
addLayer<T extends BaseLayer>(
LayerClass: new (parent: Scrptly, properties?: any, settings?: any) => T,
properties: Record<string, any> = {},
settings: Record<string, any> = {},
options: AddLayerOptions = {}
) {
const layer = new LayerClass(this, properties, settings);
this.layers.push(layer);
this.pushAction({ statement: 'addLayer', id: layer.id, type: (LayerClass as any).type, settings, properties, options });
return layer;
}
/*addFolder(properties?: Record<string, any>, settings?: Record<string, any>, options?: AddLayerOptions) {
return this.addLayer(FolderLayer, properties, settings, options);
}*/
addText(properties?: TextLayerProperties, settings?: TextLayerSettings, options?: AddLayerOptions) {
return this.addLayer(TextLayer, properties, settings, options);
}
addImage(properties?: ImageLayerProperties, settings?: ImageLayerSettings, options?: AddLayerOptions) {
return this.addLayer(ImageLayer, properties, settings, options);
}
addVideo(properties?: VideoLayerProperties, settings?: VideoLayerSettings, options?: AddLayerOptions) {
return this.addLayer(VideoLayer, properties, settings, options);
}
addAudio(properties?: AudioLayerProperties, settings?: AudioLayerSettings, options?: AddLayerOptions) {
return this.addLayer(AudioLayer, properties, settings, options);
}
addCaptions(properties?: CaptionsLayerProperties, settings?: CaptionsLayerSettings, options?: AddLayerOptions) {
settings
return this.addLayer(CaptionsLayer, properties, settings, options);
}
addTTS(properties?: TTSLayerProperties, settings?: TTSLayerSettings, options?: AddLayerOptions) {
return this.addLayer(TTSLayer, properties, settings, options);
}
addChart(properties?: ChartLayerProperties, settings?: ChartLayerSettings, options?: AddLayerOptions) {
return this.addLayer(ChartLayer, properties, settings, options);
}
// API calls
async apiCall(endpoint: string, options: any = {}) {
if (!scriptlySettings.apiKey) throw new Error('API key not set');
const url = `${scriptlySettings.apiEndpoint}${endpoint}`;
const response = await fetch(url, {
method: options?.method || 'GET',
...options,
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${scriptlySettings.apiKey}`,
...(options?.headers || {}),
},
});
if (!response.ok) throw new Error(`API call failed: ${response.statusText}`);
return await response.json();
}
async info() {
const response = await this.apiCall('info');
return response;
}
async prepareAssets(actions: Action[] = this.flow) {
for(let action of actions) {
if(action.statement === 'addLayer') {
let layer: any = this.layers.find(l => l.id === action.id) as any;
if(layer && layer.constructor.isAsset && 'source' in layer.settings && layer.settings.sourceType=='file') {
this.prepareAssetsTask.output = `Uploading ${layer.settings.source}...`;
let asset = new AssetUploader(this, layer.settings.source, layer.constructor.type);
let response = await asset.uploadAsset();
layer.settings.source = response.url;
layer.settings.sourceType = 'asset';
action.settings.source = response.url;
action.settings.sourceType = 'asset';
// Prepare image assets for video layers
if(action.type === 'video' && layer.settings?.image?.source && layer.settings.image.sourceType === 'file') {
this.prepareAssetsTask.output = `Uploading ${layer.settings.image.source}...`;
let asset = new AssetUploader(this, layer.settings.image.source, (layer.constructor as typeof MediaLayer).type);
let response = await asset.uploadAsset();
layer.settings.image.source = response.url;
layer.settings.image.sourceType = 'asset';
action.settings.image.source = response.url;
action.settings.image.sourceType = 'asset';
}
}
} else if(action.statement==='parallel') {
for(let subActions of action.actions) {
await this.prepareAssets(subActions);
}
}
}
return true;
}
async renderVideo(options:RenderOptions = {}) {
options = Object.assign({
verbose: true,
}, options);
this.renderCtx = {};
const tasks = new Listr([
{
title: 'Preparing assets',
task: async (ctx, task) => {
this.prepareAssetsTask = task;
await this.prepareAssets();
}
},
{
title: 'Rendering video',
task: async (ctx, task) => {
this.renderVideoTask = task;
const renderer = new Renderer(this, options, this.settings, this.flow);
ctx.result = await renderer.render();
},
rendererOptions: {
persistentOutput: true,
},
}
], {
renderer: options.verbose===false ? SilentRenderer : 'default',
ctx: this.renderCtx
});
await tasks.run();
return tasks.ctx.result;
}
async generateProject(options:RenderOptions = {}) {
options = Object.assign({
verbose: true,
}, options);
this.renderCtx = {};
const tasks = new Listr([
{
title: 'Preparing assets',
task: async (ctx, task) => {
this.prepareAssetsTask = task;
await this.prepareAssets();
}
},
{
title: 'Generating project',
task: async (ctx, task) => {
this.generateProjectTask = task;
const renderer = new Renderer(this, options, this.settings, this.flow);
ctx.result = await renderer.generateProject();
},
rendererOptions: {
persistentOutput: true,
},
}
], {
renderer: options.verbose===false ? SilentRenderer : 'default',
ctx: this.renderCtx
});
await tasks.run();
return tasks.ctx.result;
}
async generateAiVideo(parameters: AiAgentParameters, options:AgentOptions) {
options = Object.assign({
verbose: true,
}, options);
if(options.verbose && !parameters.approveUpTo)
console.warn('⚠️ The "approveUpTo" option is highly recommended. This option specifies the maximum number of tokens you are willing to approve for the AI-generated video. If the cost to generate the video exceeds this amount, the generation will be processed partially; note that your account will still be billed for the usage incurred. When unspecified, it defaults to 10,000 tokens.');
this.generateCtx = {};
const tasks = new Listr([
{
title: 'Uploading context images',
task: async (ctx, task) => {
this.prepareAssetsTask = task;
for(let item of (parameters.context || [])) {
if(!item.url.startsWith('https://') && !item.url.startsWith('http://')) { // Upload local file
this.prepareAssetsTask.output = `Uploading "${item.url}"...`;
let asset = new AssetUploader(this, item.url, 'image');
let response = await asset.uploadAsset();
item.url = response.url;
}
}
}
},
{
title: 'Generating AI Video',
task: async (ctx, task) => {
this.generateAiVideoTask = task;
const agent = new Agent(this, parameters, options);
return await agent.generateAiVideo(ctx);
},
rendererOptions: {
persistentOutput: true,
},
}
], {
renderer: options.verbose===false ? SilentRenderer : 'default',
ctx: this.generateCtx,
rendererOptions: {
collapseSubtasks: false
}
});
await tasks.run();
return tasks.ctx.result;
}
}