Download Text / Captions from Panopto

Step by step video guide

What Does This Tool Do?

Panopto video. Buttons provide download options for plain text, text with breaks, and captions files.
Using the tool to download captions and text from a Panopto video

This bookmarklet allows you to download captions and transcripts from Panopto videos in three different formats. When activated on a Panopto video page, it presents a dialogue with options to download a plain text transcript, text with preserved line breaks, or timestamped captions in standard SRT format.

How to Use

  1. Select and drag the bookmarklet link below to your bookmarks bar.
  2. (In Firefox you can also right-click on the link below and select Bookmark link).
  3. Navigate to any Panopto video on southampton.cloud.panopto.eu.
  4. Click the bookmarklet from your bookmarks bar whilst viewing the video.
  5. Choose your preferred download format from the dialogue that appears.

panoptoText

Alternative: Console Method

If you prefer not to use a bookmarklet, you can run the code directly in your browser's developer console:

  1. Navigate to a Panopto video page
  2. Open your browser's developer console:
    • Windows/Linux: Press F12 or Ctrl+Shift+I
    • Mac: Press Cmd+Option+I
  3. Click on the "Console" tab
  4. Click the "Copy to Clipboard" button below
  5. Paste the code into the console and press Enter
  6. The download dialogue will appear immediately
