@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
JavaScript
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}`);
}
}
}