Podcast Bulk Ingest Skill
One-off backfill of a YouTube podcast series into structured markdown files
with extracted source tables. Produces the raw material that the weekly
podcast-knowledge-ingest skill processes into ontology entries.
Implemented as the podcast-bulk-ingest Rust binary — one of three binaries
in services/podcast-ingest (crate
podcast-ingest, module bulk::*), porting the retired bulk_ingest.py.
CLI flags mirror the original argparse definition exactly; on-disk file
formats (.ingest-state.json, .enrichment/*.json, the markdown transcript
shape) are unchanged.
New domain detection + OntoCast bootstrapping
When ingesting a podcast series that covers a domain not already represented
in the ontology (e.g., biotech, materials science, geopolitics), the skill
detects this and offers to bootstrap ontology pages using OntoCast.
How it works
Domain probe: After downloading the first batch of transcripts, the
skill samples 3–5 episodes and extracts key terms using the Loom. It then
queries ontology_search for each term. If <30% of terms match existing
pages, the domain is flagged as "new".
User confirmation: The skill reports findings and asks:
- "This podcast covers [domain]. Only N% of key terms exist in the ontology.
Would you like to bootstrap ontology pages for this domain using OntoCast?"
- Options: Yes (full bootstrap), Partial (top 20 terms only), No (skip)
OntoCast extraction: If confirmed, the skill:
- Concatenates the sampled transcripts into a single document
- Sends it to OntoCast with
RENDER_MODE=ontology_and_facts and
the existing ontology as seed (via ONTOCAST_ONTOLOGY_DIRECTORY)
- OntoCast produces Turtle with new classes, relationships, and facts
Staging via ontocast_import.py: The Turtle output is processed through
pipeline.ontocast_import from the knowledgeGraph repo:
python -m pipeline.ontocast_import out/ontology.ttl \
--output-dir review/podcast-bootstrap-[domain] \
--project-token ngm \
--domain [domain] \
--source-document 'podcast:[channel]:bootstrap:[date]' \
--default-parent-iri urn:ngm:class/[domain-root] \
--default-parent-label '[Domain Root]'
Review prompt: Candidate pages are written to a review directory
(public:: false, pending-review). The skill reports:
- How many candidate classes/individuals were created
- Which existing pages they link to
- The user reviews and promotes accepted pages to the main ontology
Resume: Once the ontology has the new domain's pages, the weekly
podcast-knowledge-ingest cron will enrich them automatically.
OntoCast prerequisites
OntoCast is an upstream producer — it is NOT vendored into this skill.
To use the bootstrapping flow:
# Install OntoCast (requires Python 3.12+, an LLM API key)
pip install "ontocast[server,openai]"
# Or use the Loom as the LLM backend (no external API needed):
export LLM_PROVIDER=openai_compatible
export LLM_BASE_URL=${LOOM_BASE_URL}
export LLM_API_KEY=not-needed
export LLM_MODEL_NAME=qwen3.8-27b
The ontocast_import.py adapter lives in the knowledgeGraph repo at
pipeline/ontocast_import.py. It accepts any standards-compliant Turtle
and produces private candidate Logseq pages.
When NOT to use OntoCast
- The podcast covers a domain already well-represented in the ontology
(AI, crypto, spatial computing, governance) — use the normal enrichment flow
- You want to add a few specific pages manually — just create them
- The podcast is primarily opinion/commentary with few factual claims
What it does
Download: Fetches all episodes in a date window from a YouTube channel
via yt-dlp. One markdown file per episode: title, date, duration, link,
show notes, extracted links, full auto-generated transcript.
Source extraction: Regex NLP pass over each transcript to find named
publications, research firms, company announcements, quotes, and social
posts. Outputs per-episode JSON to .enrichment/.
Enrichment: Resolves source URLs via Perplexity search. Downloads
significant reports/papers to assets/. Inserts ## Sources Mentioned
tables into each markdown file.
Marking: Sets ingest-status:: downloaded on line 1 of each file,
signalling to the weekly skill that this file exists but has not been
processed for ontology integration.
Usage
podcast-bulk-ingest <channel> [options]
Arguments
| Arg |
Default |
Description |
channel |
required |
YouTube channel URL, @handle, or playlist URL |
--months |
6 |
Months of history to download |
--output-dir |
./transcripts |
Output directory |
--date-start |
computed |
Override start date (YYYYMMDD) |
--date-end |
computed |
Override end date (YYYYMMDD) |
--enrich |
flag |
Run source extraction + URL resolution |
--assets |
flag |
Download referenced reports/papers |
--max-episodes |
unlimited |
Cap number of episodes to download |
--old-streak |
15 |
Consecutive old episodes before early exit |
Examples
# Backfill 9 months of AI Daily Brief with enrichment
podcast-bulk-ingest @TheAIDailyBrief --months 9 --output-dir "$VAULT_TRANSCRIPTS" --enrich --assets
# Just transcripts for a different podcast, last 3 months
podcast-bulk-ingest @lexfridman --months 3 --output-dir lexfridman
# Specific date window
podcast-bulk-ingest @TheAIDailyBrief --date-start 20251118 --date-end 20260818 --enrich
Output structure
output-dir/
├── episode-title-slug.md # ingest-status:: downloaded on line 1
├── assets/
│ ├── report-name.pdf
│ └── blog-post.html
├── .enrichment/
│ ├── episode-slug.json # per-episode extracted sources
│ ├── all_sources.json # flat list for dedup
│ ├── unique_sources.json # deduplicated source index
│ ├── resolved_urls.json # Perplexity-resolved URLs
│ └── extraction_summary.json # stats
└── .ingest-state.json # video ID → status map
Markdown format
ingest-status:: downloaded
# Episode Title
- **Date**: 2026-08-18
- **Duration**: 29:13
- **YouTube**: https://www.youtube.com/watch?v=xxxxx
## Show Notes
...
## Links
...
## Sources Mentioned
| Source | Type | Context | URL |
|--------|------|---------|-----|
...
## Transcript
...
Relationship to other skills
- podcast-bulk-ingest (this skill): One-off historical backfill. Produces
markdown files marked
ingest-status:: downloaded.
- podcast-knowledge-ingest (weekly cron): Processes files marked
downloaded,
extracts assertions, integrates into ontology, marks files processed.
- youtube-transcript-archiver: Interactive agent-triggered variant. Same
download logic, different orchestration.
Design decisions
--dump-json over --print: YouTube descriptions contain newlines that
corrupt line-based metadata extraction. JSON output is reliable.
- SRT subtitle cleanup: Strip timestamps, sequence numbers, HTML tags,
deduplicate consecutive identical lines.
- Streak-based early exit: Flat playlist API returns no date ordering.
15 consecutive old episodes triggers stop (configurable via
--old-streak).
- State file:
.ingest-state.json tracks video IDs independently of
markdown files, enabling fast delta detection on re-runs.
- Ingest-status marker: Line 1 of each markdown. Values:
downloaded
(bulk ingest done), pending (weekly ingest queued), processed:DATE:N
(N assertions extracted on DATE), skipped (no extractable assertions).
Prerequisites
podcast-bulk-ingest (services/podcast-ingest) and yt-dlp resolvable on
PATH — the binary shells out to the yt-dlp console script directly (no
Python interpreter or PYTHONPATH involved on this binary's side any more)
- For enrichment: Perplexity MCP tools (
perplexity_search)
- For asset downloads:
curl or wget
1---2name: podcast-bulk-ingest3description: Bulk backfill markdown transcript files for a YouTube podcast series. Downloads transcripts, show notes, and links for all episodes in a date range, then runs source extraction and applies enrichment tables. Designed for one-off historical backfill — for ongoing weekly ingest, see podcast-knowledge-ingest. When the podcast covers a domain not yet in the ontology, guides the user through OntoCast-based ontology bootstrapping.4---56# Podcast Bulk Ingest Skill78One-off backfill of a YouTube podcast series into structured markdown files9with extracted source tables. Produces the raw material that the weekly10`podcast-knowledge-ingest` skill processes into ontology entries.1112Implemented as the `podcast-bulk-ingest` Rust binary — one of three binaries13in [`services/podcast-ingest`](../../services/podcast-ingest) (crate14`podcast-ingest`, module `bulk::*`), porting the retired `bulk_ingest.py`.15CLI flags mirror the original `argparse` definition exactly; on-disk file16formats (`.ingest-state.json`, `.enrichment/*.json`, the markdown transcript17shape) are unchanged.1819## New domain detection + OntoCast bootstrapping2021When ingesting a podcast series that covers a domain not already represented22in the ontology (e.g., biotech, materials science, geopolitics), the skill23detects this and offers to bootstrap ontology pages using OntoCast.2425### How it works26271. **Domain probe**: After downloading the first batch of transcripts, the28 skill samples 3–5 episodes and extracts key terms using the Loom. It then29 queries `ontology_search` for each term. If <30% of terms match existing30 pages, the domain is flagged as "new".31322. **User confirmation**: The skill reports findings and asks:33 - "This podcast covers [domain]. Only N% of key terms exist in the ontology.34 Would you like to bootstrap ontology pages for this domain using OntoCast?"35 - Options: Yes (full bootstrap), Partial (top 20 terms only), No (skip)36373. **OntoCast extraction**: If confirmed, the skill:38 - Concatenates the sampled transcripts into a single document39 - Sends it to OntoCast with `RENDER_MODE=ontology_and_facts` and40 the existing ontology as seed (via `ONTOCAST_ONTOLOGY_DIRECTORY`)41 - OntoCast produces Turtle with new classes, relationships, and facts42434. **Staging via ontocast_import.py**: The Turtle output is processed through44 `pipeline.ontocast_import` from the knowledgeGraph repo:45 ```bash46 python -m pipeline.ontocast_import out/ontology.ttl \47 --output-dir review/podcast-bootstrap-[domain] \48 --project-token ngm \49 --domain [domain] \50 --source-document 'podcast:[channel]:bootstrap:[date]' \51 --default-parent-iri urn:ngm:class/[domain-root] \52 --default-parent-label '[Domain Root]'53 ```54555. **Review prompt**: Candidate pages are written to a review directory56 (`public:: false`, `pending-review`). The skill reports:57 - How many candidate classes/individuals were created58 - Which existing pages they link to59 - The user reviews and promotes accepted pages to the main ontology60616. **Resume**: Once the ontology has the new domain's pages, the weekly62 `podcast-knowledge-ingest` cron will enrich them automatically.6364### OntoCast prerequisites6566OntoCast is an upstream producer — it is NOT vendored into this skill.67To use the bootstrapping flow:6869```bash70# Install OntoCast (requires Python 3.12+, an LLM API key)71pip install "ontocast[server,openai]"7273# Or use the Loom as the LLM backend (no external API needed):74export LLM_PROVIDER=openai_compatible75export LLM_BASE_URL=${LOOM_BASE_URL}76export LLM_API_KEY=not-needed77export LLM_MODEL_NAME=qwen3.8-27b78```7980The `ontocast_import.py` adapter lives in the knowledgeGraph repo at81`pipeline/ontocast_import.py`. It accepts any standards-compliant Turtle82and produces private candidate Logseq pages.8384### When NOT to use OntoCast8586- The podcast covers a domain already well-represented in the ontology87 (AI, crypto, spatial computing, governance) — use the normal enrichment flow88- You want to add a few specific pages manually — just create them89- The podcast is primarily opinion/commentary with few factual claims9091## What it does92931. **Download**: Fetches all episodes in a date window from a YouTube channel94 via yt-dlp. One markdown file per episode: title, date, duration, link,95 show notes, extracted links, full auto-generated transcript.96972. **Source extraction**: Regex NLP pass over each transcript to find named98 publications, research firms, company announcements, quotes, and social99 posts. Outputs per-episode JSON to `.enrichment/`.1001013. **Enrichment**: Resolves source URLs via Perplexity search. Downloads102 significant reports/papers to `assets/`. Inserts `## Sources Mentioned`103 tables into each markdown file.1041054. **Marking**: Sets `ingest-status:: downloaded` on line 1 of each file,106 signalling to the weekly skill that this file exists but has not been107 processed for ontology integration.108109## Usage110111```bash112podcast-bulk-ingest <channel> [options]113```114115### Arguments116117| Arg | Default | Description |118|-----|---------|-------------|119| `channel` | required | YouTube channel URL, @handle, or playlist URL |120| `--months` | 6 | Months of history to download |121| `--output-dir` | `./transcripts` | Output directory |122| `--date-start` | computed | Override start date (YYYYMMDD) |123| `--date-end` | computed | Override end date (YYYYMMDD) |124| `--enrich` | flag | Run source extraction + URL resolution |125| `--assets` | flag | Download referenced reports/papers |126| `--max-episodes` | unlimited | Cap number of episodes to download |127| `--old-streak` | 15 | Consecutive old episodes before early exit |128129### Examples130131```bash132# Backfill 9 months of AI Daily Brief with enrichment133podcast-bulk-ingest @TheAIDailyBrief --months 9 --output-dir "$VAULT_TRANSCRIPTS" --enrich --assets134135# Just transcripts for a different podcast, last 3 months136podcast-bulk-ingest @lexfridman --months 3 --output-dir lexfridman137138# Specific date window139podcast-bulk-ingest @TheAIDailyBrief --date-start 20251118 --date-end 20260818 --enrich140```141142## Output structure143144```145output-dir/146├── episode-title-slug.md # ingest-status:: downloaded on line 1147├── assets/148│ ├── report-name.pdf149│ └── blog-post.html150├── .enrichment/151│ ├── episode-slug.json # per-episode extracted sources152│ ├── all_sources.json # flat list for dedup153│ ├── unique_sources.json # deduplicated source index154│ ├── resolved_urls.json # Perplexity-resolved URLs155│ └── extraction_summary.json # stats156└── .ingest-state.json # video ID → status map157```158159## Markdown format160161```markdown162ingest-status:: downloaded163# Episode Title164165- **Date**: 2026-08-18166- **Duration**: 29:13167- **YouTube**: https://www.youtube.com/watch?v=xxxxx168169## Show Notes170...171172## Links173...174175## Sources Mentioned176177| Source | Type | Context | URL |178|--------|------|---------|-----|179...180181## Transcript182...183```184185## Relationship to other skills186187- **podcast-bulk-ingest** (this skill): One-off historical backfill. Produces188 markdown files marked `ingest-status:: downloaded`.189- **podcast-knowledge-ingest** (weekly cron): Processes files marked `downloaded`,190 extracts assertions, integrates into ontology, marks files `processed`.191- **youtube-transcript-archiver**: Interactive agent-triggered variant. Same192 download logic, different orchestration.193194## Design decisions195196- **`--dump-json` over `--print`**: YouTube descriptions contain newlines that197 corrupt line-based metadata extraction. JSON output is reliable.198- **SRT subtitle cleanup**: Strip timestamps, sequence numbers, HTML tags,199 deduplicate consecutive identical lines.200- **Streak-based early exit**: Flat playlist API returns no date ordering.201 15 consecutive old episodes triggers stop (configurable via `--old-streak`).202- **State file**: `.ingest-state.json` tracks video IDs independently of203 markdown files, enabling fast delta detection on re-runs.204- **Ingest-status marker**: Line 1 of each markdown. Values: `downloaded`205 (bulk ingest done), `pending` (weekly ingest queued), `processed:DATE:N`206 (N assertions extracted on DATE), `skipped` (no extractable assertions).207208## Prerequisites209210- `podcast-bulk-ingest` (services/podcast-ingest) and `yt-dlp` resolvable on211 PATH — the binary shells out to the `yt-dlp` console script directly (no212 Python interpreter or `PYTHONPATH` involved on this binary's side any more)213- For enrichment: Perplexity MCP tools (`perplexity_search`)214- For asset downloads: `curl` or `wget`