Metadata-Version: 2.4
Name: bookpeek
Version: 0.1.3
Summary: Extract audiobook metadata from spoken introductions.
License: MIT
Author: Brandon Shelley
Author-email: brandon@pacificaviator.co
Requires-Python: >=3.12,<4.0
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Provides-Extra: all
Provides-Extra: online
Provides-Extra: vosk
Provides-Extra: whisper
Requires-Dist: faster-whisper (>=1.1.1,<2.0.0) ; extra == "whisper" or extra == "all"
Requires-Dist: goodscraps (>=0.1.4,<0.2.0) ; extra == "online" or extra == "all"
Requires-Dist: httpx (>=0.28.1,<0.29.0) ; extra == "online" or extra == "all"
Requires-Dist: mlx-whisper (>=0.4.2,<0.5.0) ; (platform_system == "Darwin") and (extra == "whisper" or extra == "all")
Requires-Dist: mutagen (>=1.48.1,<2.0.0)
Requires-Dist: pydantic (>=2.11.5,<3.0.0)
Requires-Dist: spacy (>=3.8.10,<3.9) ; python_version >= "3.12" and python_version < "3.15"
Requires-Dist: tinta (>=1.1.0,<2.0.0)
Requires-Dist: vosk (>=0.3.45,<0.4.0) ; extra == "vosk" or extra == "all"
Description-Content-Type: text/markdown

<img width="300" alt="bookpeek" src="https://github.com/user-attachments/assets/39cd1f9b-c830-444c-b3f7-b781d0ed4809" />


# bookpeek

`bookpeek` scans the opening seconds of an audiobook and extracts the spoken
title, title candidates, authors, and narrators without requiring an online AI
service or API key. Title candidates can come from Audible-style introductions,
ID3 tags, filenames, folders, and chapter text, with scores indicating which
candidate was preferred.

Online results are grouped by provider under `online_matches`, with separate
`authors` and `works` arrays. Goodreads author results with zero similarity
are omitted when any positive-scoring author exists; otherwise authors with at
least ten works are retained as a fallback.

The result's top-level `title`, `author`, and `narrators` fields contain the
final values after provider validation. The nested `extraction` object remains
the unmodified transcript/metadata extraction.

Audnexus results appear under `online_matches.audnexus.works` and include
matched Audible identifiers, authors, narrators, region, and confidence scores.
Verified narrator names are also collected under
`online_matches.audnexus.narrators`.

## Install



### First-time setup

There are only two steps:

1. Install `ffmpeg`, which decodes audiobook files:
  ```bash
   # macOS with Homebrew
   brew install ffmpeg

   # Debian/Ubuntu
   sudo apt update && sudo apt install ffmpeg
  ```
2. Run the installer:
  ```bash
   ./install.sh
  ```

The installer bootstraps Poetry if necessary, installs the default Whisper
setup plus online enrichment, downloads spaCy's English model, and creates
`~/.local/bin/bookpeek`. If `ffmpeg` is missing, it prints the appropriate
installation commands and exits. Add `~/.local/bin` to your `PATH` if the
`bookpeek` command is not found.

### Optional installation choices

Most users should use the default command above. Use these only when you need
a different setup:

```bash
# Smaller CPU-oriented Vosk engine instead of Whisper.
BOOKPEEK_EXTRAS=vosk ./install.sh

# Vosk plus no online dependencies or lookups.
BOOKPEEK_EXTRAS=vosk ./install.sh --offline

# Whisper and Vosk together.
BOOKPEEK_EXTRAS=all ./install.sh
```

On Apple Silicon, the default Whisper installation uses Metal automatically
when `device = "auto"`. The first scan downloads the public converted MLX
checkpoint for the selected model; no Hugging Face login or access token is
needed.



## Usage

By default, bookpeek scans 30 seconds first. If that only produces an Audible
bumper such as `This is Audible`, it automatically retries at 60 seconds and
then 90 seconds. A normal introduction stops after the first successful scan.

```bash
# Scan an audiobook file:
bookpeek scan /path/to/book.m4b

# Scan a folder; bookpeek uses the first audio file alphabetically:
bookpeek scan /path/to/audiobook-folder/

# Scan longer with the small CPU-friendly Vosk engine:
bookpeek scan /path/to/book.m4b --seconds 42 --engine vosk

# Force Whisper's tiny English model on the CPU:
bookpeek scan /path/to/book.m4b --model tiny.en --device cpu

# Search (only) the specific Audible regions, in this order:
bookpeek scan /path/to/book.m4b --regions uk,ca,au

# Enable Open Library, Goodreads, and Audnexus enrichment for this scan:
bookpeek scan /path/to/book.m4b -w

# Force a completely offline scan, even if config enables enrichment:
bookpeek scan /path/to/book.m4b --offline

# Print the active configuration:
bookpeek config show

# Create a new config file with bookpeek's defaults:
# (Saves to ~/.config/bookpeek/config.toml; use --force to overwrite.)
bookpeek config new
```



