What inspired me

I was tired of Ctrl+F on comment sections. Keyword search misses "the audio is muddy" when you typed "sound quality." Creators also care when in the video people reacted, those 3:15 notes are sitting in the text, unused. I wanted search by meaning, topics you can inspect, and charts you can click.

How I built it

The stack is Flask, vanilla HTML/CSS/JS, and a local NLP pipeline.

  1. Fetch. A public YouTube URL goes to the Data API v3. Comments are capped at 200 and cached in comments.pkl so quota and load time stay under control.
  2. Clean. HTML entities are unescaped, URLs stripped, empty comments dropped.
  3. Embed. all-MiniLM-L6-v2 turns each comment into a unit vector. Cosine similarity then collapses to a dot product:

  4. Hybrid search. A hit is a keyword match or high similarity score. Results are ranked by similarity, so "sound problems" can still surface "the mic is clipping."

  5. Sentiment. VADER's compound score is labeled with the usual cutoffs (positive and negative).

  6. Topics. KMeans on the embeddings minimizes within-cluster scatter:

Each cluster gets a TF-IDF title. The core message is the comment closest to the centroid.

  1. Analytics. Chart.js dashboards for sentiment mix, comments per day, top authors, and a heatmap of in-comment timestamps (3:15 → minute 3). Clicks cross-filter the whole page. CSV and PDF export close the loop.

Nothing is sent to an LLM API. Embeddings, sentiment, and clustering all run on the machine.

What I learned

  • Normalized embeddings make search cheap: cosine becomes a dot product.
  • Hybrid retrieval beats either keywords or vectors alone. Keywords catch exact names; vectors catch paraphrases.
  • KMeans needs readable labels. TF-IDF titles plus a core comment made clusters usable instead of "Cluster 3."
  • The UI is the product. Filters, session state, and clickable charts mattered as much as the models.
  • API quota is a real constraint. Caching and a hard cap were design choices, not afterthoughts.

Challenges

Quota vs. freshness. Every fetch costs YouTube units. Caching the same video made iteration possible; a new URL still fetches live.

First-load latency. MiniLM is local and free, but the first encode is slow. Lazy-loading the model and skipping work when embeddings already exist helped.

Short, messy text. Comments are slang, sarcasm, and emojis. VADER is fast and local, but it is lexicon-based, so sarcasm still slips through.

Keeping pages in sync. Search, Topics, Analytics, and Topic Analytics all share one comment store plus the same filters. Passing video_id, query, and filter state without the views drifting was harder than the clustering math.

Small dataset size vs. chosen number of clusters. With at most 200 comments, a fixed number of clusters can over-split. I exposed the cluster count option in the UI instead of hiding a brittle "best count."

ClipChat is the tool I wanted while reading comments: search by meaning, cluster by topic, and click through the evidence, on your machine.

Share this project:

Updates

Submission history