openpod 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- openpod-0.1.0/.gitignore +25 -0
- openpod-0.1.0/CHANGELOG.md +202 -0
- openpod-0.1.0/CONTRIBUTING.md +43 -0
- openpod-0.1.0/LICENSE +21 -0
- openpod-0.1.0/PKG-INFO +241 -0
- openpod-0.1.0/README.md +197 -0
- openpod-0.1.0/pyproject.toml +90 -0
- openpod-0.1.0/src/openpod/__init__.py +48 -0
- openpod-0.1.0/src/openpod/align.py +249 -0
- openpod-0.1.0/src/openpod/asr.py +164 -0
- openpod-0.1.0/src/openpod/brand/banner.txt +4 -0
- openpod-0.1.0/src/openpod/brand/brand-tokens.json +69 -0
- openpod-0.1.0/src/openpod/brand/card-template.html +34 -0
- openpod-0.1.0/src/openpod/bridge.py +486 -0
- openpod-0.1.0/src/openpod/briefing.py +232 -0
- openpod-0.1.0/src/openpod/captions.py +360 -0
- openpod-0.1.0/src/openpod/card.py +99 -0
- openpod-0.1.0/src/openpod/catch.py +165 -0
- openpod-0.1.0/src/openpod/cli.py +878 -0
- openpod-0.1.0/src/openpod/clip.py +502 -0
- openpod-0.1.0/src/openpod/config.py +240 -0
- openpod-0.1.0/src/openpod/crosswalk.py +205 -0
- openpod-0.1.0/src/openpod/deeplink.py +169 -0
- openpod-0.1.0/src/openpod/doctor.py +121 -0
- openpod-0.1.0/src/openpod/errors.py +153 -0
- openpod-0.1.0/src/openpod/exports.py +59 -0
- openpod-0.1.0/src/openpod/follows.py +174 -0
- openpod-0.1.0/src/openpod/identity.py +132 -0
- openpod-0.1.0/src/openpod/imports.py +172 -0
- openpod-0.1.0/src/openpod/ingest/__init__.py +8 -0
- openpod-0.1.0/src/openpod/ingest/apple.py +183 -0
- openpod-0.1.0/src/openpod/ingest/resolve.py +250 -0
- openpod-0.1.0/src/openpod/ingest/rss.py +206 -0
- openpod-0.1.0/src/openpod/ingest/spotify.py +154 -0
- openpod-0.1.0/src/openpod/ingest/validate.py +134 -0
- openpod-0.1.0/src/openpod/ingest/youtube.py +188 -0
- openpod-0.1.0/src/openpod/library.py +284 -0
- openpod-0.1.0/src/openpod/mcp_server.py +569 -0
- openpod-0.1.0/src/openpod/media.py +119 -0
- openpod-0.1.0/src/openpod/models.py +400 -0
- openpod-0.1.0/src/openpod/persona.py +358 -0
- openpod-0.1.0/src/openpod/player_manifest.json +374 -0
- openpod-0.1.0/src/openpod/scan.py +179 -0
- openpod-0.1.0/src/openpod/search/__init__.py +31 -0
- openpod-0.1.0/src/openpod/search/embed.py +68 -0
- openpod-0.1.0/src/openpod/search/index.py +212 -0
- openpod-0.1.0/src/openpod/segments.py +314 -0
- openpod-0.1.0/src/openpod/skills/__init__.py +89 -0
- openpod-0.1.0/src/openpod/skills/bring-in-my-world/SKILL.md +41 -0
- openpod-0.1.0/src/openpod/skills/catch-me-up/SKILL.md +52 -0
- openpod-0.1.0/src/openpod/skills/chapter-it/SKILL.md +25 -0
- openpod-0.1.0/src/openpod/skills/cut-the-clip/SKILL.md +65 -0
- openpod-0.1.0/src/openpod/skills/find-the-moment/SKILL.md +40 -0
- openpod-0.1.0/src/openpod/skills/follow-this/SKILL.md +24 -0
- openpod-0.1.0/src/openpod/skills/make-it-shareable/SKILL.md +63 -0
- openpod-0.1.0/src/openpod/skills/remember-this/SKILL.md +61 -0
- openpod-0.1.0/src/openpod/skills/set-up-my-persona/SKILL.md +85 -0
- openpod-0.1.0/src/openpod/skills/sharpen-my-persona/SKILL.md +33 -0
- openpod-0.1.0/src/openpod/skills/whats-new/SKILL.md +40 -0
- openpod-0.1.0/src/openpod/sync.py +875 -0
- openpod-0.1.0/src/openpod/theme.py +189 -0
- openpod-0.1.0/src/openpod/transcript.py +197 -0
- openpod-0.1.0/tests/conftest.py +79 -0
- openpod-0.1.0/tests/test_align.py +93 -0
- openpod-0.1.0/tests/test_asr_fallback_gate.py +139 -0
- openpod-0.1.0/tests/test_brand_consistency.py +203 -0
- openpod-0.1.0/tests/test_bridge.py +286 -0
- openpod-0.1.0/tests/test_caption_track.py +143 -0
- openpod-0.1.0/tests/test_captions_settings_doctor.py +181 -0
- openpod-0.1.0/tests/test_catch_and_search.py +58 -0
- openpod-0.1.0/tests/test_clip_and_cli.py +40 -0
- openpod-0.1.0/tests/test_crosswalk.py +83 -0
- openpod-0.1.0/tests/test_deeplink.py +22 -0
- openpod-0.1.0/tests/test_identity.py +116 -0
- openpod-0.1.0/tests/test_imports_skills_notes.py +222 -0
- openpod-0.1.0/tests/test_library_persona_follows.py +69 -0
- openpod-0.1.0/tests/test_persona_layers.py +120 -0
- openpod-0.1.0/tests/test_resolve_kinds.py +205 -0
- openpod-0.1.0/tests/test_scan.py +76 -0
- openpod-0.1.0/tests/test_segments.py +286 -0
- openpod-0.1.0/tests/test_summary.py +94 -0
- openpod-0.1.0/tests/test_sync.py +538 -0
- openpod-0.1.0/tests/test_theme.py +166 -0
- openpod-0.1.0/tests/test_transcript.py +47 -0
- openpod-0.1.0/tests/test_validate_and_links.py +120 -0
openpod-0.1.0/.gitignore
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*.egg-info/
|
|
5
|
+
.eggs/
|
|
6
|
+
build/
|
|
7
|
+
dist/
|
|
8
|
+
.venv*/
|
|
9
|
+
venv/
|
|
10
|
+
|
|
11
|
+
# Tooling
|
|
12
|
+
.pytest_cache/
|
|
13
|
+
.mypy_cache/
|
|
14
|
+
.ruff_cache/
|
|
15
|
+
.coverage
|
|
16
|
+
htmlcov/
|
|
17
|
+
|
|
18
|
+
# OS
|
|
19
|
+
.DS_Store
|
|
20
|
+
|
|
21
|
+
# OpenPod local libraries created while developing/testing
|
|
22
|
+
.openpod/
|
|
23
|
+
|
|
24
|
+
# Never commit internal strategy docs into the public repo
|
|
25
|
+
/Docs/
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to OpenPod are documented here. Format loosely follows
|
|
4
|
+
[Keep a Changelog](https://keepachangelog.com/); versions follow SemVer.
|
|
5
|
+
|
|
6
|
+
## [0.1.0] — 2026-07-16
|
|
7
|
+
|
|
8
|
+
First Stage 1 alpha: local-pure, pull-only.
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- **Two-layer persona: who you are vs. what this folder is for.** Global
|
|
12
|
+
(`~/.openpod/persona.md`): role, language & style, standing
|
|
13
|
+
interests/filters, what you want from long-form — true in every
|
|
14
|
+
workspace, the layer a cloud tier would sync. Workspace
|
|
15
|
+
(`.openpod/persona.md`): current projects, local amplify/filter topics,
|
|
16
|
+
and the machine-derived blocks (derived from *this* library). The MCP
|
|
17
|
+
`persona` tool returns both layers labeled — identity/language lean
|
|
18
|
+
global, relevance decisions lean workspace — and routes writes by kind.
|
|
19
|
+
Existing single-file personas get a **split proposal**: sections are
|
|
20
|
+
classified (Role → global, Current projects → local, unknowns → ask) and
|
|
21
|
+
offered as one multiple-choice confirmation — the user picks, never
|
|
22
|
+
writes prose — then `persona_split` / `openpod persona split --to-global`
|
|
23
|
+
moves exactly the confirmed sections. Machine-owned blocks are never
|
|
24
|
+
offered and never move. CLI: `persona init|show --global`, `persona
|
|
25
|
+
split`. House rule codified in the skill: users choose from options
|
|
26
|
+
mined from evidence; system files never demand elaboration.
|
|
27
|
+
- **`summary.md` — the cross-session memory artifact**: a durable, local
|
|
28
|
+
distillation of an episode, written by the agent (or user) after actually
|
|
29
|
+
engaging with it, and loaded by future sessions as context. The contract
|
|
30
|
+
is deliberately minimal: OpenPod owns placement and identity frontmatter
|
|
31
|
+
(entry_id, episode_key — sync-ready for the cloud tier, updated_at,
|
|
32
|
+
revision count); the body — structure, language, depth, relevance — is
|
|
33
|
+
entirely the author's. `write_summary(append=True)` lets sessions add
|
|
34
|
+
dated takes without losing prior ones. Surfaces: MCP `save_summary` +
|
|
35
|
+
`recall_summaries` (the "load what past sessions learned" call),
|
|
36
|
+
summary included in `get_briefing`, CLI `openpod summary` (read / write
|
|
37
|
+
/ list), and the **Remember This** skill — guidance (personalize from
|
|
38
|
+
the session, exclude what didn't matter, cite moments, budget tokens),
|
|
39
|
+
explicitly defaults-not-mandates.
|
|
40
|
+
- **No more silent Whisper fallback** — a cost-based gate keeps interaction
|
|
41
|
+
fast: every ASR run is *announced* (why captions failed, the wall-clock
|
|
42
|
+
estimate, and that it's local compute — no tokens or API cost — recorded
|
|
43
|
+
in `transcript.notes`), but the user is only *asked* when it's worth
|
|
44
|
+
asking. Caption failures are classified: permanent (no captions exist →
|
|
45
|
+
ASR just runs, announced) vs transient (429/IP throttling → a short
|
|
46
|
+
transcription under `asr.auto_threshold_seconds` (default 180s) runs with
|
|
47
|
+
a notice, a long one raises structured `captions_unavailable` with both
|
|
48
|
+
options priced — wait and re-catch for free captions, or `asr="now"` /
|
|
49
|
+
`--asr-now`). `--no-asr` never transcribes. Throttled-then-ASR
|
|
50
|
+
transcripts carry "a later re-catch may upgrade this to platform
|
|
51
|
+
captions". RSS enclosure ASR announces itself the same way. Caption
|
|
52
|
+
language follows `locale.preferred_language` (a Hebrew interview fetches
|
|
53
|
+
Hebrew captions first, then fallback, then English).
|
|
54
|
+
- **Caption data contract + export package** (naval-clipper design port —
|
|
55
|
+
contracts, not renderers):
|
|
56
|
+
- Clips carry a **structured caption track in their `.json`**: phrases
|
|
57
|
+
chunked by the karaoke rules (≤5 words / sentence / `‖` force-break —
|
|
58
|
+
the marker translated Hebrew keeps so nothing regroups it), language +
|
|
59
|
+
`rtl`, and honest `timing` (`approximate` for rolling YouTube cues,
|
|
60
|
+
`exact` only with word-level).
|
|
61
|
+
- **Word-level track** (`--word-level` on `clip` and `captions`, asr
|
|
62
|
+
extra): whisper on just the clip window/file → `*.words.json` — the
|
|
63
|
+
karaoke-highlight layer.
|
|
64
|
+
- **Burn gate**: burning refuses unverified translations and any
|
|
65
|
+
`captions_file` that fails the coverage check (dropped speech named in
|
|
66
|
+
the note); RTL burns warn to eyeball `verify.png` for missing glyphs.
|
|
67
|
+
- **Export package**: the working folder now also gets `deeplink.txt`,
|
|
68
|
+
`label.json` (label + optional `hook`), `*.words.json`, and `verify.png`
|
|
69
|
+
(start/mid/end frames of the burned derivative, tiled).
|
|
70
|
+
- **Speakers**: `{title}`/`{role}` template aliases and a workspace
|
|
71
|
+
`speakers.yaml` (keyed by show) as fallback — teach OpenPod a show's
|
|
72
|
+
hosts once.
|
|
73
|
+
- **`clip.style` setting** (default `karaoke`) reserved for renderers.
|
|
74
|
+
- **New skill: Make it Shareable** — the export pipeline end-to-end
|
|
75
|
+
(doctor first, captions as data, line-locked translation, verified burn,
|
|
76
|
+
export-folder hygiene); `cut-the-clip` hands off to it, and
|
|
77
|
+
`sharpen-my-persona` may offer `locale.preferred_language` when the
|
|
78
|
+
library skews non-English.
|
|
79
|
+
- **The two-artifact clip contract** (QA session 2026-07-09): the library
|
|
80
|
+
clip is the clean master and is never mutated; presentation — captions,
|
|
81
|
+
name-plate labels, social encodes — are **export derivatives**:
|
|
82
|
+
- **Settings schema v0** (`settings.yaml`, documented defaults merged at
|
|
83
|
+
read time): `locale.preferred_language`/`fallback_language`,
|
|
84
|
+
`clip.captions` (`off|soft|burn`), `clip.caption_language`,
|
|
85
|
+
`clip.burn_in` (default **false**), `clip.label`, `clip.label_template`,
|
|
86
|
+
`clip.export_dir`, `clip.keep_clean_master`. Unknown keys are rejected
|
|
87
|
+
with the documented list. CLI `openpod settings`, MCP `settings` tool.
|
|
88
|
+
- **Caption primitive** (`captions.py`): SRT/VTT sidecars generated from
|
|
89
|
+
`transcript.window(start, end)` — full coverage by construction — plus a
|
|
90
|
+
**coverage verify** (`openpod captions --verify`, MCP `captions`) that
|
|
91
|
+
catches dropped lines in edited/translated caption files before any
|
|
92
|
+
burn-in. Translation is flagged (`translation_needed`), never guessed.
|
|
93
|
+
- **Clip presentation flags**: `--captions off|soft|burn`, `--lang`,
|
|
94
|
+
`--label` / `--label-from-meta`, `--out DIR` (and `clip.export_dir`):
|
|
95
|
+
derivatives and copies land in the working folder; burned files land
|
|
96
|
+
*only* there. Labels render from structured `source.speakers`
|
|
97
|
+
(name/role), never from agent memory.
|
|
98
|
+
- **`openpod doctor`**: ffmpeg capability report (libass/drawtext probed
|
|
99
|
+
before any burn is promised), settings status, and library hygiene —
|
|
100
|
+
foreign files (venvs, scripts, previews) under `library/` are flagged
|
|
101
|
+
and quarantined to `library/_scratch/` with `--fix`.
|
|
102
|
+
- **cut-the-clip skill v2**: teaches the two-artifact contract — never
|
|
103
|
+
burn the master, captions from transcript with verify, labels from
|
|
104
|
+
meta, working files never in the vault.
|
|
105
|
+
- **Deterministic cross-platform resolution** — episode identity is product
|
|
106
|
+
code now, not per-session agent improvisation:
|
|
107
|
+
- **Apple Podcasts ingest** (`ingest/apple.py`): `id{show}` + `i={episode}`
|
|
108
|
+
+ country parsed from the URL, resolved via the keyless iTunes Lookup API
|
|
109
|
+
to the RSS feed and the episode's guid — the exact episode, never
|
|
110
|
+
"newest in feed".
|
|
111
|
+
- **Spotify ingest** (`ingest/spotify.py`): oEmbed title → iTunes episode
|
|
112
|
+
search → guid-anchored feed match. No HTML scraping, no open-ended web
|
|
113
|
+
search; Spotify-exclusives fail with a structured, actionable error.
|
|
114
|
+
- **Identity crosswalk** (`crosswalk.py`, `EpisodeIdentity`): per-episode
|
|
115
|
+
records under `.openpod/crosswalk/` mapping YouTube/Spotify/Apple/RSS ids
|
|
116
|
+
with per-field provenance, plus a show-level table. Resolved once, cached
|
|
117
|
+
forever; schema is user-free so it can later be served globally.
|
|
118
|
+
- **Confirmation gate**: fuzzy matches below confidence 0.8 raise/return a
|
|
119
|
+
structured `needs_confirmation` candidate instead of silently ingesting;
|
|
120
|
+
`catch(..., confirmed=true)` records the user's confirmation. Confidence
|
|
121
|
+
is corroborated (title + duration + date), never a single signal.
|
|
122
|
+
- **No silent defaults**: unclassifiable links are `unknown` and return a
|
|
123
|
+
structured `unresolved_link` error (with what was tried), instead of
|
|
124
|
+
falling through into the RSS parser.
|
|
125
|
+
- **Origin/source decoupling + capability-aware links** — `catch` persists
|
|
126
|
+
what the user pasted (`origin`) separately from the transcript source;
|
|
127
|
+
`build_link()` renders the output on the user's platform (explicit arg →
|
|
128
|
+
`preferred_playback_app` setting → origin) from a per-app capability table
|
|
129
|
+
and reports honestly when it degrades ("Apple opens the episode, not the
|
|
130
|
+
moment"). New MCP tools: `playback_link`, `set_preferred_app`.
|
|
131
|
+
- **Cross-source timestamp alignment** (`align.py`) — piecewise-constant
|
|
132
|
+
offset maps recovered by probe-and-bisect (≈10–20 fifteen-second ASR
|
|
133
|
+
probes per episode, O(log n) per ad break), cached on the crosswalk record.
|
|
134
|
+
- **Content-type validation** (`ingest/validate.py`) — HEAD + magic-byte
|
|
135
|
+
sniffing with a defined support matrix; tracking redirectors resolve to
|
|
136
|
+
their terminal URL once; unsupported content raises `unsupported_format`
|
|
137
|
+
instead of being piped into Whisper on luck.
|
|
138
|
+
- **Shared media cache + video-preserving clips** — one download per episode
|
|
139
|
+
under `.openpod/media/`; `clip` keeps video by default when the source has
|
|
140
|
+
it (`--video` / `--audio-only` on the CLI, `video=` on MCP) and reports
|
|
141
|
+
`has_video` + a capability note *before* delivering a mismatched artifact.
|
|
142
|
+
- **Follow-time show resolution** — following an Apple show URL resolves and
|
|
143
|
+
stores its RSS feed once, recorded in the crosswalk show table.
|
|
144
|
+
|
|
145
|
+
- **`catch`** — ingest a podcast/RSS/YouTube link (or local file) into a local
|
|
146
|
+
library: timed transcript, extracted key ideas, navigable TOC, and a briefing
|
|
147
|
+
scaffold for the agent to complete.
|
|
148
|
+
- **`search`** — cross-episode local search over the whole library (SQLite FTS5
|
|
149
|
+
keyword retrieval + a dependency-free local embedding re-rank).
|
|
150
|
+
- **`export_timestamps`** — emit timed segments + deep-links as JSON or Markdown.
|
|
151
|
+
- **`clip`** — sentence-snapped, local clip extraction via ffmpeg, plus a
|
|
152
|
+
shareable deep-link card. Local files only; no re-hosting.
|
|
153
|
+
- **`follow` / `digest`** — local follow list (podcast RSS + YouTube channels)
|
|
154
|
+
and a "what's new" digest polled locally. Digest items carry
|
|
155
|
+
`in_rotation`: follows the user has never caught are flagged as the
|
|
156
|
+
**discovery pool** — the What's New skill surfaces interest-matching
|
|
157
|
+
episodes from unfamiliar shows with a cheap trial (briefing or the one
|
|
158
|
+
matching beat) instead of burying them under familiar names. Persona
|
|
159
|
+
filters apply to topics, never to sources.
|
|
160
|
+
- **`persona`** — a local, user-owned `persona.md` that evolves from the
|
|
161
|
+
library. Machine-owned blocks (`## Derived from my library`, `## Imported
|
|
162
|
+
interests (opt-in)`) are regenerated in place under a marker contract that
|
|
163
|
+
provably never touches the human-authored sections.
|
|
164
|
+
- **`import`** — opt-in OPML import: stages the raw export verbatim in
|
|
165
|
+
`.openpod/imports/`, merges subscriptions into `follows.yaml` de-duplicated
|
|
166
|
+
and tagged with `source:` provenance, and refreshes the persona's
|
|
167
|
+
imported-interests block. Snapshot, not sync; no OAuth, no cloud APIs.
|
|
168
|
+
- **`note`** — append to an episode's user-owned `notes.md` (also exposed to
|
|
169
|
+
agents as `append_note`, append-on-request only).
|
|
170
|
+
- **`persona scan`** — a fast, bounded, read-only workspace evidence sweep
|
|
171
|
+
(project folders by recency, markdown doc titles, `CLAUDE.md` headings,
|
|
172
|
+
library themes, follows) powering a **guess-and-confirm interview**: the
|
|
173
|
+
agent presents multi-select options mined from the evidence and the user
|
|
174
|
+
picks instead of typing. Extra roots are opt-in only. The trust-question
|
|
175
|
+
was retired (follows/OPML answer it); all interview questions are now
|
|
176
|
+
multi-select guesses with a free-text escape hatch.
|
|
177
|
+
- **Skills catalog** — nine versioned `SKILL.md` bundles shipped in the package
|
|
178
|
+
(Catch Me Up, Set Up My Persona, Bring In My World, Sharpen My Persona, Find
|
|
179
|
+
the Moment, Cut the Clip, What's New, Chapter It, Follow This); `openpod
|
|
180
|
+
skills` lists them and the MCP server exposes them as prompts.
|
|
181
|
+
- **MCP server** (`openpod-mcp`) exposing the primitives to AI agents, plus
|
|
182
|
+
`list_entries`, `append_note`, and `import_opml`; every tool result carries
|
|
183
|
+
the entry id and the path(s) touched.
|
|
184
|
+
- **CLI UX contract** — mutating commands print the path(s) they wrote;
|
|
185
|
+
read/produce commands take `--json` shaped identically to the MCP tools;
|
|
186
|
+
errors name the fix; `catch` on a persona-less workspace suggests "Set Up My
|
|
187
|
+
Persona".
|
|
188
|
+
- Transcript parsers for WebVTT, SubRip, YouTube `json3`, and cue lists.
|
|
189
|
+
- Deep-link construction for YouTube, Spotify, and open podcast enclosures.
|
|
190
|
+
- **The anchor ladder** — every idea, search hit, and TOC entry carries up to
|
|
191
|
+
three labeled deep-links, because different readers want different
|
|
192
|
+
landings: the creator's **chapter** ("take me to the topic"), the detected
|
|
193
|
+
**beat** where the idea starts being articulated ("play the argument from
|
|
194
|
+
its start"), and the exact **moment** ("show me where they said it").
|
|
195
|
+
Beats come from creator chapters when the source ships them (YouTube
|
|
196
|
+
chapter markers, or timestamp lists parsed from the description) — with
|
|
197
|
+
over-long chapters sub-segmented by local lexical-cohesion topic detection
|
|
198
|
+
— and from pure topic detection otherwise. No link ever lands mid-sentence
|
|
199
|
+
without a labeled alternative. Beats persist in `meta.json`; hits carry
|
|
200
|
+
`chapter_*`, `segment_*`, and `deeplink` fields; `ideas.md` renders the
|
|
201
|
+
ladder with a one-line legend.
|
|
202
|
+
- MIT license; offline test suite; CI across Python 3.10–3.13.
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Contributing to OpenPod
|
|
2
|
+
|
|
3
|
+
Thanks for helping! OpenPod is a local-first tool with a few hard invariants —
|
|
4
|
+
please keep them in mind, they're the whole point of the project.
|
|
5
|
+
|
|
6
|
+
## The invariants (non-negotiable)
|
|
7
|
+
|
|
8
|
+
1. **Local-pure.** No code path may upload the user's audio, transcripts,
|
|
9
|
+
library, or persona to any server. If a feature needs a server, it's Stage 2
|
|
10
|
+
and doesn't belong here.
|
|
11
|
+
2. **Pull, not push.** No background processes, no always-on monitoring.
|
|
12
|
+
3. **Artifacts, not telemetry.** Persist what the user explicitly produces.
|
|
13
|
+
Never capture behavioral signal (play/skip/replay).
|
|
14
|
+
4. **Navigate, don't re-host.** Sharing is deep-links to the moment in the
|
|
15
|
+
native player, never republished audio/video.
|
|
16
|
+
|
|
17
|
+
If a change touches these, open an issue first.
|
|
18
|
+
|
|
19
|
+
## Dev setup
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
python -m venv .venv && . .venv/bin/activate
|
|
23
|
+
pip install -e '.[dev]'
|
|
24
|
+
pytest
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
The test suite must stay **fully offline** — no network calls, no model
|
|
28
|
+
downloads. Use fixtures (see `tests/conftest.py`). Heavy/network dependencies
|
|
29
|
+
(`yt-dlp`, `youtube-transcript-api`, `faster-whisper`, `mcp`) are optional
|
|
30
|
+
extras and must be imported lazily so the core installs and tests without them.
|
|
31
|
+
|
|
32
|
+
## Style
|
|
33
|
+
|
|
34
|
+
- Standard library first; add a dependency only when it clearly earns its place.
|
|
35
|
+
- Keep the public surface small (`catch`, `clip`, `export_timestamps`, `search`,
|
|
36
|
+
plus `follow`/`persona`).
|
|
37
|
+
- New optional integrations go behind an extra in `pyproject.toml` and a lazy
|
|
38
|
+
import with a clear "install `openpod[...]`" error message.
|
|
39
|
+
|
|
40
|
+
## License
|
|
41
|
+
|
|
42
|
+
By contributing you agree your contributions are licensed under
|
|
43
|
+
**MIT**, the project's license.
|
openpod-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Yoav Dori
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
openpod-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: openpod
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Local-first CLI + MCP server that turns long-form audio/video into personalized, cited, navigable briefings — everything runs on your machine.
|
|
5
|
+
Project-URL: Homepage, https://github.com/openpodhq/openpod
|
|
6
|
+
Project-URL: Repository, https://github.com/openpodhq/openpod
|
|
7
|
+
Author: OpenPod contributors
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: ai-agent,cli,local-first,mcp,podcast,transcript,youtube
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Topic :: Multimedia :: Sound/Audio
|
|
17
|
+
Classifier: Topic :: Text Processing
|
|
18
|
+
Requires-Python: >=3.10
|
|
19
|
+
Requires-Dist: feedparser>=6.0
|
|
20
|
+
Requires-Dist: pyyaml>=6.0
|
|
21
|
+
Provides-Extra: all
|
|
22
|
+
Requires-Dist: faster-whisper>=1.0; extra == 'all'
|
|
23
|
+
Requires-Dist: mcp>=1.2; extra == 'all'
|
|
24
|
+
Requires-Dist: numpy>=1.24; extra == 'all'
|
|
25
|
+
Requires-Dist: playwright>=1.40; extra == 'all'
|
|
26
|
+
Requires-Dist: youtube-transcript-api>=0.6; extra == 'all'
|
|
27
|
+
Requires-Dist: yt-dlp>=2024.1; extra == 'all'
|
|
28
|
+
Provides-Extra: asr
|
|
29
|
+
Requires-Dist: faster-whisper>=1.0; extra == 'asr'
|
|
30
|
+
Provides-Extra: bridge
|
|
31
|
+
Provides-Extra: card-png
|
|
32
|
+
Requires-Dist: playwright>=1.40; extra == 'card-png'
|
|
33
|
+
Provides-Extra: dev
|
|
34
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
35
|
+
Provides-Extra: embeddings
|
|
36
|
+
Requires-Dist: numpy>=1.24; extra == 'embeddings'
|
|
37
|
+
Provides-Extra: mcp
|
|
38
|
+
Requires-Dist: mcp>=1.2; extra == 'mcp'
|
|
39
|
+
Provides-Extra: sync
|
|
40
|
+
Provides-Extra: youtube
|
|
41
|
+
Requires-Dist: youtube-transcript-api>=0.6; extra == 'youtube'
|
|
42
|
+
Requires-Dist: yt-dlp>=2024.1; extra == 'youtube'
|
|
43
|
+
Description-Content-Type: text/markdown
|
|
44
|
+
|
|
45
|
+
<p align="center">
|
|
46
|
+
<picture>
|
|
47
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/openpodhq/openpod/main/assets/brand/wordmark-blue-dark-636.png">
|
|
48
|
+
<img src="https://raw.githubusercontent.com/openpodhq/openpod/main/assets/brand/wordmark-blue-636.png" alt="OpenPod" width="212">
|
|
49
|
+
</picture>
|
|
50
|
+
</p>
|
|
51
|
+
|
|
52
|
+
<p align="center">
|
|
53
|
+
<img src="https://raw.githubusercontent.com/openpodhq/openpod/main/assets/demo.gif" alt="An AI agent pulls the minutes that matter from hundreds of hours across the shows you follow — every hit timestamped to verify" width="720">
|
|
54
|
+
</p>
|
|
55
|
+
|
|
56
|
+
# OpenPod
|
|
57
|
+
|
|
58
|
+
**Pull the ten minutes that matter — to you.**
|
|
59
|
+
|
|
60
|
+
[](https://pypi.org/project/openpod/) [](https://pypi.org/project/openpod/) [](https://github.com/openpodhq/openpod/actions) [](LICENSE) [](#nothing-leaves-your-machine) [](https://modelcontextprotocol.io)
|
|
61
|
+
|
|
62
|
+
Hundreds of hours pile up across the shows you follow. OpenPod pulls out the minutes you're chasing — cited, timestamped to verify, extracted on your own machine — as the starting point your AI agent works from.
|
|
63
|
+
|
|
64
|
+
OpenPod is a local-first agent toolkit — primitives plus packaged skills — that your agent drives over MCP or the CLI. It connects to nothing external; the only corpus is your library on disk.
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
pip install openpod
|
|
68
|
+
openpod catch "https://example.com/podcast/feed.xml"
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
**Catch your first episode** — one command, about a minute. You get a timestamped transcript, the key ideas, and jump-to-moment links, all as plain files in a folder you own.
|
|
72
|
+
|
|
73
|
+
`0 servers · 0 accounts · 0 telemetry · $0 to run · test suite runs fully offline`
|
|
74
|
+
|
|
75
|
+
> **Status:** Stage 1 alpha. Local-pure, pull-only. It does what this page shows — nothing more yet.
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## The problem
|
|
80
|
+
|
|
81
|
+
You subscribe to twelve shows. You're forty episodes behind, every one is three hours long, and the one segment you actually needed last week — you can't find it, because nobody remembers a timestamp. So the backlog grows, the guilt compounds, and "I'll listen on the weekend" becomes a lie you tell yourself on Mondays.
|
|
82
|
+
|
|
83
|
+
Cloud summarizers answer this by asking you to upload what you listen to — and to trust their summary with no way to check it. OpenPod doesn't summarize your listening on someone else's computer. It gives **your** agent the ability to listen, cite, and link — locally.
|
|
84
|
+
|
|
85
|
+
## What you get
|
|
86
|
+
|
|
87
|
+
Point OpenPod (or your agent) at an RSS feed or a YouTube link. In return:
|
|
88
|
+
|
|
89
|
+
- **A briefing you can verify.** Every claim carries a timestamp and a deep link. Don't trust the summary? Click and hear the source in the native player. OpenPod never re-hosts anyone's audio — it navigates you to the moment.
|
|
90
|
+
- **A library that compounds.** Everything you catch lands in `.openpod/` as plain files — transcripts, briefings, ideas, notes, clips. Searchable across everything you've ever caught, keyword and semantic, all local.
|
|
91
|
+
- **Personalized by your own agent.** OpenPod reads your local `persona.md`, so your AI foregrounds the decisions, tools, and numbers *you* care about and drops the rest — the same episode briefs differently for you than for the next person, and no cloud model ever sees who you are.
|
|
92
|
+
|
|
93
|
+
## Nothing leaves your machine
|
|
94
|
+
|
|
95
|
+
There is no OpenPod server, no account, no telemetry — there is nothing to opt out of, because nothing phones home. The only corpus is your own library on disk. Publisher transcripts and captions are used when they exist; otherwise audio is transcribed locally with Whisper. Your agent brings the intelligence; OpenPod supplies extraction, structure, citations, deep links, and local search. That's the whole trade — and it's why running it costs nothing.
|
|
96
|
+
|
|
97
|
+
## Your agent can drive all of it
|
|
98
|
+
|
|
99
|
+
OpenPod's toolkit is exposed as an [MCP](https://modelcontextprotocol.io) server, so Claude Code, Cowork, Codex — any MCP client — can catch, search, clip, and brief by name:
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
openpod-mcp
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
```json
|
|
106
|
+
{
|
|
107
|
+
"mcpServers": {
|
|
108
|
+
"openpod": { "command": "openpod-mcp", "env": { "OPENPOD_HOME": "/path/to/your/workspace" } }
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Say *"catch me up on this episode"* and the agent runs `catch`, reads your local persona file, and writes a cited briefing back into your library — then hands you the path. Mid-conversation, no dashboard, no copy-paste.
|
|
114
|
+
|
|
115
|
+
## The four primitives
|
|
116
|
+
|
|
117
|
+
| Tool | Does | Writes |
|
|
118
|
+
|---|---|---|
|
|
119
|
+
| `catch <link>` | Ingest → transcribe → structure → brief | `transcript.json`, `ideas.md`, `briefing.md` |
|
|
120
|
+
| `search <query>` | Retrieve across your local library | ranked cues with deep links |
|
|
121
|
+
| `export_timestamps <entry>` | Navigable TOC with deep links | JSON / Markdown |
|
|
122
|
+
| `clip <entry> <start> <end>` | Sentence-snapped local cut + shareable deep-link card | media in `clips/`, card |
|
|
123
|
+
|
|
124
|
+
Every mutating command prints the path it wrote — the output of an action is the address of the artifact it produced. Every read command takes `--json`, shaped identically to the matching MCP tool.
|
|
125
|
+
|
|
126
|
+
## Skills — the features, by name
|
|
127
|
+
|
|
128
|
+
Primitives are the API; **skills are the product.** Each is a versioned instruction bundle (`openpod skills` lists them; the MCP server exposes them as prompts) that tells an agent how to compose the primitives and report back:
|
|
129
|
+
|
|
130
|
+
| Skill | You ask | It writes |
|
|
131
|
+
|---|---|---|
|
|
132
|
+
| **Catch Me Up** | "Brief me on this episode" | `briefing.md`, `ideas.md` |
|
|
133
|
+
| **Find the Moment** | "Where did they talk about X?" | deep-linked hits |
|
|
134
|
+
| **What's New** | "What dropped in my feeds?" | digest |
|
|
135
|
+
| **Cut the Clip** | "Pull the shareable minute" | media + share card |
|
|
136
|
+
| **Chapter It** | "Give me a navigable TOC" | timestamps (md/json) |
|
|
137
|
+
| **Follow This** | "Keep me on this show" | `follows.yaml` entry |
|
|
138
|
+
| **Set Up My Persona** | "Learn who I am" | `persona.md` — yours, local |
|
|
139
|
+
| **Sharpen My Persona** | "Update what you know about me" | persona Derived block |
|
|
140
|
+
| **Bring In My World** | "Import my subscriptions" | `imports/`, `follows.yaml` |
|
|
141
|
+
|
|
142
|
+
## More things it does
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
# Already have a transcript? Skip the network entirely.
|
|
146
|
+
openpod catch "https://example.com/ep1" --kind podcast --transcript ./ep1.vtt
|
|
147
|
+
|
|
148
|
+
# Search everything you've ever caught (keyword + local semantic)
|
|
149
|
+
openpod search "what did they say about raft consensus"
|
|
150
|
+
|
|
151
|
+
# Follow feeds locally; see what's new on your schedule — nothing polls in the background
|
|
152
|
+
openpod follow "https://example.com/feed.xml"
|
|
153
|
+
openpod digest
|
|
154
|
+
|
|
155
|
+
# Cut a local, sentence-snapped clip (needs ffmpeg) + a shareable deep-link card
|
|
156
|
+
openpod clip "test-pod/episode-one-consensus" 320 385
|
|
157
|
+
|
|
158
|
+
# Import subscriptions from a file you export (OPML) — point-in-time, opt-in
|
|
159
|
+
openpod import ~/Downloads/overcast.opml
|
|
160
|
+
|
|
161
|
+
# Annotate; notes feed future personalization
|
|
162
|
+
openpod note "test-pod/episode-one-consensus" "the Raft segment matters for our design"
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
Everything lands in a plain, user-owned tree:
|
|
166
|
+
|
|
167
|
+
```
|
|
168
|
+
.openpod/
|
|
169
|
+
persona.md # who you are (evolving, local)
|
|
170
|
+
follows.yaml # subscribed RSS + YouTube channels
|
|
171
|
+
library/<show>/<episode>/
|
|
172
|
+
transcript.json briefing.md ideas.md notes.md clips/
|
|
173
|
+
index/ # local search index (SQLite FTS + embeddings)
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
Want it on two machines? Put the tree in git or Dropbox yourself. OpenPod won't sync it for you.
|
|
177
|
+
|
|
178
|
+
## Install options
|
|
179
|
+
|
|
180
|
+
```bash
|
|
181
|
+
pip install openpod # core: RSS ingest, local search, deep links, clip
|
|
182
|
+
pip install 'openpod[youtube]' # + YouTube captions & audio
|
|
183
|
+
pip install 'openpod[asr]' # + local Whisper transcription
|
|
184
|
+
pip install 'openpod[mcp]' # + the MCP server
|
|
185
|
+
pip install 'openpod[all]' # everything
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
`ffmpeg` on your `PATH` is required for `clip`.
|
|
189
|
+
|
|
190
|
+
## Use it as a library
|
|
191
|
+
|
|
192
|
+
```python
|
|
193
|
+
from openpod import catch, search
|
|
194
|
+
|
|
195
|
+
result = catch("https://example.com/ep1", transcript_path="ep1.vtt", kind="podcast")
|
|
196
|
+
print(result.entry_id, len(result.transcript), result.ideas)
|
|
197
|
+
|
|
198
|
+
for hit in search("consensus algorithms"):
|
|
199
|
+
print(hit.show, hit.start, hit.deeplink)
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
## Trust & posture FAQ
|
|
203
|
+
|
|
204
|
+
**Does OpenPod download or re-host other people's content?**
|
|
205
|
+
It navigates, it doesn't republish. Deep links open the moment in YouTube, Spotify, or the open podcast enclosure. Podcast RSS — content published for open distribution — is the primary surface; YouTube support is an optional module that uses official captions, or local transcription of audio you can already access. OpenPod never touches DRM, paywalls, or age gates.
|
|
206
|
+
|
|
207
|
+
**What data do you collect about me?**
|
|
208
|
+
None. There is no endpoint to send it to.
|
|
209
|
+
|
|
210
|
+
**How is transcription chosen?**
|
|
211
|
+
Publisher-provided timed transcripts and captions first — free, fast, good for navigation (±2–4s cue timing). Otherwise local Whisper (`asr` extra). Nothing is uploaded either way. `clip` snaps cuts to cue boundaries so clips land on sentences.
|
|
212
|
+
|
|
213
|
+
**Why MIT?**
|
|
214
|
+
[MIT](LICENSE): maximally permissive and frictionless — install it at work without tripping a license policy, embed it, build on it, no obligations. OpenPod isn't protected by its license; it's protected by being local-pure and simple. No copyleft, no lock-in.
|
|
215
|
+
|
|
216
|
+
**Can I hack on it without heavy models or network?**
|
|
217
|
+
Yes — the test suite runs fully offline against fixtures.
|
|
218
|
+
|
|
219
|
+
Built by Yoav — building OpenPod in public.
|
|
220
|
+
|
|
221
|
+
## Development
|
|
222
|
+
|
|
223
|
+
```bash
|
|
224
|
+
git clone https://github.com/openpodhq/openpod
|
|
225
|
+
cd openpod
|
|
226
|
+
python -m venv .venv && . .venv/bin/activate
|
|
227
|
+
pip install -e '.[dev]'
|
|
228
|
+
pytest
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
Contributions that package or connect OpenPod travel furthest: Homebrew formula, Docker image, MCP client recipes, note-app bridges. Open an issue first; response is fast.
|
|
232
|
+
|
|
233
|
+
---
|
|
234
|
+
|
|
235
|
+
**Long-form is a lot. OpenPod pulls the ten minutes that matter — to you — with a link straight to every one.**
|
|
236
|
+
|
|
237
|
+
```bash
|
|
238
|
+
pip install openpod
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
If it saved you an hour this week, [star the repo](../../stargazers) — stars are how the next person finds it.
|