UNPKG

@pictory/pictory-mcp-server

Version:

MCP server for Pictory AI video creation platform - now available as a Desktop Extension

224 lines (223 loc) 8.62 kB
export class PictoryAPI { baseUrl; userId; clientId; clientSecret; token; constructor() { this.baseUrl = process.env.PICTORY_API_BASE_URL || "https://api.pictory.ai/pictoryapis/v1"; this.userId = process.env.PICTORY_USER_ID || ""; this.clientId = process.env.PICTORY_CLIENT_ID || ""; this.clientSecret = process.env.PICTORY_CLIENT_SECRET || ""; this.token = { access_token: null, expires_at: null }; if (!this.userId || !this.clientId || !this.clientSecret) { throw new Error("Missing required environment variables: PICTORY_USER_ID, PICTORY_CLIENT_ID, PICTORY_CLIENT_SECRET"); } } getHeaders(token) { return { "Authorization": `Bearer ${token}`, "X-Pictory-User-Id": this.userId, "Content-Type": "application/json" }; } async getAccessToken() { // Check if token is still valid if (this.token.access_token && this.token.expires_at && Date.now() / 1000 < this.token.expires_at) { return this.token.access_token; } try { const response = await fetch(`${this.baseUrl}/oauth2/token`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ client_id: this.clientId, client_secret: this.clientSecret }) }); if (!response.ok) { throw new Error(`Failed to get access token: ${response.status} ${response.statusText}`); } const data = await response.json(); this.token.access_token = data.access_token; this.token.expires_at = Date.now() / 1000 + data.expires_in - 300; // 5 minutes buffer if (!this.token.access_token) { throw new Error('Failed to get access token from API response'); } return this.token.access_token; } catch (error) { throw new Error(`Failed to authenticate with Pictory API: ${error}`); } } prepareStoryboardScenes(script) { console.error('Starting prepareStoryboardScenes with:', { script }); const finalScenes = []; for (const sceneText of script) { finalScenes.push({ text: sceneText, voiceOver: true, subtitle: true }); } return finalScenes; } async createStoryboard(script, title = '', voice = 'Amanda', dimensions = [1920, 1080]) { if (!title) { title = script[0]?.substring(0, 100) || 'Untitled Video'; } const scenes = this.prepareStoryboardScenes(script); const token = await this.getAccessToken(); try { const response = await fetch(`${this.baseUrl}/video/storyboard`, { method: 'POST', headers: this.getHeaders(token), body: JSON.stringify({ videoName: title, videoWidth: dimensions[0], videoHeight: dimensions[1], language: 'en', audio: { autoBackgroundMusic: true, backGroundMusicVolume: 0.1, aiVoiceOvers: [ { speaker: voice } ] }, scenes: scenes }) }); if (!response.ok) { throw new Error(`Failed to create storyboard: ${response.status} ${response.statusText}`); } const data = await response.json(); return data.data.job_id; } catch (error) { throw new Error(`Failed to create storyboard: ${error}`); } } async pollJobStatus(jobId, progressCallback) { const url = `${this.baseUrl}/jobs/${jobId}`; const maxAttempts = 60; let attempts = 0; while (attempts < maxAttempts) { try { const token = await this.getAccessToken(); const response = await fetch(url, { headers: this.getHeaders(token) }); if (!response.ok) { throw new Error(`Failed to poll job status: ${response.status} ${response.statusText}`); } const responseData = await response.json(); const data = responseData.data; // For storyboard jobs, check for preview URL if (data.preview) { return { ...data, preview: data.preview }; } // For render jobs, check for completed status and video URL if (data.status === 'completed') { return { ...data, output_url: data.videoURL || data.shareVideoURL }; } if (data.status === 'failed') { throw new Error(`Job failed: ${data.error || 'Unknown error'}`); } if (progressCallback) { progressCallback(jobId, data.status); } // Wait 5 seconds before next poll await new Promise(resolve => setTimeout(resolve, 5000)); attempts++; } catch (error) { throw new Error(`Failed to poll job status: ${error}`); } } throw new Error('Job timed out'); } async renderVideo(storyboardJobId) { const token = await this.getAccessToken(); try { const response = await fetch(`${this.baseUrl}/video/render/${storyboardJobId}`, { method: 'PUT', headers: this.getHeaders(token), body: JSON.stringify({ format: 'mp4', quality: 'high' }) }); if (!response.ok) { throw new Error(`Failed to render video: ${response.status} ${response.statusText}`); } const data = await response.json(); return data.data.job_id; } catch (error) { throw new Error(`Failed to render video: ${error}`); } } async getAllVideoTemplates() { const token = await this.getAccessToken(); try { const response = await fetch(`${this.baseUrl}/templates`, { headers: this.getHeaders(token) }); if (!response.ok) { throw new Error(`Failed to get video templates: ${response.status} ${response.statusText}`); } const data = await response.json(); return data.items; } catch (error) { throw new Error(`Failed to get video templates: ${error}`); } } async getVideoTemplateById(templateId) { const token = await this.getAccessToken(); try { const response = await fetch(`${this.baseUrl}/templates/${templateId}`, { headers: this.getHeaders(token) }); if (!response.ok) { throw new Error(`Failed to get video template: ${response.status} ${response.statusText}`); } const data = await response.json(); return data; } catch (error) { throw new Error(`Failed to get video template: ${error}`); } } async createStoryboardFromTemplate(templateId, templateVariables) { const token = await this.getAccessToken(); try { const response = await fetch(`${this.baseUrl}/video/from-template`, { method: 'POST', headers: this.getHeaders(token), body: JSON.stringify({ templateId: templateId, variables: templateVariables }) }); if (!response.ok) { throw new Error(`Failed to create storyboard from template: ${response.status} ${response.statusText}`); } const data = await response.json(); return data.data.jobId; } catch (error) { throw new Error(`Failed to create storyboard from template: ${error}`); } } }