VTT → NVivo (Milliseconds Removed – 100% Working)

by hcanning2012

HTML

<div class="box">
  <h1>VTT → NVivo Transcript Converter</h1>
  <p><strong>Removes only milliseconds</strong>: 00:00:02.760 → 00:00:02<br>
     Output: <code>00:00:02[TAB]your text here</code></p>

  <textarea id="vttInput" placeholder="Paste your .vtt content here (or drop the file)..."></textarea><br>

  <button id="convertBtn">Convert (remove milliseconds)</button>
  <button id="copyBtn">Copy Result</button>
  <button id="downloadBtn">Download .txt</button>

  <br><br>
  <textarea id="output" readonly placeholder="Converted result will appear here..."></textarea>

  <div class="note">
    Tested with YouTube, Zoom, Microsoft Teams, Otter.ai, Descript, etc.<br>
    Works even when there are no blank lines between cues.
  </div>
</div>

CSS

body { font-family: Arial, sans-serif; max-width: 850px; margin: 40px auto; padding: 20px; line-height: 1.6; background:#f5f5f5; }
  .box { background:white; padding:25px; border-radius:12px; box-shadow:0 4px 12px rgba(0,0,0,0.1); }
  textarea { width:100%; height:280px; font-family:Consolas,monospace; font-size:14px; padding:10px; }
  button { padding:12px 20px; font-size:16px; margin:8px 8px 8px 0; cursor:pointer; }
  .note { background:#e8f4fc; padding:12px; border-radius:6px; margin-top:20px; font-size:0.95em; }

JavaScript

// Remove milliseconds: 00:00:02.760 → 00:00:02
function stripMs(ts) {
  return ts.replace(/\.\d+$/, '');
}

// Main converter – robust against all VTT variations
function convertVtt(vtt) {
  const lines = vtt.split(/\r?\n/);
  const result = [];
  let currentStart = null;

  for (let line of lines) {
    line = line.trim();

    // 1. Detect timing line: 00:00:04.980 --> 00:00:07.529  (with optional styling)
    const timing = line.match(/^(\d{2}:\d{2}:\d{2}\.\d{3})\s*-->\s*\d{2}:\d{2}:\d{2}\.\d{3}/);
    if (timing) {
      currentStart = stripMs(timing[1]);
      continue;   // go to next line (text follows)
    }

    // 2. If we have a start time and this line is actual text
    if (currentStart && line !== '' &&
        !line.startsWith('WEBVTT') &&
        !line.startsWith('NOTE') &&
        !line.startsWith('Kind:') &&
        !line.startsWith('Language:') &&
        !/^\d+$/.test(line) &&          // skip cue numbers like "1"
        !line.includes('-->')) {

      result.push(currentStart + '\t' + line);
      currentStart = null;   // reset so multi-line cues are merged correctly
    }

    // 3. Handle multi-line cue text (no blank line between cues)
    else if (currentStart && line !== '' && !timing) {
      // Append to previous line (some VTTs have wrapped text)
      result[result.length - 1] += ' ' + line;
    }
  }
  return result.length ? result.join('\n') : '// No transcript found';
}

// Buttons
document.getElementById('convertBtn').onclick = () => {
  const input = document.getElementById('vttInput').value.trim();
  if (!input) {
    alert('Please paste or drop a .vtt file first');
    return;
  }
  const output = convertVtt(input);
  document.getElementById('output').value = output;
};

document.getElementById('copyBtn').onclick = () => {
  const ta = document.getElementById('output');
  if (!ta.value || ta.value.startsWith('//')) { alert('Nothing to copy'); return; }
 ...