Show Console Code
(function() {
  // Cache document and body references
  const d = document;
  const b = d.body;
  
  // Prevent multiple dialogues from opening
  if (d.getElementById('panopto-caption-dl')) {
    return;
  }
  
  // Store the currently focused element to return focus later
  let previousFocus = d.activeElement;
  
  // Create the dialogue container
  const m = d.createElement('div');
  m.id = 'panopto-caption-dl';
  m.setAttribute('role', 'dialog');
  m.setAttribute('aria-labelledby', 'panopto-dl-title');
  m.setAttribute('aria-modal', 'true');
  
  // Inject the dialogue HTML with embedded styles
  m.innerHTML = '<style>' +
    '#panopto-caption-dl{' +
      'position:fixed;top:50%;left:50%;' +
      'transform:translate(-50%,-50%);' +
      'background:#fffff4;' +
      'border:2px solid #00131D;' +
      'padding:20px;' +
      'z-index:999999;' +
      'box-shadow:0 4px 6px rgba(0,0,0,0.3);' +
      'max-width:500px;' +
      'font-family:sans-serif' +
    '}' +
    '#panopto-caption-dl h2{' +
      'margin:0 0 15px 0;' +
      'font-size:1.25rem;' +
      'color:#00131D' +
    '}' +
    '#panopto-caption-dl button{' +
      'display:block;width:100%;margin:10px 0;padding:12px;' +
      'font-size:1rem;background:#002E3B;color:#fff;' +
      'border:none;cursor:pointer;border-radius:4px;' +
      'text-align:left' +
    '}' +
    '#panopto-caption-dl button:hover{background:#005C84}' +
    '#panopto-caption-dl button:focus,' +
    '#panopto-caption-dl .close-btn:focus{' +
      'background:#00131D;' +
      'outline:2px transparent solid;' +
      'box-shadow:0 0 0 2px white,0 0 0 4px #002e3b,0 0 4px 8px white' +
    '}' +
    '#panopto-caption-dl button:disabled{' +
      'background:#9FB1BD;color:#231F20;cursor:not-allowed' +
    '}' +
    '#panopto-caption-dl .close-btn{' +
      'background:#00131D;color:#FCBC00;' +
      'border:1px solid #00131D;text-align:center' +
    '}' +
    '#panopto-caption-dl .close-btn:hover{' +
      'background:#FCBC00;color:#00131D;border:1px solid #00131D' +
    '}' +
    '#panopto-caption-dl .status{' +
      'margin:10px 0;padding:10px;' +
      'background:#f0f0f0;border-radius:4px;font-size:0.9rem' +
    '}' +
    '#panopto-caption-dl .btn-desc{' +
      'display:block;font-size:0.85rem;opacity:0.9;margin-top:4px' +
    '}' +
    '#panopto-caption-dl .visually-hidden{' +
      'position:absolute;width:1px;height:1px;' +
      'padding:0;margin:-1px;overflow:hidden;' +
      'clip:rect(0,0,0,0);white-space:nowrap;border:0' +
    '}' +
    '</style>' +
    '<h2 id="panopto-dl-title">Download Captions</h2>' +
    '<p class="visually-hidden">Press Escape to close this dialogue</p>' +
    '<div id="status-msg" class="status" style="display:none" ' +
      'role="status" aria-live="polite"></div>' +
    '<button id="dl-clean" type="button" ' +
      'aria-label="Download clean text - continuous text, no timestamps">' +
      'Clean Text<span class="btn-desc">Continuous text, no timestamps</span>' +
    '</button>' +
    '<button id="dl-full" type="button" ' +
      'aria-label="Download text with line breaks - original formatting preserved">' +
      'Text with Line Breaks<span class="btn-desc">Original formatting preserved</span>' +
    '</button>' +
    '<button id="dl-basic" type="button" ' +
      'aria-label="Download captions with timestamps - standard caption file format">' +
      'Captions with Timestamps<span class="btn-desc">Standard caption file format</span>' +
    '</button>' +
    '<button class="close-btn" id="close-dl" type="button" ' +
      'aria-label="Close dialogue">Close</button>';
  
  // Add dialogue to page
  b.appendChild(m);
  
  // Extract video ID from URL
  const u = new URLSearchParams(window.location.search);
  const vid = u.get('id');
  
  // Check if we found a video ID
  if (!vid) {
    alert('Could not find video ID. Please ensure you are on a Panopto video page.');
    m.remove();
    if (previousFocus) previousFocus.focus();
    return;
  }
  
  // Get all focusable elements for focus trap
  const focusableEls = m.querySelectorAll('button');
  const firstFocusable = focusableEls[0];
  const lastFocusable = focusableEls[focusableEls.length - 1];
  
  // Focus trap function - keeps Tab navigation within dialogue
  const trapFocus = (e) => {
    if (e.key === 'Tab') {
      if (e.shiftKey) {
        // Shift+Tab on first element loops to last
        if (d.activeElement === firstFocusable) {
          e.preventDefault();
          lastFocusable.focus();
        }
      } else {
        // Tab on last element loops to first
        if (d.activeElement === lastFocusable) {
          e.preventDefault();
          firstFocusable.focus();
        }
      }
    }
  };
  
  // Escape key handler - closes dialogue
  const handleEscape = (e) => {
    if (e.key === 'Escape') {
      closeDialog();
    }
  };
  
  // Close dialogue and clean up
  const closeDialog = () => {
    m.removeEventListener('keydown', trapFocus);
    d.removeEventListener('keydown', handleEscape);
    m.remove();
    // Return focus to previous element
    if (previousFocus) previousFocus.focus();
  };
  
  // Build download URLs
  const host = window.location.host;
  const basicUrl = 'https://' + host + 
    '/Panopto/Pages/Transcription/GenerateSRT.ashx?id=' + vid + '&language=1';
  const fullUrl = basicUrl + '&full=true';
  
  // Get status message element
  const statusEl = d.getElementById('status-msg');
  
  // Extract video title from page for filename
  const getVideoTitle = () => {
    const t = d.querySelector('#primaryVideoName, .video-title, h1');
    return t ? 
      t.textContent.trim().replace(/[^a-z0-9]/gi, '_').substring(0, 50) : 
      'panopto_captions';
  };
  
  // Show status message to user
  const showStatus = (msg, isError) => {
    statusEl.textContent = msg;
    statusEl.style.display = 'block';
    statusEl.style.background = isError ? '#fee' : '#f0f0f0';
    statusEl.style.color = isError ? '#c00' : '#333';
  };
  
  // Clean transcript - removes timestamps and line numbers, joins into continuous text
  const cleanTranscript = (text) => {
    const lines = text.split('\n');
    let cleanedText = '';
    let firstLine = true;
    
    for (let line of lines) {
      line = line.trim();
      
      // Skip empty lines
      if (!line) continue;
      
      // Skip line numbers (e.g., "1", "2", "3")
      if (/^\d+$/.test(line)) continue;
      
      // Skip timestamps (e.g., "00:00:08,080 --> 00:00:13,680")
      if (/^\d{2}:\d{2}:\d{2}/.test(line)) continue;
      
      // Keep disclaimer lines (e.g., "[Auto-generated transcript...]")
      if (line.startsWith('[') && line.includes('transcript')) {
        cleanedText += line + '\n';
        continue;
      }
      
      // Add caption text with space separator
      if (line) {
        if (!firstLine && !cleanedText.endsWith('\n')) {
          cleanedText += ' ';
        }
        cleanedText += line;
        firstLine = false;
      }
    }
    
    return cleanedText.trim();
  };
  
  // Clean with line breaks - removes timestamps but preserves paragraph structure
  const cleanWithLineBreaks = (text) => {
    const lines = text.split('\n');
    let cleanedText = '';
    let inCaptionBlock = false;
    
    for (let line of lines) {
      line = line.trim();
      
      // Empty line marks end of caption block
      if (!line) {
        if (inCaptionBlock) {
          cleanedText += '\n\n';
          inCaptionBlock = false;
        }
        continue;
      }
      
      // Skip line numbers
      if (/^\d+$/.test(line)) continue;
      
      // Skip timestamps
      if (/^\d{2}:\d{2}:\d{2}/.test(line)) continue;
      
      // Keep disclaimer with extra spacing
      if (line.startsWith('[') && line.includes('transcript')) {
        cleanedText += line + '\n\n';
        continue;
      }
      
      // Add caption text with single newline
      cleanedText += line + '\n';
      inCaptionBlock = true;
    }
    
    return cleanedText.trim();
  };
  
  // Download file - fetches captions and triggers download
  const downloadFile = async (url, filename, cleanType) => {
    const btns = m.querySelectorAll('button');
    
    // Disable all buttons during download
    btns.forEach(btn => btn.disabled = true);
    showStatus('Downloading captions...');
    
    try {
      // Fetch caption data
      const resp = await fetch(url);
      if (!resp.ok) throw new Error('Download failed');
      
      let text = await resp.text();
      
      // Apply cleaning based on type
      if (cleanType === 'full') {
        text = cleanTranscript(text);
      } else if (cleanType === 'breaks') {
        text = cleanWithLineBreaks(text);
      }
      
      // Create download blob and link
      const blob = new Blob([text], { type: 'text/plain' });
      const link = d.createElement('a');
      link.href = URL.createObjectURL(blob);
      link.download = filename;
      link.style.display = 'none';
      
      // Trigger download
      b.appendChild(link);
      link.click();
      b.removeChild(link);
      URL.revokeObjectURL(link.href);
      
      // Show success message and auto-close
      showStatus('Download complete! Check your downloads folder.');
      setTimeout(closeDialog, 2000);
      
    } catch (err) {
      // Show error message
      showStatus('Error: Unable to download captions. You may not have permission.', true);
    } finally {
      // Re-enable buttons
      btns.forEach(btn => btn.disabled = false);
    }
  };
  
  // Get video title for filenames
  const videoTitle = getVideoTitle();
  
  // Set up event listeners
  m.addEventListener('keydown', trapFocus);
  d.addEventListener('keydown', handleEscape);
  
  // Wire up button click handlers
  d.getElementById('dl-clean').onclick = () => 
    downloadFile(fullUrl, videoTitle + '_clean.txt', 'full');
  
  d.getElementById('dl-full').onclick = () => 
    downloadFile(fullUrl, videoTitle + '_transcript.txt', 'breaks');
  
  d.getElementById('dl-basic').onclick = () => 
    downloadFile(basicUrl, videoTitle + '_captions.srt', null);
  
  d.getElementById('close-dl').onclick = closeDialog;
  
  // Set initial focus to first button
  firstFocusable.focus();
})();

