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.

Revisions

  1. nxnarbais revised this gist Jul 7, 2025. 1 changed file with 6 additions and 2 deletions.
    8 changes: 6 additions & 2 deletions synthesia_bulk_video_generation.gs
    Original file line number Diff line number Diff line change
    @@ -1,10 +1,14 @@
    // Gist: https://gist.github.com/nxnarbais/3ad3ebc4ed67bb3f4b0e435b002ae9c4

    /**
    * 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"];
    const OUTPUT_COLUMNS_MANDATORY = ["template_id", "title", "lineIndex", "videoID", "publicVideoLink"];
    const TEST = "false";

    // Logging configuration
    const LOG_LEVELS = {
    @@ -138,7 +142,7 @@ function generateVideos() {
    throw new Error('Failed to create output sheet');
    }

    const titles = Object.keys(parsedRows[0]);
    const titles = [...new Set(Object.keys(parsedRows[0]).concat(OUTPUT_COLUMNS_MANDATORY))];
    const rows = parsedRows.map(parsedRow =>
    titles.map(title => parsedRow[title])
    );
    @@ -272,7 +276,7 @@ function generateRequest(parsedRow) {
    });

    const jsonData = {
    test: "false",
    test: TEST,
    templateId: template_id,
    templateData: templateData,
    title: title,
  2. nxnarbais created this gist May 5, 2025.
    423 changes: 423 additions & 0 deletions synthesia_bulk_video_generation.gs
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,423 @@
    /**
    * 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;
    }
    }