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.py defines document_to_podcast() (called from the document-to-podcast console script) and main().
  • Demo UI – demo/app.py builds a Streamlit interface; the heavy lifting lives in the same functions used by the CLI.
  • Model loading – src/document_to_podcast/inference/model_loaders.py implements load_llama_cpp_model, _load_kokoro_tts, load_tts_model, and the TTSModel class.
  • Pre‑processing – src/document_to_podcast/preprocessing/data_loaders.py reads files (load_pdf, load_txt, load_docx, load_url) and src/document_to_podcast/preprocessing/data_cleaners.py normalises HTML/Markdown via clean_html, clean_markdown, and clean_with_regex.
  • Inference – src/document_to_podcast/inference/text_to_text.py runs the LLM (chat_completion, text_to_text_stream) and src/document_to_podcast/inference/text_to_speech.py produces audio (text_to_speech).
  • Utility – src/document_to_podcast/utils.py concatenates audio chunks with stack_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:

  1. Reads the user‑supplied config.yaml (filesystem read via Path.read_text).
  2. Validates input files and model paths (src/document_to_podcast/config.py).
  3. Calls load_llama_cpp_model (model loader) and load_tts_model (TTS loader).
  4. Loads the source document through load_pdf/load_txt/load_docx/load_url.
  5. Cleans the raw text with clean_html/clean_markdown → clean_with_regex.
  6. Generates a script by streaming LLM responses (text_to_text_stream → chat_completion).
  7. Converts each segment to audio (text_to_speech → _text_to_speech_kokoro).
  8. 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 like pypa/gh-action-pypi-publish@release/v1. Replace with a 40‑char SHA to avoid supply‑chain drift.
  • High – Missing lockfile – pyproject.toml declares dependencies but no poetry.lock/uv.lock. Generate and commit a lockfile to guarantee reproducible builds.
  • Medium – Least‑privilege GITHUB_TOKEN – .github/workflows/lint.yaml lacks explicit permissions; add permissions: contents: read.
  • Medium – Pin Docker base image – demo/Dockerfile uses nvidia/cuda:12.2.2-cudnn8-devel-ubuntu22.04 without a digest. Use @sha256:<digest>.
  • Medium – No dependency vulnerability scan – Add dependency-review-action or osv-scanner to CI.
  • Medium – Notebook outputs – demo/notebook.ipynb stores >22 MiB of outputs; install nbstripout and 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.yaml should set persist-credentials: false.
  • Low – Job timeouts – .github/workflows/docs.yaml lacks timeout-minutes; add sensible limits.
  • Medium – Deep nesting – demo/app.py has 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.