transcript-analyzer
Ingest Granola + Pocket AI transcripts, extract insights with a local LLM, store in Obsidian, browse + chat via a local dashboard.
transcript-analyzer
Personal system that ingests your Granola + Pocket AI transcripts, extracts insights with the Claude API, stores them as notes in your Obsidian vault, and — the important part — pushes synthesis back into the vault: a daily digest, a live commitment tracker, dossiers on the people you talk to repeatedly, research-study rollups, and prep notes for tomorrow's meetings. A local dashboard surfaces the synthesis as a CEO briefing (digest, commitments, people, prep) and lets you ask questions with citations.
Every recording gets a detailed summary you can read straight through instead of a two-line abstract. Recordings the model classifies as lectures get more: study-grade notes with real, rendered diagrams and a printable PDF, written into the vault beside the transcript note.
Privacy note: transcript storage is local (your Obsidian vault + a local SQLite index), but analysis is not — transcripts are sent to the Anthropic API for insight extraction, synthesis, and chat. If a conversation includes other people (interviewees, colleagues), their words are sent too. Don't enable this system for conversations where that's not acceptable.
How it works
Granola API ──┐ ┌─> Obsidian notes (source of truth)
├─> sync ──> Claude API ────────┤
Pocket API ──┘ (junk filter -> insights) └─> SQLite index (derived by parsing notes)
│
lecture? ──> study-notes pass (Opus 5) ──────────┤
• Study Notes/<stem>.md │
• Study Notes/<stem>.pdf │
│
daily synthesis (Claude API) ──────┤
• Digests/YYYY-MM-DD.md │
• Digests/Commitments.md │
• People/<Name>.md (dossiers) │
• Studies/<Name>.md (rollups) │
• Prep/<date> <mtg>.md │
│
FastAPI dashboard (localhost:8787)
• today’s digest • commitments • people
• prep • lectures • chat • browse
- Pocket AI and Granola are pulled via their official public APIs (incremental, with
created_athigh-water marks;--forcefor a full resync). - A junk filter drops test recordings and background noise at ingest, before any billable LLM call.
- Insight notes are written flat, organized by recording date into
Transcript Insights/(the canonical store). Each note’s title is a descriptive one-liner with a long date (e.g.Pricing deck review with Angela, July 1st, 2026). Attendee emails are persisted in frontmatter — they're the stable person-identity key that powers dossiers and meeting prep. - Synthesis runs at most once per day (not per sync cycle) and writes only into
Digests/,People/,Studies/, andPrep/— inside managed regions (<!-- synth:begin -->…<!-- synth:end -->), so anything you write outside the markers survives regeneration. Every LLM claim must carry a verbatim quote from the conversation it cites; claims that fail this gate are dropped in code, not trusted from the prompt. - The dashboard home is a CEO briefing: it reads those synthesis notes (plus live commitments from the index), links every claim back to its source conversation, and can trigger synthesis from the UI.
- Commitments are pure extraction (no LLM): every unchecked
- [ ]across your notes, linked back. Tick the box in the conversation note to close one.- Chat is agentic retrieval: Claude reads every conversation summary in context (no embeddings, no top-k) and pulls full transcripts on demand — speaker labels, dates, and proper nouns stay exact. - Pocket audio is downloaded into
Transcript Insights/Attachments/and embedded in each note. Transcript lines include[M:SS]timestamps you can click in the dashboard to seek the recording. Granola's API exposes no audio (timestamps still appear for reference). - Every note carries two summaries. The note body shows the long one, sized to the
recording (~150 words for a short call, 600–900 for an hour meeting, full study-note
treatment for a lecture). A one-paragraph
abstract:stays in frontmatter as the retrieval field: digests, dossiers, rollups and chat all read it, and chat sends every abstract on every question, so that is the field that has to stay small. - Each note is regenerated inside managed markers
(
<!-- transcript-analyzer:begin -->…:end). Anything you write below the end marker survives, and a checkbox you ticked stays ticked.
Lectures
The extraction pass classifies each recording (lecture / meeting / interview /
personal) in the same call that writes the summary, so detection costs nothing extra. There
is no course registry: the model names the course, and the code is normalized against the
courses already in the vault, so week 2 of 21-241 rejoins week 1 even when spelled 21241.
A lecture then buys a second pass (Opus 5 by default) that writes into
Transcript Insights/Study Notes/:
- Grounded, and separated from what is not. Every section carries a verbatim span from the transcript and is dropped if that span does not string-match; so is every "assigned or examinable" claim. Gap-filling background is allowed but never blended in — it renders in its own clearly marked Background (not from lecture) block.
- Real diagrams, never generated images. The model emits diagram specs — Mermaid for flows and timelines, KaTeX for math, fenced code for SML — which are rendered deterministically in the Playwright Chromium. A diagram that fails to render is dropped, from the PDF and the markdown alike. Nothing is faked to stand in for it.
- ASR repairs are auditable. Lecture audio is a single mic with no speaker labels; a real 15150 transcript says "this new age of AR" where the professor said AI. Repairs are allowed, and each one is listed in the note's frontmatter with the span it replaced — a repair whose span is not in the transcript is dropped.
- The PDF is linked from the transcript note, from the study note, and from the dashboard's Lectures page. Regeneration is idempotent, and both files follow the note if it is renamed.
PDF rendering needs the Playwright Chromium (pip install -e '.[pdf]' && playwright install chromium). Without it the markdown study notes are still written; set [lecture] pdf = false
to skip it deliberately.
Cost guard
This runs unattended against a paid API, so the guards are hard, not advisory:
[anthropic] monthly_budget_usd— spend ledger in SQLite; calls stop at the ceiling.[anthropic] max_calls_per_run— bounds any single sync/synthesis run.- Kill switch:
touch data/llm.killstops all API calls immediately; delete to resume. GET /healthon the dashboard shows this month's spend.
Expected cost at ~50 conversations/month with Opus 4.8: roughly $4–6/month. A lecture adds one Opus 5 study-notes call on top of its extraction.
Models and reasoning effort are set per stage, not globally
([anthropic.stage_models] / [anthropic.stage_effort]): extraction runs at low effort
because it only fills in a schema, and the study-notes pass gets the expensive model. Thinking
is on by default on Opus 5 and billed as output, so an unpinned stage is a silent cost.
Setup
cd transcript-analyzer
python3 -m venv .venv
./.venv/bin/pip install -e '.[dev]'
cp config.example.toml config.toml # then edit config.toml
Edit config.toml:
[vault] path/name— your Obsidian vault.[anthropic] api_key— your Claude API key (or setANTHROPIC_API_KEY). Default model isclaude-opus-4-8; setmodel = "claude-sonnet-5"to cut costs ~60%.[granola] token/[pocket] api_key— source API keys (leave blank to skip a source).[synthesis]— setself_names/self_emailsso you don't get a dossier on yourself, and declare[[synthesis.studies]]blocks for research rollups.[calendar] ics_url— optional secret ICS feed for tomorrow's-meeting prep notes.
Usage
# One-off sync (all configured sources; runs synthesis if it hasn't run today)
./.venv/bin/python scripts/run_sync.py
# Just Pocket, first 3, dry run (no writes) — good for a first test
./.venv/bin/python scripts/run_sync.py --source pocket --limit 3 --dry-run
# Synthesis on demand
./.venv/bin/python scripts/synthesize.py # all steps
./.venv/bin/python scripts/synthesize.py --only digest # one step
./.venv/bin/python scripts/synthesize.py --force # ignore change detection
# Start the dashboard
./.venv/bin/python -m transcript_analyzer.web.app
# -> http://127.0.0.1:8787
Categorizing (on demand)
Notes are organized by date. To group them into categories you choose, run:
./.venv/bin/python scripts/categorize.py Fundraising Hiring Product Personal
# With descriptions (steers matching):
./.venv/bin/python scripts/categorize.py \
"Fundraising: LP updates, term sheets, raise strategy" \
"Hiring: interviews, offer loops, recruiting debriefs"
Claude uses each category’s description to place notes, synthesizes a scoped
briefing (themes + open threads) per category, and writes non-destructive
Category notes under Transcript Insights/Categories/. Re-run anytime with a
different list; --reset clears it. The Browse page has the same UI.
Migrating a vault written before titles + timestamps
Both are one-shot; each supports --dry-run and --limit so you can check a
handful first, and both are safe to re-run: retitle_notes.py keeps a headline
a note already has, and backfill_timestamps.py skips a transcript that already
carries [M:SS] lines. --force overrides either.
# Retitle existing notes. Uses Claude by default; --cheap derives the headline
# from the note's summary with no API calls. Renames files + matching audio.
./.venv/bin/python scripts/retitle_notes.py --limit 5 --dry-run
./.venv/bin/python scripts/retitle_notes.py
# Re-summarize existing notes into the detailed-summary shape (and give
# existing lectures study notes + PDFs). DRY RUN BY DEFAULT; --apply writes,
# after copying every note it touches to data/backfill-backups/<timestamp>/.
python scripts/backfill_summaries.py # see what it would do
python scripts/backfill_summaries.py --apply --limit 5 # try five first
python scripts/backfill_summaries.py --apply --batch --max-calls 200
# Re-fetch timed segments from Pocket/Granola and rewrite the ## Transcript
# section in place. No insight LLM calls.
./.venv/bin/python scripts/backfill_timestamps.py --source pocket --dry-run
./.venv/bin/python scripts/backfill_timestamps.py
A backfill note skipped with "N sync_state rows point at this note" cannot be
traced to a single recording, so it is left alone rather than filled in with
someone else's transcript; scripts/backfill_timestamps.py --help describes how
to resolve it by hand.
Background automation (launchd)
bash scripts/install_launchd.sh # sync every [sync.interval_seconds]; dashboard always on
launchctl list | grep transcript # verify
bash scripts/install_launchd.sh uninstall
Layout
src/transcript_analyzer/connectors/—pocket_api.py,pocket.py(vault fallback),granola.pysrc/transcript_analyzer/pipeline/—llm.py(Claude API + cost guard),quality.py(junk filter),insights.py,synthesize.py(digest/dossiers/studies/prep),organize.py,indexer.pysrc/transcript_analyzer/pipeline/lecture.py— the lecture profile: study-notes prompt pack, the citation/ASR/diagram gates, and the write order that keeps the PDF and the markdown in stepsrc/transcript_analyzer/pipeline/batch.py— Message Batches API (half price) for the backfillsrc/transcript_analyzer/pipeline/citations.py— the one verbatim-quote match, shared by the synthesis citation gate and the lecture gatessrc/transcript_analyzer/courses.py— course identity for lectures, with no registry in configsrc/transcript_analyzer/render/—study.py(markdown + HTML, pure),pdf.py(Playwright, and the gate that drops diagrams that will not draw),assets.py(local mermaid/KaTeX mirror)src/transcript_analyzer/obsidian/writer.py— transcript notes + managed-region synthesis writessrc/transcript_analyzer/titles.py— note titles (headline + long date), shared by the writer, indexer and insight extractionsrc/transcript_analyzer/transcript_fmt.py—[M:SS]transcript lines, shared by both connectors and the dashboard's seek linkssrc/transcript_analyzer/calendar_feed.py— ICS feed for meeting prepsrc/transcript_analyzer/sync.py— orchestrator (--source,--limit,--dry-run,--force,--no-synthesis)src/transcript_analyzer/rag.py— agentic retrieval chat with citationssrc/transcript_analyzer/web/— FastAPI briefing dashboard (reads synthesis notes + triggers runs)tests/— pytest suite (citation gate, cost guard, junk filter, indexer guards, managed regions)data/— SQLite index, spend ledger, logs,llm.killswitch (gitignored)
Safety notes
- The indexer only reads flat notes with a
transcript_idin frontmatter and skips the synthesis folders explicitly — synthesis output is never re-ingested (no feedback loop). - Synthesis never edits transcript notes; it writes only inside its own namespaces, and only between its own markers.
- Insight-extraction failures are counted (
insight_failures_totalin the meta table) and surfaced in sync logs — an LLM failure never silently writes an empty note. - A response cut off at the model's output cap is deterministic — the same recording would
overflow again every cycle — so it is recorded instead of retried, and billed once. The note
is written with a visible warning callout and a queryable
extract_error:/study_notes_error:key, keeping whatever the last complete pass wrote (summary, key points, action items, and the filename) rather than downgrading it. - Nothing is written, renamed or deleted at a vault path that is not provably that
transcript's own. A study note carries
transcript_id; the PDF beside it is claimed through that note, exactly as an mp3 inAttachments/is claimed through the note at its stem. - The study-notes pass is an upgrade, never a precondition: if it fails, the note is still written with its detailed summary.
scripts/backfill_summaries.pyis on demand only, dry-run by default, backs up every note it rewrites outside the vault, and skips any note whosetranscript_iddoes not match.
Tests
./.venv/bin/python -m pytest
How to Install
- Download the dashboard markdown file from GitHub
- Drop it into your vault (anywhere)
- Install the Homepage plugin and point it at the file
- Enable any listed CSS snippets for the intended look
Stats
Stars
0
Forks
0
Last updated 1mo ago
Categories