### Example response

The JSON result includes the extracted metadata and online provider matches:

```json
{
  "path": "/path/to/The Blighted Stars.m4b",
  "title": "The Blighted Stars (The Devoured Worlds, #1)",
  "author": "Megan E. O'Keefe",
  "narrators": [
    "Ciaran Saward"
  ],
  "extraction": {
    "title": "The Blighted Stars",
    "title_candidates": [
      {
        "text": "The Blighted Stars",
        "source": "id3_title",
        "score": 0.9
      },
      {
        "text": "The Devoured Worlds 01 - The Blighted Stars",
        "source": "folder",
        "score": 0.65
      }
    ],
    "author": "Megan E. O'Keefe",
    "narrators": [
      "Kieran Sord"
    ]
  },
  "online_matches": {
    "audnexus": {
      "authors": [
        {
          "asin": "B00IMSE6VQ",
          "name": "Megan E. O'Keefe",
          "region": "us",
          "score": 1.0
        }
      ],
      "works": [
        {
          "asin": "B0BJ4JLW57",
          "title": "The Blighted Stars",
          "authors": ["Megan E. O'Keefe"],
          "narrators": ["Ciaran Saward"],
          "region": "us",
          "isbn": "9781668615331",
          "score": 0.933,
          "narrator_score": 0.667,
          "match_reason": "title_author"
        }
      ],
      "narrators": ["Ciaran Saward"]
    },
    "goodreads": {
      "authors": [],
      "works": [
        {
          "title": "The Blighted Stars (The Devoured Worlds, #1)",
          "title_complete": "The Blighted Stars (The Devoured Worlds, #1)",
          "author": "Megan E. O'Keefe",
          "score": 0.857
        }
      ]
    },
    "openlibrary": {
      "authors": [],
      "works": [
        {
          "title": "Devoured Worlds Series, 3-book collection ...",
          "author": "Megan E. O'Keefe",
          "score": 0.632
        }
      ]
    }
  }
}
```

Configuration is read from `~/.config/bookpeek/config.toml`; command-line
options override it:

`bookpeek config new` creates that file with the default settings. It will not
overwrite an existing config unless `bookpeek config new --force` is used.

```toml
[asr]
engine = "whisper"
whisper_model = "tiny.en"
vosk_model = "vosk-model-small-en-us-0.15"
device = "auto"
seconds = 30

[extract]
spacy_model = "en_core_web_sm"

[enrich]
enabled = false # set true to always perform web lookup
openlibrary = true
goodreads = true
audnexus = true
# Search order matters: earlier regions win ties and are returned first.
audible_regions = ["us", "uk", "ca", "au"]
```

Supported Audnexus/Audible regions are `us`, `uk`, `ca`, and `au`.
`audible_regions` is an ordered priority list, not just a set of enabled
regions: if equivalent matches are found in multiple stores, the first
region listed wins and its result is returned first. `--regions` replaces this
priority order for one scan, so `--regions ca,us` gives Canada precedence over
the US.

`-w` / `--online` may query Open Library, Goodreads, and Audnexus. Goodreads
support is installed as a package dependency. Set
`[enrich].enabled = true` to perform that lookup on every scan without passing
the flag. Use `--offline` to override that setting for a single scan. Narrator
lookup is performed through Audible catalog search followed by Audnexus
metadata lookup when matching is enabled.

## CI and PyPI releases

GitHub Actions runs tests, Ruff, and mypy on pushes to `main` and pull
requests. After those checks pass, a change to the package version on `main`
automatically creates the matching GitHub release, which triggers PyPI
publishing through Trusted Publishing.

To release a new version:

```bash
poetry run python scripts/set_version.py 0.1.1
git add pyproject.toml src/bookpeek/__init__.py
git commit -m "Bump package version"
git push origin main
```

The repository's PyPI project must have a Trusted Publisher configured for the
GitHub repository, workflow `.github/workflows/publish.yml`, and `pypi`
environment before the first release.

## Development

```bash
poetry run pytest
poetry run ruff check .
poetry run mypy src/
```

The provider URL smoke test is opt-in because it makes network requests:

```bash
BOOKPEEK_LIVE=1 poetry run pytest tests/test_urls_live.py
```


