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; }
...