Skip to content

Instantly share code, notes, and snippets.

@nxnarbais
Last active July 7, 2025 12:34
Show Gist options
  • Select an option

  • Save nxnarbais/3ad3ebc4ed67bb3f4b0e435b002ae9c4 to your computer and use it in GitHub Desktop.

Select an option

Save nxnarbais/3ad3ebc4ed67bb3f4b0e435b002ae9c4 to your computer and use it in GitHub Desktop.
Creat video at scale with Synthesia API
/**
* Constants and Configuration
*/
const SYNTHESIA_API_KEY = "";
const SHEET_OUTPUT_VIDEO = 'output';
const SYNTHESIA_API_URL = 'https://api.synthesia.io/v2/videos/fromTemplate';
const COLUMNS_TO_EXCLUDE = ["_to_json", "template_id", "title", "description", "section", "lineIndex", "cta_label", "cta_url"];
// Logging configuration
const LOG_LEVELS = {
DEBUG: 'DEBUG',
INFO: 'INFO',
WARN: 'WARN',
ERROR: 'ERROR'
};
function log(level, message, data = null) {
const timestamp = new Date().toISOString();
const logMessage = `[${timestamp}] [${level}] ${message}`;
if (data) {
Logger.log(`${logMessage}\nData: ${JSON.stringify(data, null, 2)}`);
} else {
Logger.log(logMessage);
}
}
/**
* Menu and High Level Functions
*/
function onOpen() {
try {
var ui = SpreadsheetApp.getUi();
ui.createMenu('Synthesia')
.addItem('Generate videos', 'generateVideos')
.addToUi();
} catch (error) {
Logger.log(`Error in onOpen: ${error.message}`);
throw error;
}
}
function generateVideos() {
try {
log(LOG_LEVELS.INFO, 'Starting video generation process');
// Validate API key
if (!getSynthesiaAPIKey()) {
log(LOG_LEVELS.ERROR, 'Synthesia API key is not set');
throw new Error('Synthesia API key is not set');
}
const sheet = SpreadsheetApp.getActiveSheet();
if (!sheet) {
log(LOG_LEVELS.ERROR, 'No active sheet found');
throw new Error('No active sheet found');
}
log(LOG_LEVELS.INFO, 'Reading table values from sheet', { sheetName: sheet.getName() });
const tableValues = getTableValues(sheet);
log(LOG_LEVELS.DEBUG, 'Table values retrieved', { rowCount: tableValues.length });
const parsedRows = parseRows(tableValues);
log(LOG_LEVELS.INFO, 'Parsed rows', {
totalRows: parsedRows.length,
firstRowKeys: Object.keys(parsedRows[0] || {})
});
if (!parsedRows || parsedRows.length === 0) {
log(LOG_LEVELS.WARN, 'No valid rows found to process');
SpreadsheetApp.getUi().alert('No valid rows found to process');
return;
}
let successCount = 0;
let errorCount = 0;
parsedRows.forEach((row, index) => {
try {
log(LOG_LEVELS.INFO, `Processing row ${index + 1}/${parsedRows.length}`, {
templateId: row.template_id,
title: row.title
});
const response = generateVideoFromTemplate(row);
const responseCode = response.getResponseCode();
log(LOG_LEVELS.DEBUG, `API Response for row ${index + 1}`, {
responseCode,
responseText: response.getContentText()
});
if (responseCode !== 201) {
log(LOG_LEVELS.ERROR, `Error generating video for row ${index + 1}`, {
responseCode,
responseText: response.getContentText()
});
errorCount++;
return;
}
const responseBodyJSON = JSON.parse(response.getContentText());
if (!responseBodyJSON.id) {
log(LOG_LEVELS.ERROR, `No video ID in response for row ${index + 1}`, {
responseBody: responseBodyJSON
});
errorCount++;
return;
}
row.videoID = responseBodyJSON.id;
row.publicVideoLink = `https://share.synthesia.io/${responseBodyJSON.id}`;
log(LOG_LEVELS.INFO, `Successfully generated video for row ${index + 1}`, {
videoId: responseBodyJSON.id,
publicLink: row.publicVideoLink
});
successCount++;
} catch (error) {
log(LOG_LEVELS.ERROR, `Error processing row ${index + 1}`, {
error: error.message,
rowData: row
});
errorCount++;
}
});
log(LOG_LEVELS.INFO, 'Creating output sheet', {
sheetName: SHEET_OUTPUT_VIDEO,
successCount,
errorCount
});
const outputSheet = getOrCreateSheet(SHEET_OUTPUT_VIDEO, 10, 4);
if (!outputSheet) {
log(LOG_LEVELS.ERROR, 'Failed to create output sheet');
throw new Error('Failed to create output sheet');
}
const titles = Object.keys(parsedRows[0]);
const rows = parsedRows.map(parsedRow =>
titles.map(title => parsedRow[title])
);
log(LOG_LEVELS.DEBUG, 'Preparing to fill output sheet', {
rowCount: rows.length,
columnCount: titles.length
});
fillTable(outputSheet, rows, titles);
const ui = SpreadsheetApp.getUi();
const message = `Video generation complete:\n${successCount} videos created successfully\n${errorCount} videos failed`;
log(LOG_LEVELS.INFO, message);
ui.alert(message);
} catch (error) {
log(LOG_LEVELS.ERROR, 'Error in generateVideos', {
error: error.message,
stack: error.stack
});
SpreadsheetApp.getUi().alert(`Error generating videos: ${error.message}`);
}
}
/**
* Synthesia API Functions
*/
function getSynthesiaAPIKey() {
if (!SYNTHESIA_API_KEY) {
Logger.log('Warning: Synthesia API key is not set');
return null;
}
return SYNTHESIA_API_KEY;
}
function getCallbackId(parsedRow) {
if (!parsedRow || !parsedRow.template_id) {
throw new Error('Invalid row data for callback ID');
}
const callbackId = { template_id: parsedRow.template_id };
Object.keys(parsedRow).forEach(key => {
if (!COLUMNS_TO_EXCLUDE.includes(key) && key.startsWith("_")) {
callbackId[key] = parsedRow[key];
}
});
return callbackId;
}
function getTemplateData(parsedRow) {
if (!parsedRow) {
throw new Error('Invalid row data for template data');
}
const templateData = {};
Object.keys(parsedRow).forEach(key => {
if (!COLUMNS_TO_EXCLUDE.includes(key) && !key.startsWith("_")) {
templateData[key] = parsedRow[key];
}
});
return templateData;
}
function getCtaSettings(parsedRow) {
if (!parsedRow) {
return null;
}
if (parsedRow.cta_label && parsedRow.cta_url) {
return {
label: parsedRow.cta_label,
url: parsedRow.cta_url
};
}
return null;
}
function generateVideoFromTemplate(parsedRow) {
try {
log(LOG_LEVELS.DEBUG, 'Generating video from template', {
templateId: parsedRow.template_id,
title: parsedRow.title
});
const request = generateRequest(parsedRow);
log(LOG_LEVELS.DEBUG, 'Generated API request', {
url: request.url,
method: request.method,
payload: JSON.parse(request.payload)
});
const responses = UrlFetchApp.fetchAll([request]);
const response = responses[0];
log(LOG_LEVELS.DEBUG, 'Received API response', {
responseCode: response.getResponseCode(),
responseText: response.getContentText()
});
return response;
} catch (error) {
log(LOG_LEVELS.ERROR, 'Error in generateVideoFromTemplate', {
error: error.message,
templateId: parsedRow.template_id
});
throw error;
}
}
function generateRequest(parsedRow) {
if (!parsedRow || !parsedRow.template_id) {
log(LOG_LEVELS.ERROR, 'Invalid row data for video generation', {
row: parsedRow
});
throw new Error('Invalid row data for video generation');
}
try {
const { template_id, title, description } = parsedRow;
const templateData = getTemplateData(parsedRow);
const callbackId = getCallbackId(parsedRow);
log(LOG_LEVELS.DEBUG, 'Preparing request data', {
templateId: template_id,
title,
templateData,
callbackId
});
const jsonData = {
test: "false",
templateId: template_id,
templateData: templateData,
title: title,
description: description || "",
visibility: "public",
callbackId: JSON.stringify(callbackId)
};
const ctaSettings = getCtaSettings(parsedRow);
if (ctaSettings) {
jsonData.ctaSettings = ctaSettings;
log(LOG_LEVELS.DEBUG, 'Added CTA settings to request', { ctaSettings });
}
return {
url: SYNTHESIA_API_URL,
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': getSynthesiaAPIKey()
},
payload: JSON.stringify(jsonData)
};
} catch (error) {
log(LOG_LEVELS.ERROR, 'Error in generateRequest', {
error: error.message,
templateId: parsedRow.template_id
});
throw error;
}
}
/**
* Sheet Manipulation Functions
*/
function getOrCreateSheet(sheetName, desiredRows, desiredColumns) {
if (!sheetName) {
throw new Error('Sheet name is required');
}
try {
const spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
if (!spreadsheet) {
throw new Error('No active spreadsheet found');
}
const sheets = spreadsheet.getSheets();
const sheetExists = sheets.some(sheet => sheet.getName() === sheetName);
if (!sheetExists) {
const newSheet = spreadsheet.insertSheet(sheetName);
if (desiredRows) {
newSheet.deleteRows(desiredRows + 1, newSheet.getMaxRows() - desiredRows);
}
if (desiredColumns) {
newSheet.deleteColumns(desiredColumns + 1, newSheet.getMaxColumns() - desiredColumns);
}
}
return spreadsheet.getSheetByName(sheetName);
} catch (error) {
Logger.log(`Error in getOrCreateSheet: ${error.message}`);
throw error;
}
}
function fillTable(sheet, rows, titles) {
if (!sheet || !rows || rows.length === 0) {
throw new Error('Invalid parameters for fillTable');
}
try {
if (titles && titles.length > 0) {
sheet.getRange(1, 1, 1, titles.length).setValues([titles]);
}
sheet.getRange(2, 1, rows.length, rows[0].length).setValues(rows);
} catch (error) {
Logger.log(`Error in fillTable: ${error.message}`);
throw error;
}
}
function getTableValues(sheet) {
if (!sheet) {
throw new Error('No sheet provided');
}
return sheet.getDataRange().getValues();
}
function isRowEmpty(row) {
if (!row || !Array.isArray(row)) {
return true;
}
return row.every(cell => cell === "");
}
function isRowSection(row) {
// Implement section detection logic if needed
return false;
}
function parseRow(row, attributeNames, section, lineIndex) {
if (!row || !attributeNames) {
throw new Error('Invalid parameters for parseRow');
}
try {
const parsedRow = {};
row.forEach((cell, index) => {
parsedRow[attributeNames[index]] = cell;
});
parsedRow.section = section;
parsedRow.lineIndex = lineIndex;
return parsedRow;
} catch (error) {
Logger.log(`Error in parseRow: ${error.message}`);
throw error;
}
}
function parseRows(rows) {
if (!rows || rows.length < 2) {
throw new Error('Invalid rows data');
}
try {
const attributeNames = rows[0];
const parsedRows = [];
let section = "";
for (let i = 1; i < rows.length; i++) {
const row = rows[i];
if (isRowEmpty(row)) {
continue;
}
if (isRowSection(row)) {
section = row[0];
continue;
}
parsedRows.push(parseRow(row, attributeNames, section, i));
}
return parsedRows;
} catch (error) {
Logger.log(`Error in parseRows: ${error.message}`);
throw error;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment