Skip to main content

spoken-md

A podcast transcript API returns a podcast episode as text in one request, instead of as audio. There are two kinds. A retrieval API hands back the transcript a show has already published, in seconds and for cents. A transcription API generates one from the audio, in minutes and for dollars, with speakers labelled "Speaker 0".

Spoken is the first kind. This package is its Python client: any published podcast episode as Markdown with real speaker names and timestamps, one call, no dependencies outside the standard library.

pip install spoken-md
from spoken_md import Spoken

spoken = Spoken()                                  # SPOKEN_API_KEY from the environment, else the demo key
episode = spoken.search("huberman sleep")[0]       # search by text, or paste a Spotify or YouTube link
transcript = spoken.transcript(episode.id)         # Markdown, 1 credit the first time, free after that
print(transcript)
**Andrew Huberman** (0:00)
Welcome to the Huberman Lab podcast, where we discuss science and
science-based tools for everyday life. Today my guest is Dr. Matt Walker...

**Matt Walker** (0:45)
Thank you for having me, Andrew.

The demo key pt_demo needs no signup. It searches and lists any show, and fetches the demo episode. A key of your own comes from spoken.md: 100 transcripts for $15, credits never expire, errors are never charged.

A whole show

Listing a show's episodes is free, and every id it returns has a transcript. archive walks the list, skips what you already hold, and yields the episode with transcript=None for the rare episode that has no transcript today.

from pathlib import Path
from spoken_md import Spoken

spoken = Spoken("pt_your_key")
show = spoken.episodes(episode.podcast_id)          # podcast_id comes with every search result
out = Path(show.podcast)
out.mkdir(exist_ok=True)

done = {p.stem for p in out.glob("*.md")}            # re-running only fetches what is missing
for item in spoken.archive(show.podcast_id, skip=done):
    if item.transcript:
        (out / f"{item.episode.id}.md").write_text(item.transcript.markdown)

A 300-episode show is 300 credits. Re-running it later costs nothing for the episodes already on disk, and a scheduled run picks up new episodes as they publish.

From the terminal

The package installs a spoken-md command with the same verbs.

export SPOKEN_API_KEY=pt_your_key            # or --key, or leave it unset for the demo key

spoken-md search "acquired costco"           # id, date, show, title per line; --json for JSON
spoken-md episodes 1050462261                # every fetchable episode of a show, free
spoken-md transcript 1000625088063 > costco.md
spoken-md archive 1050462261 -o acquired/    # one .md per episode, resumable
spoken-md balance
spoken-md following                          # the shows this key is kept current on
spoken-md follow 1050462261                  # declare a follow, or clear a mute
spoken-md new                                # unfetched episodes on those shows: id, date, show, title

Keeping a folder current

Every charged fetch makes its show a follow, and new() lists what those shows have published that you have not fetched. A scheduled run of this is the whole job:

inbox = Path("inbox")
for episode in spoken.new():                  # newest first, across every followed show
    (inbox / f"{episode.id}.md").write_text(spoken.transcript(episode.id).markdown)

Each fetch raises that show's floor, so the next run lists only what came after. Shows you never fetched from can be declared with spoken.follow(podcast_id); spoken.unfollow() mutes one so later fetches do not re-add it.

Errors

Every non-2xx response raises a typed error, so a loop can decide without reading a body.

Error Status Meaning Charged Retry
AuthError 401 Missing or invalid key. Carries purchase_url and demo_key. No No, fix the key
PaymentRequired 402 No credits left. Carries top_up_url; POST to it. No After topping up
NotFound 404 No such episode, or no published transcript for it. No No
Throttled 429 Throttled under load. No Yes
UpstreamError 502 Upstream failure. No Yes

Throttled and UpstreamError are retried twice with backoff before they reach you; set Spoken(retries=0) to see every one. All five subclass SpokenError.

from spoken_md import NotFound, PaymentRequired, Spoken

spoken = Spoken("pt_your_key")
try:
    transcript = spoken.transcript("1000625088063")
except NotFound:
    ...                                              # this episode has no transcript
except PaymentRequired as e:
    print("top up:", e.top_up_url)

What comes back

transcript() returns a Transcript: .markdown, .credits_remaining, .credits_charged (1 on a first fetch, 0 on a repeat), and .top_up_url when the balance is low. str() on it is the Markdown. search() returns Episode objects with id, title, podcast, podcast_id and date. episodes() returns a Show you can iterate. balance() returns credits, the attached email and recent usage. following() returns Follow objects (podcast_id, podcast, source, fetch_count, newest_fetched_id) plus the muted shows and the limits; new() returns a WhatsNew you can iterate for NewEpisode objects (id, title, date, transcript_url), with .shows keeping them grouped.

When Spoken is the wrong tool

Spoken returns the published transcript of a podcast episode and nothing else. For your own audio, a meeting or an unpublished recording, use a speech-to-text API and add diarization. For YouTube videos that are not distributed as podcasts, use a YouTube transcript tool. Word-level timestamps and caption files are out of scope. The full list, with what to use instead, is in agents.md.

More

MIT licensed. Issues and pull requests: github.com/spokenmd/spoken.

Release files for spoken-md 0.2.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for spoken-md 0.2.0
File Size Uploaded
spoken_md-0.2.0.tar.gz 16.4 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for spoken-md 0.2.0
File Interpreter ABI Platform
spoken_md-0.2.0-py3-none-any.whl Python 3 none any Details

Total release size: 30.7 kB

Release files / spoken_md-0.2.0.tar.gz

Download URL spoken_md-0.2.0.tar.gz
Size 16.4 kB
Tags Source
SHA-256 checksum
How to use checksums
453a7d23146aaa47420128819f7c64bc5aea699097b3794a2d6d940158982ca4
BLAKE2b-256 checksum
How to use checksums
82f1d49d26960418eb100f36f365cd677bcc6571b3565208dace6912f0f9051f
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.12.14 {"installer":{"name":"uv","version":"0.12.14","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

Release files / spoken_md-0.2.0-py3-none-any.whl

Download URL spoken_md-0.2.0-py3-none-any.whl
Size 14.3 kB
Tags Python 3
SHA-256 checksum
How to use checksums
f5e36ead4d6bb6e14627ce36bdf5a10a4a98da32549055c365f5276cbc5ea955
BLAKE2b-256 checksum
How to use checksums
3284e22a4554a6882c92d1ebfce438be9af76f7bccd97855d7ec712b98ab9384
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.12.14 {"installer":{"name":"uv","version":"0.12.14","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

Release history Release notifications | RSS feed

This release

0.2.0 This release

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page