# YouTube & Document Summarizer
A .NET 10 console app that summarizes YouTube videos *and* local documents —
text, Markdown, CSV, PDF, Word, OpenDocument, and Excel — using an LLM
(OpenAI or a local Ollama model). One prompt accepts either a YouTube URL or
a file path; everything else (summarize, save, display) runs through the
same pipeline.
---
## Prerequisites
- [.NET 10 SDK](https://dotnet.microsoft.com/download)
- [**yt-dlp**](https://github.com/yt-dlp/yt-dlp) on your `PATH` — used to fetch
YouTube metadata and captions. No YouTube API key needed.
```bash
pip install yt-dlp
```
- **Local Ollama** (recommended, free) or an **OpenAI API key**
---
## Setup
```bash
cd summarize
# Edit appsettings.json with your LLM endpoint/key (see Configuration below)
nano appsettings.json
dotnet restore
dotnet run
```
---
## Using it
The app loops on a single prompt:
```
Enter a YouTube URL or a local file path (or 'q' to quit):
```
- **YouTube**: paste any `watch?v=...`, `youtu.be/...`, `/shorts/...`, or
`/embed/...` URL (or a bare 11-character video ID).
- **Local document**: paste or type a path to an existing file. Supported
types: `.txt` `.md` `.csv` `.pdf` `.docx` `.odt` `.xlsx`. The kind of input
is auto-detected — no need to say which one it is.
- `q` quits.
For each input you're asked:
1. **Save transcript to file?** — `y/n`, defaults to **yes** on Enter.
Saves the extracted transcript/document text plus the summary to
`~/Downloads/transcripts/`.
2. **Choose summary mode** — an arrow-key list, Standard pre-selected:
- **Standard** — detailed bullet-point summary (the default; Enter picks it)
- **Personal Filter** — a 1–2 sentence summary plus an ACT / MONITOR /
IGNORE relevance verdict against personal priorities (time, finances,
health, family, service to others)
- **Custom** — write your own instructions on the spot (multi-line; finish
with a blank line). Replaces the built-in prompt entirely, for when you
want something other than a summary or a verdict — a table, an
extraction task, a specific format, etc.
Ctrl+C cancels the current operation without killing the app. The terminal
tab title tracks progress (`Summarize` while idle, `Summarize
` while
working on something).
---
## Configuration Reference
All settings live in `appsettings.json` (bind to `AppSettings` in
[AppSettings.cs](AppSettings.cs)):
| Key | Description | Default |
| --- | --- | --- |
| `LLM:BaseUrl` | API endpoint — `https://api.openai.com/v1` for OpenAI, `http://localhost:11434/v1` for Ollama | `https://api.openai.com/v1` |
| `LLM:ApiKey` | API key (any non-empty value works for Ollama) | *(empty — required for OpenAI)* |
| `LLM:Model` | Chat model, e.g. `gpt-4o-mini` (OpenAI) or `qwen3:14b` (Ollama) | `gpt-4o-mini` |
| `LLM:MaxTokens` | Max tokens in the summary response | `1500` |
| `LLM:TimeoutSeconds` | Max time to wait per API call | `100` |
| `Summarizer:ChunkWordLimit` | Word count above which a transcript/document is split into chunks (map-reduce, see below). Must stay comfortably above ~200 — the fixed chunk overlap — or chunking will error on short inputs. | `3000` |
| `Summarizer:ShowTranscript` | Print the full extracted text before summarizing | `false` |
Any value can be overridden with an environment variable using `__` as the
section separator (handy for CI/containers):
```bash
export LLM__ApiKey="sk-..."
export LLM__Model="gpt-4o"
dotnet run
```
---
## Architecture
```
Program.cs
│ Main loop → auto-detects YouTube URL vs. local file path
│
├── YouTubeService — shells out to yt-dlp
│ ├── ExtractVideoId() — URL parsing
│ ├── GetVideoMetadataAsync() — video title/channel/date/duration
│ └── GetTranscriptAsync() — caption download + VTT/SRT/timedtext parsing
│
├── DocumentService — reads a local file
│ ├── BuildMetadata() — title/type/modified-date from the file
│ └── ExtractTextAsync() — dispatches by extension:
│ .txt/.md/.csv → read directly
│ .pdf → PdfPig
│ .docx/.xlsx → DocumentFormat.OpenXml
│ .odt → hand-rolled zip + XML (ODF content.xml)
│
│ Both feed the same VideoMetadata / VideoTranscript shapes into:
│
├── SummarizerService
│ ├── SummarizeAsync() — routes to single-pass or chunked, per mode
│ │ (Standard / Personal Filter / Custom)
│ ├── SinglePassSummarize() — one LLM call for short inputs
│ └── ChunkedSummarizeAsync() — map-reduce for long inputs
│
├── TranscriptFileService — saves transcript + summary to a .txt file
│
└── ConsoleRenderer — all terminal output (Spectre.Console):
banner, prompts, progress spinners, and
the final summary panel/grid
```
### Source Quality Transparency
The app tracks how the text was obtained and flags it accordingly:
| Source | Badge | Warning shown? |
| --- | --- | --- |
| Owner-published captions | `✓ Owner-published` | No |
| Community-contributed captions | `✓ Community captions` | Minor note |
| Auto-generated captions (ASR) | `~ Auto-generated` | Yes — accuracy caveat |
| No captions (metadata only) | `✗ Metadata only` | Yes — limited accuracy |
| Local document | `✓ Local document` | No |
### Long-Input Strategy
Transcripts/documents exceeding `ChunkWordLimit` words use a **map-reduce**
approach — split into overlapping chunks (200-word overlap preserves context
at boundaries), each chunk summarized independently, then combined into one
coherent result. This applies to every mode, including Custom (the combine
step gets an auto-generated wrapper prompt asking the model to merge the
partial responses while still honoring your instructions). Handles
hour-long videos, long reports, and multi-sheet spreadsheets without hitting
model context limits.