Metadata-Version: 2.4
Name: slidestolectures
Version: 0.1.0
Summary: Turn slideshow PDFs into bookmarked PDFs, slide text, lecture transcripts and narrated audio
Keywords: slides,pdf,lecture,transcript,text-to-speech,claude
Author: Addison Grant
License-Expression: MIT
License-File: LICENSE
Classifier: Development Status :: 3 - Alpha
Classifier: Environment :: Console
Classifier: Intended Audience :: Education
Classifier: Programming Language :: Python :: 3
Classifier: Topic :: Education
Classifier: Topic :: Multimedia :: Sound/Audio :: Speech
Classifier: Topic :: Text Processing
Requires-Dist: anthropic>=1.13.0
Requires-Dist: pymupdf>=1.28.2
Requires-Dist: speechify-api>=4.0.0 ; extra == 'tts'
Requires-Python: >=3.11
Project-URL: Homepage, https://github.com/ExNumberless/SlidesToLectures
Project-URL: Issues, https://github.com/ExNumberless/SlidesToLectures/issues
Provides-Extra: tts
Description-Content-Type: text/markdown

# SlidesToLectures

Turn a directory of slideshow PDFs into:

- **Bookmarked PDFs**: a copy of each deck with a `[N] Slide title` bookmark per topic, plus one combined PDF of every deck.
- **Slide text**: a Markdown file per deck with one `## [N] Title` section per slide, from the PDF's text layer or transcribed from slide images by Claude.
- **Lecture transcripts**: a spoken, word-for-word lecture per deck, written by Claude to cover every point on every slide.
- **Narrated audio**: an MP3 per lecture with a chapter per slide, using Speechify text-to-speech.

Slides are numbered across the whole set (from the numbers printed on them when present), so bookmarks, text, lectures and audio chapters all line up.

## Install

```bash
uv tool install "slidestolectures[tts]"
```

or `pip install "slidestolectures[tts]"`. Leave off `[tts]` if you don't need audio. Requires Python 3.11+.

- The `vision` and `lecture` steps call the Claude API: set `ANTHROPIC_API_KEY`.
- The `tts` step needs `SPEECHIFY_API_KEY` and [ffmpeg](https://ffmpeg.org/) on your `PATH`.

## Quick start

```bash
slides-to-lectures toc --input slides       # out/pdfs/ and out/combined/
slides-to-lectures text --input slides      # out/text/   (or: vision, to transcribe slide images with Claude)
slides-to-lectures lecture                  # out/lectures/
slides-to-lectures tts                      # out/audio/
```

Every command takes `--input DIR` (the slideshow PDFs), `--out DIR` (default `out`), `--plugin` and `--config`. Source PDFs are never modified. Run `slides-to-lectures <command> --help` for each command's options:

| Command | What it does | Notable options |
|---|---|---|
| `toc` | Bookmarked copy of each deck, plus a combined PDF | `--dry-run` prints the bookmarks |
| `text` | Slide text from the PDF text layer | |
| `vision` | Slide text transcribed from slide images by Claude; cached per slide | `--model` |
| `lecture` | Lecture transcript per deck from its slide text | `--model`, `--force` |
| `tts` | Narrated MP3 per lecture; audio cached per request | `--voice`, `--rps`, `--concurrency`, `--force` |

`toc`, `text` and `vision` accept deck names to process only some decks; `lecture` and `tts` accept Markdown files.

## Configuration

Put a `slidestolectures.toml` in the directory you run from (or pass `--config`), so you don't have to repeat options:

```toml
plugin = "my_plugin.py"   # optional, see below
input = "slides"
output = "out"

[output_dirs]             # optional: put individual outputs elsewhere
pdfs = "bookmarked"

[settings]                # plugin settings
course = "Introduction to Biology (BIO 101)"
audience = "a class of first-year undergraduates"
footer = "Example University"
```

Settings:

| Setting | Default | Used for |
|---|---|---|
| `course` | none | What the lectures teach, in the lecture prompt |
| `audience` | `"a class"` | Who the lecture is presented to |
| `relevance` | `"in practice"` | The lecture explains why each point matters *relevance* |
| `footer` | none | Footer text on every slide, which `vision` leaves out |
| `vision_model`, `lecture_model` | `claude-opus-5-5` | Claude models |
| `tts_voice` | `henry` | Speechify voice |
| `numbering` | `"continuous"` | For decks without printed slide numbers: `"continuous"` across decks, or `"per-deck"` |
| `footer_y`, `title_min_size`, `fallback_min_size`, `fallback_max_y` | 480, 36, 24, 150 | Layout thresholds in points for a 540pt-tall slide, scaled to each page |

## Plugins

When your slides need more than settings, such as particular filenames, deck titles, or fixes for slides whose title is an image, write a plugin: a subclass of `slidestolectures.Plugin` that overrides the hooks you need. Call `super()` to wrap the default behavior.

```python
from slidestolectures import Plugin


class BiologyPlugin(Plugin):
    course = "Introduction to Biology (BIO 101)"

    def deck_title(self, path, doc):
        return path.stem.split(" - ", 1)[-1]  # "Week 3 - Cell Division" -> "Cell Division"

    def slide_title(self, deck, number, detected):
        if number == 42:
            return "The Krebs Cycle"  # this slide's title is an image
        return super().slide_title(deck, number, detected)
```

Use it with `--plugin my_plugin.py`, `--plugin my_plugin.py:BiologyPlugin`, `--plugin package.module:BiologyPlugin`, or `plugin = ...` in the config. See [examples/example_plugin.py](examples/example_plugin.py).

| Hook | Default |
|---|---|
| `decks(paths)` | Every PDF in the input directory, sorted by name |
| `deck_title(path, doc)` | The filename, with `_` as spaces |
| `output_name(deck)` | `<filename>_<first>-<last>` |
| `combined_name(decks)` / `should_combine(decks)` | `<input dir>_<first>-<last>`; combine when there's more than one deck |
| `find_slide_number(lines, page)` | The number at the end of the footer |
| `detect_title(lines, page)` / `slide_body(lines, title, page)` | Largest text near the top / the rest as Markdown |
| `slide_title(deck, number, detected)` | The detected title |
| `is_continuation(title)` | Titles with "cont'd" or "continued" get no bookmark |
| `vision_prompt()`, `lecture_system()`, `lecture_prompt(title, slides)` | Built from the settings |

## Development

```bash
uv sync --all-extras
uv run pytest
uv run ruff check .
```

With [mise](https://mise.jdx.dev/), `mise run install`, `mise run test`, `mise run lint` and `mise run build` do the same.

### Releasing

Bump `version` in `pyproject.toml`, then publish a GitHub release tagged `v<version>`. The Release workflow builds the package and publishes it to PyPI with trusted publishing.

## License

[MIT](LICENSE)
