Create the local working folders before the first book:
./pdftoaudio workspace --fixPut source PDFs in books/:
books/my-book.pdf
Generated job files are written under jobs/<book>/. The repository keeps *.pdf, books/*, and jobs/ in .gitignore so source PDFs, generated text, and audio do not get checked into GitHub by accident. Credentials belong outside repositories under $HOME/.keys/.
Machine setup lives in REQUIREMENTS.md.
Set up the locked Python environment with uv:
direnv allow
uv sync --lockedThe new workflow uses one directory per book under jobs/.
./pdftoaudio workspace --fix
./pdftoaudio init my-book ./books/my-book.pdf
./pdftoaudio extract my-book
./pdftoaudio sanitize my-book
./pdftoaudio review my-book
./pdftoaudio clean my-book --mode codex
./pdftoaudio chapters my-book
./pdftoaudio chunk my-book
./pdftoaudio synthesize my-book --provider google
./pdftoaudio finalize my-book
./pdftoaudio status my-bookFiles are written under:
jobs/my-book/
source/original.pdf
text/raw.txt
text/sanitized.txt
text/cleaned.txt
cleanup/sections.json
cleanup/input/001.json
cleanup/output/001.json
book.json
chapters.json
reports/sanitize.json
reports/line-map.json
reports/review.json
reports/cleanup.json
reports/cleanup.md
reports/tts-validation.json
reports/chunk.json
reports/synthesize.json
reports/finalize.json
reports/finalize-chapters.ffmetadata
chunks/001.txt
chunks/001.txt.done
chunks/002.txt
audio/001.mp3
audio/final.mp3
audio/final.m4b
manifest.json
sanitize applies conservative character and whitespace cleanup. review flags suspicious line ranges without editing text. clean --mode codex repairs extraction damage in resumable sections and records each change. chapters creates an editable structure plan. chunk splits each included chapter into TTS requests, synthesize creates MP3 segments, and finalize publishes MP3 and M4B audiobooks.
./pdftoaudio clean my-book --mode codexCleanup preserves prose meaning and order, joins PDF-wrapped paragraphs, repairs split words, and removes reviewed page furniture. Codex converts dense code listings and tables into spoken explanations while retaining technical meaning. Cleanup requests audiobook-ready plain text and removes paired Markdown backticks around inline technical tokens while preserving the enclosed token. Each section carries a source digest, so reruns reuse completed work and reject stale output.
Use --section N to clean one section without assembling the book. Use --force after changing sanitized text or cleanup settings. Audit details live in reports/cleanup.json and reports/cleanup.md.
To inspect an existing cleaned job for Markdown that Google might pronounce aloud, run:
./pdftoaudio remediate-markdown my-bookThis is a dry run and does not modify files. Review its findings first. To remove safe paired inline backticks from text/cleaned.txt and existing chunk text, run:
./pdftoaudio remediate-markdown my-book --apply --forceThe command refuses to rewrite anything if unpaired or fenced backticks remain, and it never deletes or rewrites audio. After applying remediation, regenerate chapter/chunk metadata as needed and explicitly resynthesize affected audio. Google may bill those synthesis requests again.
./pdftoaudio chapters my-bookThe command writes book.json and chapters.json in the job directory. Review both files before paid synthesis. You can edit titles, chapter types, inclusion flags, authors, narrator, description, language, genre, and the optional cover path. Leave chapter offsets and the source digest unchanged.
Testimonials, copyright pages, contents, prefaces, introductions, and other front matter become named chapters by default. To omit them before chunking and avoid their TTS cost, run:
./pdftoaudio chapters my-book --force --exclude-front-matterchunk validates the chapter plan against text/cleaned.txt. It never combines text from two chapters in one TTS request. If cleaned text changes, regenerate the plan before chunking.
Example book.json:
{
"schema_version": 1,
"title": "The Pragmatic Programmer",
"authors": ["Andrew Hunt", "David Thomas"],
"narrator": "Google en-US-Casual-K",
"description": "",
"language": "en-US",
"genre": "Audiobook",
"cover": "/path/outside/the/repository/cover.jpg"
}Use a JPEG or PNG cover. The pipeline reads the image from its existing location and does not copy it into the repository.
chunk reads text/cleaned.txt by default:
./pdftoaudio chunk my-bookFor diagnostics, you can bypass cleanup explicitly:
./pdftoaudio chunk my-book --source sanitizedThe default chunk limit is 4,900 UTF-8 bytes. chunk keeps each request-sized file under that byte limit. It prefers sentence endings, then paragraph, line, and whitespace boundaries; if punctuation is missing, it still chunks by size instead of treating the whole run as one sentence.
synthesize uses Google TTS for now:
./pdftoaudio synthesize my-book --provider googleBy default it uses en-US-Casual-K, writes MP3 segments under jobs/my-book/audio/, and creates chunks/NNN.txt.done sidecars after successful synthesis. Keep Google credentials outside repositories at $HOME/.keys/googletexttospeech/gen-lang-client.json, or pass --credentials with another external path.
Before contacting Google, synthesis rejects empty text, replacement characters, unsafe controls, Markdown backticks, and sentences of 500 characters or more. Literal <, >, and & produce warnings because the current provider sends plain text rather than SSML.
./pdftoaudio finalize my-bookfinalize verifies the ordered synthesis report and every expected MP3. It produces:
audio/final.mp3, the broad-compatibility copy of the synthesized segments.audio/final.m4b, an AAC audiobook with embedded chapter timestamps, book metadata, and optional cover art.
M4B uses the MPEG-4 audio container and the .m4b extension so audiobook applications treat it as a book instead of a song. FFmpeg encodes AAC at 64 kbps. The command uses ffprobe to verify duration, size, codec, chapter order, timestamps, and requested cover art before publishing the file.
stitch is an alias. Use --cover PATH to override the cover in book.json for one run. Use --no-m4b only when you need the legacy MP3 output. The command records both outputs and embedded chapters in reports/finalize.json.
Use Apple Books as the default player on iPhone, iPad, Mac, Apple Watch, and CarPlay. Import final.m4b through Books on a Mac, then use iCloud or Finder audiobook sync to move it to an iPhone. Apple documents audiobook import and Finder syncing.
BookPlayer is a free, open-source iPhone alternative that imports M4B, M4A, and MP3 files from Files, AirDrop, and other applications. VLC provides a free cross-platform fallback. Audiobookshelf adds a self-hosted library and cross-device progress when you need server-backed listening.
Apple Podcasts is designed around podcast feeds. It is not the default application for local audiobook files.
Google TTS can reject input that contains very long sentences. The older sentences_length helper still exists for inspecting one legacy chunk file:
./sentences_length ../text/document_title/04.txtThe new CLI checks this earlier:
./pdftoaudio review my-bookreview scans jobs/my-book/text/sanitized.txt and writes jobs/my-book/reports/review.json. Sentences at 500 characters or longer are reported with the issue code long_sentence. The command only reports the issue; it does not split or rewrite the sentence.
The older script workflow still exists while the CLI migration is in progress.
- Place a text PDF within the
books/directory. To make it easier, rename it something that is easy to type such as,pdf_name.pdf - Within the project root directory, run
./read pdf ${pdf_name.pdf}in the terminal. - App will extract the text from the pdf and give a rough estimate on the cost to convert to audio .mp3 file using Google TTS standard.
- Review and clean up the .txt file manually, erase any oddities that you don't want in your mp3 file.
- Activate the Google TTS / Audio container by running
./read speech pdf_name.pdf.txt(thepdf_name.pdf.txtis a placeholder to later turn this into a full function). This will put you in the container shell after installing needed tools. - With your cleaned up
pdf_name.pdf.txtfile, runchunk_texts pdf_name.pdf.txtto chunk the large text file that you manually cleaned up into 5000 or less byte chunks numbered01.txt,02.txtand so on in the directorypdf_name. This is necessary to make requests via Google TTS. - Run
texttospeech pdf_nameon your designated text file. This will send requests to Google TTS and output individual files of the format01.mp3,02.mp3, etc. to the directoryaudio/pdf_name. Please see the section on Google TTS Quotas below for API usage. - Finally you can string together all of the mp3s generated by running the command
string_together_mp3s pdf_namewhich will place a final mp3 titledaudio/finalized/pdf_name.mp3which will be a concatenation of all of the individual numbered mp3 files.
- As of writing this readme file, Google TTS imposes a 5000 bytes per request quota, and a 1000 requests per minute quota.
- Therefore:
(5000 bytes/request)(1000 requests/minute) = 5000000 bytes/minute = 5M bytes/minute
- If appropriately chunked, assuming there are 300,000 characters in a large document, this would suggest around 16 large documents per minute can be processed.
- That being said, the free tier of Google TTS is 1,000,000 or 1M bytes, so after 1M bytes there would be a cost per byte.
- Bytes are approximately equal to one character, accepting special characters which may be more than one byte, but our cost estimation function assumes incorrectly that 1 character = 1 byte.
https://cloud.google.com/text-to-speech/docs/ssml
- The following type of error may arise:
An error occurred: 400 This request contains sentences that are too long. Consider splitting up long sentences with sentence ending punctuation e.g. periods. Also consider removing SSML sentence tags (e.g. '<s>') as they can confuse Cloud Text-to-Speech.
- We have put together
sentences_lengthto analyze individual documents for sentences that might be too long. Run this on the document in question with, for example:
./sentences_length ../text/document_title/04.txt
-
...This will print out the length of each sentence via a simple regex function, demarcating sentences by
.,!or?. -
Typically sentences with 500+ characters are too long for Google TTS.