The Problem
Enterprises that need to turn policy documents, research papers, or internal reports into audio for broader consumption often rely on cloud APIs that expose proprietary models and data. The cost, latency, and privacy concerns of sending raw text to external services become blockers for regulated environments.
What This Does
document-to-podcast is a self‑contained Python Blueprint that runs entirely on‑premise. It parses PDFs, DOCX, HTML or plain‑text, cleans the content, feeds it to a local LLM (via llama.cpp) to generate a script, and then uses a locally‑hosted TTS model to synthesize two speaker tracks that are stitched together into a podcast episode.
Key implementation pieces:
- CLI entry point –
src/document_to_podcast/cli.pydefinesdocument_to_podcast()(called from thedocument-to-podcastconsole script) andmain(). - Demo UI –
demo/app.pybuilds a Streamlit interface; the heavy lifting lives in the same functions used by the CLI. - Model loading –
src/document_to_podcast/inference/model_loaders.pyimplementsload_llama_cpp_model,_load_kokoro_tts,load_tts_model, and theTTSModelclass. - Pre‑processing –
src/document_to_podcast/preprocessing/data_loaders.pyreads files (load_pdf,load_txt,load_docx,load_url) andsrc/document_to_podcast/preprocessing/data_cleaners.pynormalises HTML/Markdown viaclean_html,clean_markdown, andclean_with_regex. - Inference –
src/document_to_podcast/inference/text_to_text.pyruns the LLM (chat_completion,text_to_text_stream) andsrc/document_to_podcast/inference/text_to_speech.pyproduces audio (text_to_speech). - Utility –
src/document_to_podcast/utils.pyconcatenates audio chunks withstack_audio_segments.
The internal call graph shows a shallow flow: the CLI → document_to_podcast → config validation → model loaders → preprocessing → inference → audio stacking. The most widely used functions (load_txt, text_to_text_stream, load_llama_cpp_model, load_tts_model, clean_with_regex) each have ≤6 callers, limiting blast radius for changes.
How It Is Wired
Execution starts at the console script defined in pyproject.toml (document-to-podcast). This invokes src/document_to_podcast/cli.py:document_to_podcast, which:
- Reads the user‑supplied
config.yaml(filesystem read viaPath.read_text). - Validates input files and model paths (
src/document_to_podcast/config.py). - Calls
load_llama_cpp_model(model loader) andload_tts_model(TTS loader). - Loads the source document through
load_pdf/load_txt/load_docx/load_url. - Cleans the raw text with
clean_html/clean_markdown→clean_with_regex. - Generates a script by streaming LLM responses (
text_to_text_stream→chat_completion). - Converts each segment to audio (
text_to_speech→_text_to_speech_kokoro). - Stacks the resulting WAV chunks (
stack_audio_segments) and writes the final podcast file.
The demo Streamlit app (demo/app.py) follows the same pipeline but adds UI callbacks (gen_button_clicked). The file suffers from deep nesting (max indentation depth 7), making the control flow harder to follow; refactoring into guard clauses would improve readability.
External interactions are limited to:
- Filesystem – reading the input document and config, writing the podcast output.
- Network – optional URL loading (
load_url) and any model files fetched by the user; no built‑in remote API calls.
How To Use It
# Clone the repo
git clone https://github.com/moses-y/document-to-podcast
cd document-to-podcast
# Install the package (uses pyproject.toml)
pip install .
# or, for an isolated build:
docker build -t doc2pod -f demo/Dockerfile .
Run the CLI (example from README):
document-to-podcast \
--input_file "example_data/Mozilla-Trustworthy_AI.pdf" \
--output_folder "example_data" \
--text_to_text_model "Qwen/Qwen2.5-1.5B-Instruct-GGUF/qwen2.5-1.5b-instruct-q8_0.gguf"
Or start the UI:
python demo/app.py
Configuration lives in example_data/config.yaml; adjust model paths and speaker parameters there.
Real‑World Use
A compliance team can integrate the CLI into an internal CI pipeline that watches a policies/ folder. On each push, a GitHub Action runs document-to-podcast to generate an MP3 that is then uploaded to the company’s internal media server. Because all models are loaded from local disks, no confidential text leaves the network.
Code Health & Issues
- High – Pin GitHub Actions –
.github/workflows/*uses tags likepypa/gh-action-pypi-publish@release/v1. Replace with a 40‑char SHA to avoid supply‑chain drift. - High – Missing lockfile –
pyproject.tomldeclares dependencies but nopoetry.lock/uv.lock. Generate and commit a lockfile to guarantee reproducible builds. - Medium – Least‑privilege
GITHUB_TOKEN–.github/workflows/lint.yamllacks explicit permissions; addpermissions: contents: read. - Medium – Pin Docker base image –
demo/Dockerfileusesnvidia/cuda:12.2.2-cudnn8-devel-ubuntu22.04without a digest. Use@sha256:<digest>. - Medium – No dependency vulnerability scan – Add
dependency-review-actionorosv-scannerto CI. - Medium – Notebook outputs –
demo/notebook.ipynbstores >22 MiB of outputs; installnbstripoutand clear them. - Medium – Large binaries – The same notebook exceeds 5 MiB; track with Git LFS or external storage.
- Medium – Checkout persist‑credentials –
.github/workflows/docs.yamlshould setpersist-credentials: false. - Low – Job timeouts –
.github/workflows/docs.yamllackstimeout-minutes; add sensible limits. - Medium – Deep nesting –
demo/app.pyhas indentation depth 7; refactor for maintainability.
No critical or low‑severity functional bugs were detected by static analysis.
The Bottom Line
document-to-podcast provides a clear, fully local path from document to audio and is organized around a modest, well‑tested codebase. To move to production, address the high‑severity CI and lockfile gaps, pin mutable dependencies, and simplify the demo UI’s nesting. It is suitable for teams that need on‑premise podcast generation without exposing content to cloud services.