How to create a more readable transcript from the clean text you download

To make a better formatted transcript with headings, paragraphs, bullet points and so on, use the below prompt with Copilot or a similar generative AI tool and give it the transcript you downloaded.

Instructions:

Please format the following transcript to be more readable.

Use headings, paragraph breaks, and bullet points as appropriate.

Important: because this is a transcript, it is essential that the text itself is kept verbatim. Please make sure you do not change any of the original words or the order in which they are presented.

Transcript:

[Insert your transcript here or attach it]

Keep in mind that the AI may adjust the text without you noticing, despite the direct request in the prompt not to do so.

Why This Tool is Useful

Students often need access to video transcripts for various accessibility and learning purposes, but downloading captions from Panopto typically requires instructor permissions or navigating through multiple settings menus. This creates barriers for students who:

  • Want to process transcripts with AI tools to create structured notes with headings and lists
  • Need plain text versions for copying and pasting into documents
  • Prefer reading transcripts rather than watching videos
  • Require captions files for personal video editing or archival purposes
  • Have limited time and need to quickly search through transcript content

This bookmarklet addresses these challenges by providing immediate, one-click access to captions in multiple formats, directly from the video viewing page without requiring special permissions or technical knowledge.

Top Tips for Usage

  1. Use the Clean Text option when you plan to paste the transcript into ChatGPT, Claude, or other AI tools to reformat it with proper headings and structure.
  2. Choose Text with Line Breaks if you want to preserve the original caption timing structure whilst removing timestamps.
  3. Select Captions with Timestamps (SRT format) if you need the file for video editing software or caption viewers.
  4. The downloaded files are automatically named using the video title, making them easy to identify in your downloads folder.
  5. You can press Escape to close the dialogue at any time, and keyboard navigation with Tab is fully supported.

What This Tool Won't Do

  1. It cannot download captions from videos that don't have captions enabled or published.
  2. It will not work on Panopto videos where you don't have viewing permissions.
  3. It cannot download the actual video file itself, only the text captions and transcripts.
  4. It will not automatically format or improve the quality of auto-generated captions.
  5. It cannot create captions for videos that don't already have them.