Someone links a 45-minute conference talk and says "the good part is in the middle somewhere." You want three sentences. You do not want 45 minutes.
So I built SummarizeVideoToText: paste a video link, get a text workspace — full transcript, AI summary, timestamped chapters, a mind map, and a Q&A panel you can interrogate about the video. No sign-up needed to try it.
That's the pitch. The interesting part is what broke along the way.
1. Getting captions is a fallback chain, not an API call
My first version called one endpoint and assumed a transcript came back. In practice, that endpoint fails constantly — YouTube rotates its internals, some videos need a proof-of-origin token, some tracks exist but not in the language you asked for.
What actually works is a chain of providers where each layer falls back to the next:
export class ChainProvider implements TranscriptProvider {
constructor(private readonly providers: TranscriptProvider[]) {}
// try each in turn; fall through on failure
}
The non-obvious part is knowing when not to fall through. Two cases end the chain immediately:
-
invalid_url— the link itself is broken. No provider will do better. -
no_transcript— layer one confirmed the page loads fine and has no caption track at all. A paid provider will confirm the same thing and bill you for it.
Everything else falls through. That distinction is the difference between a robust chain and a machine that burns API credits to rediscover the same "nope."
The honest limitation this leaves: if a video has no captions in any form, there's nothing to summarize. I show that plainly instead of pretending. Audio transcription for YouTube is on the roadmap; TikTok and Instagram already go through AI transcription because they rarely ship captions.
2. Caching wasn't about speed. It was about money.
I started with Redis and a TTL, like you do. Then I watched the logs: a video would get summarized, sit for a week, the key would expire, someone would open the same URL — and the whole pipeline would run again. New caption fetch, new LLM call, new bill.
The realization: a video's content never changes. There is no correctness reason to ever evict a summary. TTL made sense for a hot cache, not for the artifact itself.
So it became two layers:
- Redis — hot cache, short TTL, absorbs the repeat traffic.
- Postgres — permanent store, no TTL. Redis misses land here, not on the model.
Storing a few KB of text forever costs orders of magnitude less than regenerating it once. If your pipeline has an expensive deterministic step, "cache expiry" and "delete the result" should not be the same decision.
The user-visible payoff is that opening a video someone else already summarized is instant and costs nobody anything — which is also why I could leave the free tier usable without an account.
3. Notion doesn't take Markdown
I wanted "export this whole note to Notion." I assumed I'd POST some Markdown. Notion's API is a block model — every heading, paragraph, and list item is a typed object, and the constraints stack up fast:
- Max 100 child blocks per request. A transcript is hundreds of lines.
- Max 2000 characters per rich-text object. Long paragraphs need chunking.
- Max 2 levels of nesting per request. So a collapsible toggle containing a full transcript can't be created in one shot: you create the toggle, read its ID out of the response, then append its children in batches.
And the one that cost me an evening of confusion: an integration without "read content" permission gets partial objects back. Creating a page returns an object with an id and no url. Searching returns pages with no properties, so no title. Nothing errors. You just get undefined where you expected a link, and a page titled "".
Two lessons. First, when an API returns a suspiciously empty field, check the permission scope before you check your code. Second, degrade instead of inventing: my first fix put the string "(Untitled page)" in the UI, which turned a missing title into a confidently wrong one. The real fix was to reconstruct the URL from the ID (notion.so/<id-without-dashes> is a valid link) and drop the page name from the message when it isn't known.
Bonus: the Obsidian URL that was always too long
Obsidian has a URI scheme: obsidian://new?name=...&content=.... Clean, one click, works great in the demo.
It never worked in production. A full transcript blows past the URI length limit every single time, so my code silently fell back to downloading a .md file. Users clicked "Export to Obsidian" and got a file in their Downloads folder — technically not a failure, so nothing ever showed up in the error logs.
The fix was one flag I'd missed in the docs:
await copyText(content);
window.location.href = `obsidian://new?name=${encodeURIComponent(title)}&clipboard=true`;
clipboard=true tells Obsidian to pull the body from the clipboard. The URI now carries only the title, and length stops being a factor.
The analytics bug that made everything else unmeasurable
One more, because it invalidated a week of numbers.
My event helper was defensive:
export function track(event, params) {
const gtag = window.gtag;
if (typeof gtag !== 'function') return; // ← this line
...
}
Sensible: ad blockers exist, analytics should never break a click. But the gtag stub was loading with afterInteractive, meaning window.gtag doesn't exist until hydration finishes. On a slow connection that's a multi-second window — and the "Summarize" button is the first thing anyone clicks. Those events were dropped silently, so my funnel's denominator was quietly too small.
The fix is to load the tiny stub beforeInteractive (it only pushes to an array) and let the real script arrive later and replay the queue. That's what Google's own snippet does; I'd split it apart without thinking about ordering.
Related lesson from the same audit: instrument outcomes, not intentions. I was tracking "user clicked Export" but not whether the export succeeded. 100 clicks could be 97 successes or 3. Every click event that kicks off async work deserves a matching result event with a failure code.
What it is now
- YouTube via official captions; TikTok / Instagram via AI transcription
- Summary, timestamped chapters, key insights, an interactive mind map, and Q&A grounded in the actual transcript
- 25 summary templates (study notes, meeting minutes, Twitter thread, flashcards, SEO article…) and 14 output languages, independent of the video's language
- Export the whole note to Notion, Obsidian, or Markdown, with clickable timestamps that jump back into the video
- Free: 2 videos/day with no account (up to 15 min), 10/day with a free Google sign-in (up to 1 hour)
Try it: summarizevideototext.com
If you've fought with the Notion block API or the YouTube caption endpoints, I'd genuinely like to compare notes in the comments — especially if you found a cleaner answer than a fallback chain.
Top comments (0)