Get the full transcript of any YouTube video in seconds: as plain text to paste into ChatGPT, Claude or Gemini, with timestamps, as SRT subtitles or as a spreadsheet. Free, no API key, no extension.
Open the video (no need to sign in) and run the script: it opens the transcript panel by itself.
// Responsive Muse · YouTube Transcript Downloader · v1.0 · 2026-10-23
// Saves the transcript of a YouTube video as plain text (handy to paste into ChatGPT, Claude or
// Gemini), text with timestamps, SRT subtitles or CSV. Works on any video that has a transcript
// (most do, auto-generated or not), in the language YouTube shows it. It opens the transcript
// panel itself. It makes no network requests of its own and never reads cookies, passwords or tokens.
(async () => {
const TOOL = 'Download YouTube transcript';
const FORMAT = 'txt'; // 'txt' (plain text), 'timestamps' (text with [mm:ss]), 'srt' (subtitles) or 'csv'
const video = new URLSearchParams(location.search).get('v');
if (location.hostname !== 'www.youtube.com' || !video) {
alert('Open the YouTube video first, for example:\nhttps://www.youtube.com/watch?v=zjkBMFhNj_g');
return;
}
// A small box on the page shows progress and asks before changing anything,
// so you never need to read or edit the console.
const many = (n, word) => `${n} ${word}${n === 1 ? '' : 's'}`; // 1 video, 2 videos
const panel = (() => {
const box = document.createElement('div');
box.style.cssText = 'position:fixed;right:16px;bottom:16px;z-index:2147483647;width:340px;max-width:calc(100vw - 32px);' +
'background:#111318;color:#e6e8ee;font:14px/1.5 system-ui,sans-serif;border-radius:12px;padding:16px;' +
'box-shadow:0 8px 30px rgba(0,0,0,.4);text-align:left';
const head = document.createElement('strong');
head.textContent = TOOL;
const msg = document.createElement('p');
msg.style.cssText = 'margin:8px 0 12px;white-space:pre-line';
const row = document.createElement('div');
row.style.cssText = 'display:flex;flex-wrap:wrap;gap:8px;justify-content:flex-end';
box.append(head, msg, row);
document.body.append(box);
const button = (label, primary, onClick) => {
const b = document.createElement('button');
b.textContent = label;
b.style.cssText = 'font:600 14px system-ui,sans-serif;padding:8px 14px;border-radius:8px;cursor:pointer;' +
(primary ? 'background:#0f766e;color:#fff;border:1px solid #0f766e' : 'background:none;color:#e6e8ee;border:1px solid #3a3f4b');
b.onclick = onClick;
row.append(b);
};
return {
text(t) { msg.textContent = t; },
ask(t, yes) {
msg.textContent = t;
row.replaceChildren();
return new Promise(r => {
button('Cancel', false, () => { row.replaceChildren(); r(false); });
button(yes, true, () => { row.replaceChildren(); r(true); });
});
},
stoppable(onStop) { row.replaceChildren(); button('Stop', false, onStop); },
actions(t, list) {
msg.textContent = t;
row.replaceChildren();
list.forEach(([label, primary, run]) => button(label, primary, run));
},
finish(t) { this.actions(t, [['Close', true, () => box.remove()]]); },
close() { box.remove(); },
};
})();
// Offer the result as a downloaded file or as clipboard text (a real click, so it always works)
const deliver = (summary, body, file) => {
const save = () => {
const a = document.createElement('a');
a.href = URL.createObjectURL(new Blob([/\.csv$/.test(file) ? '\ufeff' + body : body], { type: 'text/plain;charset=utf-8' }));
a.download = file;
a.click();
setTimeout(() => URL.revokeObjectURL(a.href), 5000);
panel.text(`${summary}\nSaved as ${file} (check your Downloads).`);
};
const copy = () => navigator.clipboard.writeText(body)
.then(() => panel.text(`${summary}\nCopied. Paste it into a spreadsheet or a note.`))
.catch(() => panel.text(`${summary}\nThe browser blocked the clipboard: use Download instead.`));
panel.actions(summary, [['Close', false, () => panel.close()], ['Copy', false, copy], ['Download', true, save]]);
};
const sleep = ms => new Promise(r => setTimeout(r, ms));
const clean = s => (s || '').replace(/\s+/g, ' ').trim();
// New transcript panel (transcript-segment-view-model) and the old one (ytd-transcript-segment-renderer)
const segments = () => document.querySelectorAll('transcript-segment-view-model, ytd-transcript-segment-renderer');
// 1. Open the transcript panel (the "Show transcript" button in the description) and wait for it to fill
document.querySelector('video')?.pause();
if (!segments().length) {
const open = document.querySelector('ytd-video-description-transcript-section-renderer button');
if (!open) {
panel.finish('This video has no transcript (no "Show transcript" button in its description). Try another video.');
return 0;
}
open.click();
}
panel.text('Opening the transcript…');
let n = 0;
for (let i = 0, stable = 0; i < 40 && stable < 4; i++) { // up to 20 s, until the count stops growing
await sleep(500);
const now = segments().length;
stable = now && now === n ? stable + 1 : 0;
n = now;
}
const rows = [...segments()].map(s => ({
ts: clean((s.querySelector('[class$="SegmentViewModelTimestamp"], .segment-timestamp') || {}).textContent),
text: clean((s.querySelector('.ytAttributedStringHost, .segment-text') || {}).textContent),
})).filter(r => r.text);
if (!rows.length) {
panel.finish(n ? 'The transcript is empty or YouTube changed its layout: please report it on responsive-muse.com.'
: 'The transcript didn\'t load. Reload the page, wait until the video starts and run the script again.');
return 0;
}
// 2. Build the file and offer it
const secs = ts => ts.split(':').reduce((a, v) => a * 60 + Number(v), 0);
const srtTime = s => {
const h = Math.floor(s / 3600), m = Math.floor(s / 60) % 60, sec = Math.floor(s % 60), ms = Math.round((s % 1) * 1000);
return `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}:${String(sec).padStart(2, '0')},${String(ms).padStart(3, '0')}`;
};
const end = document.querySelector('video')?.duration || secs(rows[rows.length - 1].ts) + 5;
const cell = v => '"' + String(v).replace(/"/g, '""') + '"';
const body = {
txt: rows.map(r => r.text).join(' ').replace(/ {2,}/g, ' '),
timestamps: rows.map(r => `[${r.ts}] ${r.text}`).join('\n'),
srt: rows.map((r, i) => `${i + 1}\n${srtTime(secs(r.ts))} --> ${srtTime(i + 1 < rows.length ? secs(rows[i + 1].ts) : end)}\n${r.text}\n`).join('\n'),
csv: ['Start,Seconds,Text', ...rows.map(r => [r.ts, secs(r.ts), r.text].map(cell).join(','))].join('\n'),
}[FORMAT] || rows.map(r => r.text).join(' ');
const title = clean(document.querySelector('h1.ytd-watch-metadata, #title h1')?.textContent) || video;
const ext = { txt: 'txt', timestamps: 'txt', srt: 'srt', csv: 'csv' }[FORMAT] || 'txt';
const name = ('transcript-' + title).slice(0, 80).replace(/[\\/:*?"<>|]+/g, '_');
const words = rows.map(r => r.text).join(' ').split(/\s+/).length;
deliver(`Transcript ready: ${rows.length} lines, about ${words.toLocaleString()} words (up to ${rows[rows.length - 1].ts}).`, body, `${name}.${ext}`);
return rows.length;
})();
Bookmarklet: drag the middle button to your bookmarks bar, open www.youtube.com and click the bookmark. It runs the same script with the options above.
- Sends nothing anywhere. No fetch, XHR or external scripts. The only network activity is www.youtube.com itself loading your list.
- Never touches your login. It doesn't read cookies, passwords, tokens or browser storage.
- Only runs on www.youtube.com. Anywhere else it stops before doing anything.
- Short and readable. 128 commented lines, not minified or obfuscated.
- Shows its progress on the page. A small box tells you what it's doing and offers Download or Copy at the end.
- Same code for everyone. SHA-256 of the default version:
73efdbc931c1b779…
Verify it yourself in 30 seconds
- Read it: every line is above, with comments.
- Search the code for
fetch,XMLHttpRequest,cookie,localStorageoreval. None of them appear. - Open the Network tab before running it: you'll only see requests to www.youtube.com.
How to use it
- Open the video on youtube.com. You don’t need to be signed in.
- Open the console: F12 or Ctrl+Shift+J (Cmd+Option+J on Mac). If Chrome asks, type
allow pastingand press Enter. - Copy the script, paste it in the console and press Enter. It pauses the video, opens the transcript and offers the file a few seconds later.
Which format should I pick?
- Plain text: one block of text without times. The best choice to summarize a video with AI, translate it or read it.
- Text with timestamps: each line starts with [mm:ss], handy for notes and quotes.
- SRT subtitles: a subtitle file you can load in a video player or editor.
- CSV: start time, seconds and text in columns, for analysis in Excel or Sheets.
Summarize a YouTube video with ChatGPT or Claude
Download the plain text, paste it into the chat and ask, for example: “Summarize this talk in 10 bullet points and list the main claims with their timestamps” (use the timestamps format for that one). A one-hour video is roughly 10,000 words.
FAQ
Which videos have a transcript?
Most of them: YouTube creates one automatically for videos in many languages, and creators can upload their own. If the description has no "Show transcript" button, the video has none.
Can I summarize a video with AI?
Yes. Download the plain text, paste it into ChatGPT, Claude or Gemini and ask for a summary, key points or quotes. A one-hour talk is about 10,000 words, which fits in any of them.
In which language is the transcript?
In the one YouTube shows in the transcript panel. If the video has several, pick the language there first and run the script again.
Is it free and safe?
Yes. It runs in your own YouTube tab, needs no API key or account and sends nothing anywhere.
