UNPKG

scrptly

Version:

Scrptly is a Video Development Kit (VDK) for programmatically generating AI-powered videos.

306 lines (267 loc) 10.4 kB
import AssetUploader from './assetUploader'; import Renderer from './renderer'; import type {RenderOptions} from './renderer'; import { Listr, SilentRenderer } from 'listr2'; import type { Time, Id, Easing, Action, AddLayerOptions } from './types'; export type { Time, Id, Easing, Action, AddLayerOptions }; 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; }; interface RenderCtx { 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 = {}; 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', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${scriptlySettings.apiKey}`, ...(options?.headers || {}), }, ...options }); 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; } }