pdftts 0.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pdftts-0.2.0/.dockerignore +16 -0
- pdftts-0.2.0/.github/workflows/publish.yml +30 -0
- pdftts-0.2.0/.github/workflows/test.yml +98 -0
- pdftts-0.2.0/.gitignore +21 -0
- pdftts-0.2.0/.python-version +1 -0
- pdftts-0.2.0/CHANGELOG.md +80 -0
- pdftts-0.2.0/CONTRIBUTING.md +48 -0
- pdftts-0.2.0/Dockerfile +78 -0
- pdftts-0.2.0/LICENSE +21 -0
- pdftts-0.2.0/PKG-INFO +468 -0
- pdftts-0.2.0/README.md +427 -0
- pdftts-0.2.0/pyproject.toml +69 -0
- pdftts-0.2.0/src/pdftts/__init__.py +8 -0
- pdftts-0.2.0/src/pdftts/audio.py +139 -0
- pdftts-0.2.0/src/pdftts/batch.py +81 -0
- pdftts-0.2.0/src/pdftts/cache.py +130 -0
- pdftts-0.2.0/src/pdftts/chapters.py +37 -0
- pdftts-0.2.0/src/pdftts/chunk.py +99 -0
- pdftts-0.2.0/src/pdftts/clean.py +100 -0
- pdftts-0.2.0/src/pdftts/cli.py +275 -0
- pdftts-0.2.0/src/pdftts/core.py +92 -0
- pdftts-0.2.0/src/pdftts/device.py +88 -0
- pdftts-0.2.0/src/pdftts/documents.py +250 -0
- pdftts-0.2.0/src/pdftts/engines/__init__.py +73 -0
- pdftts-0.2.0/src/pdftts/engines/base.py +61 -0
- pdftts-0.2.0/src/pdftts/engines/chatterbox_engine.py +77 -0
- pdftts-0.2.0/src/pdftts/engines/kokoro_engine.py +173 -0
- pdftts-0.2.0/src/pdftts/engines/miso_engine.py +92 -0
- pdftts-0.2.0/src/pdftts/engines/piper_engine.py +77 -0
- pdftts-0.2.0/src/pdftts/engines/system_engine.py +57 -0
- pdftts-0.2.0/src/pdftts/extract.py +118 -0
- pdftts-0.2.0/src/pdftts/library.py +115 -0
- pdftts-0.2.0/src/pdftts/ocr.py +49 -0
- pdftts-0.2.0/src/pdftts/server.py +591 -0
- pdftts-0.2.0/src/pdftts/subtitles.py +59 -0
- pdftts-0.2.0/src/pdftts/tts.py +152 -0
- pdftts-0.2.0/src/pdftts/tunnel.py +150 -0
- pdftts-0.2.0/tests/test_auth.py +79 -0
- pdftts-0.2.0/tests/test_batch.py +109 -0
- pdftts-0.2.0/tests/test_cache.py +130 -0
- pdftts-0.2.0/tests/test_clean.py +83 -0
- pdftts-0.2.0/tests/test_dashboard_script.py +131 -0
- pdftts-0.2.0/tests/test_documents.py +150 -0
- pdftts-0.2.0/tests/test_engines.py +129 -0
- pdftts-0.2.0/tests/test_extract.py +24 -0
- pdftts-0.2.0/tests/test_kindle.py +87 -0
- pdftts-0.2.0/tests/test_languages.py +65 -0
- pdftts-0.2.0/tests/test_server_api.py +104 -0
- pdftts-0.2.0/tests/test_subtitles.py +52 -0
- pdftts-0.2.0/tests/test_timeline.py +38 -0
- pdftts-0.2.0/tests/test_tunnel.py +145 -0
- pdftts-0.2.0/uv.lock +4802 -0
- pdftts-0.2.0/vendor/ocrpdf.swift +38 -0
- pdftts-0.2.0/web/index.html +602 -0
- pdftts-0.2.0/web/sw.js +77 -0
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Publish to PyPI on a tagged release, using a Trusted Publisher — no token is
|
|
2
|
+
# stored anywhere. Inert until the project is claimed on PyPI and this workflow
|
|
3
|
+
# is registered as its publisher; until then, tagging simply does nothing.
|
|
4
|
+
name: publish
|
|
5
|
+
|
|
6
|
+
on:
|
|
7
|
+
release:
|
|
8
|
+
types: [published]
|
|
9
|
+
workflow_dispatch:
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
publish:
|
|
13
|
+
runs-on: ubuntu-latest
|
|
14
|
+
environment: pypi
|
|
15
|
+
permissions:
|
|
16
|
+
id-token: write # what Trusted Publishing authenticates with
|
|
17
|
+
steps:
|
|
18
|
+
- uses: actions/checkout@v4
|
|
19
|
+
- uses: astral-sh/setup-uv@v5
|
|
20
|
+
- run: uv build
|
|
21
|
+
- name: the wheel carries its assets
|
|
22
|
+
run: |
|
|
23
|
+
uv run --no-project python - <<'PY'
|
|
24
|
+
import glob, zipfile, sys
|
|
25
|
+
names = zipfile.ZipFile(glob.glob("dist/*.whl")[0]).namelist()
|
|
26
|
+
missing = [p for p in ("pdftts/web/index.html", "pdftts/web/sw.js",
|
|
27
|
+
"pdftts/vendor/ocrpdf.swift") if p not in names]
|
|
28
|
+
sys.exit(f"missing from wheel: {missing}" if missing else 0)
|
|
29
|
+
PY
|
|
30
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
name: tests
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
workflow_dispatch:
|
|
8
|
+
|
|
9
|
+
concurrency:
|
|
10
|
+
group: ${{ github.workflow }}-${{ github.ref }}
|
|
11
|
+
cancel-in-progress: true
|
|
12
|
+
|
|
13
|
+
jobs:
|
|
14
|
+
test:
|
|
15
|
+
# Both platforms matter: OCR is macOS-only, so Linux proves the rest of the
|
|
16
|
+
# pipeline does not quietly depend on it.
|
|
17
|
+
runs-on: ${{ matrix.os }}
|
|
18
|
+
strategy:
|
|
19
|
+
fail-fast: false
|
|
20
|
+
matrix:
|
|
21
|
+
os: [ubuntu-latest, macos-latest]
|
|
22
|
+
steps:
|
|
23
|
+
- uses: actions/checkout@v4
|
|
24
|
+
- uses: astral-sh/setup-uv@v5
|
|
25
|
+
with:
|
|
26
|
+
enable-cache: true
|
|
27
|
+
cache-dependency-glob: uv.lock
|
|
28
|
+
- uses: actions/setup-node@v4
|
|
29
|
+
with: { node-version: "22" } # the dashboard-script test executes the page
|
|
30
|
+
- run: uv sync --locked
|
|
31
|
+
- run: uv run pytest -q
|
|
32
|
+
|
|
33
|
+
build:
|
|
34
|
+
# A wheel that omits the dashboard or the OCR helper installs fine and fails
|
|
35
|
+
# at runtime, so check the assets are actually inside it.
|
|
36
|
+
runs-on: ubuntu-latest
|
|
37
|
+
steps:
|
|
38
|
+
- uses: actions/checkout@v4
|
|
39
|
+
- uses: astral-sh/setup-uv@v5
|
|
40
|
+
- run: uv build
|
|
41
|
+
- name: the wheel carries its assets
|
|
42
|
+
run: |
|
|
43
|
+
uv run --no-project python - <<'PY'
|
|
44
|
+
import glob, zipfile, sys
|
|
45
|
+
names = zipfile.ZipFile(glob.glob("dist/*.whl")[0]).namelist()
|
|
46
|
+
missing = [p for p in ("pdftts/web/index.html", "pdftts/web/sw.js",
|
|
47
|
+
"pdftts/vendor/ocrpdf.swift") if p not in names]
|
|
48
|
+
sys.exit(f"missing from wheel: {missing}" if missing else 0)
|
|
49
|
+
PY
|
|
50
|
+
- name: it installs and runs from the wheel alone
|
|
51
|
+
run: |
|
|
52
|
+
uv venv /tmp/probe
|
|
53
|
+
VIRTUAL_ENV=/tmp/probe uv pip install dist/*.whl
|
|
54
|
+
/tmp/probe/bin/pdftts --list-languages
|
|
55
|
+
|
|
56
|
+
docker:
|
|
57
|
+
# This image cannot be built on the machine I develop on — no Docker daemon
|
|
58
|
+
# there — so CI is what proves it works, not a claim in the README.
|
|
59
|
+
runs-on: ubuntu-latest
|
|
60
|
+
steps:
|
|
61
|
+
- uses: actions/checkout@v4
|
|
62
|
+
- uses: docker/setup-buildx-action@v3
|
|
63
|
+
- name: build
|
|
64
|
+
uses: docker/build-push-action@v6
|
|
65
|
+
with:
|
|
66
|
+
context: .
|
|
67
|
+
load: true
|
|
68
|
+
tags: pdftts:ci
|
|
69
|
+
cache-from: type=gha
|
|
70
|
+
cache-to: type=gha,mode=max
|
|
71
|
+
- name: the phonemiser model is baked in, not downloaded at runtime
|
|
72
|
+
run: |
|
|
73
|
+
docker run --rm --entrypoint python pdftts:ci -c \
|
|
74
|
+
"import spacy.util; assert spacy.util.is_package('en_core_web_sm')"
|
|
75
|
+
- name: it runs, and knows what it can do
|
|
76
|
+
run: |
|
|
77
|
+
docker run --rm pdftts:ci --list-languages
|
|
78
|
+
docker run --rm pdftts:ci --list-engines
|
|
79
|
+
- name: it narrates a file and writes a tagged m4b
|
|
80
|
+
run: |
|
|
81
|
+
mkdir -p books
|
|
82
|
+
printf 'The first sentence. A second one follows it. And a third.\n' > books/sample.txt
|
|
83
|
+
docker run --rm --user "$(id -u):$(id -g)" \
|
|
84
|
+
-v "$PWD/books:/books" pdftts:ci /books/sample.txt --m4a --srt
|
|
85
|
+
ls -l books/
|
|
86
|
+
test -s books/sample.wav
|
|
87
|
+
test -s books/sample.m4a
|
|
88
|
+
test -s books/sample.srt
|
|
89
|
+
test "$(stat -c %u books/sample.wav)" = "$(id -u)" # written as the caller
|
|
90
|
+
- name: the dashboard answers over a published port
|
|
91
|
+
run: |
|
|
92
|
+
docker run -d --name dash -p 8765:8765 pdftts:ci --serve --lan
|
|
93
|
+
for i in $(seq 1 60); do
|
|
94
|
+
curl -sf http://127.0.0.1:8765/api/engines >/dev/null && break || sleep 2
|
|
95
|
+
done
|
|
96
|
+
curl -sf http://127.0.0.1:8765/ | grep -q "Drop a document here"
|
|
97
|
+
curl -sf "http://127.0.0.1:8765/api/voices?engine=kokoro" | grep -q af_heart
|
|
98
|
+
docker rm -f dash
|
pdftts-0.2.0/.gitignore
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
.venv/
|
|
5
|
+
build/
|
|
6
|
+
dist/
|
|
7
|
+
*.egg-info/
|
|
8
|
+
.pytest_cache/
|
|
9
|
+
|
|
10
|
+
# Audio output and working files
|
|
11
|
+
*.wav
|
|
12
|
+
*.m4a
|
|
13
|
+
*.mp3
|
|
14
|
+
narration.*
|
|
15
|
+
|
|
16
|
+
# Source documents: never commit copyrighted readings
|
|
17
|
+
samples/*.pdf
|
|
18
|
+
*.pdf
|
|
19
|
+
|
|
20
|
+
# OS
|
|
21
|
+
.DS_Store
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.12
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.2.0
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- **Resume after an interruption.** Every finished chunk is cached by exactly the
|
|
7
|
+
inputs that determine its audio, so a book that dies at chunk 900 of 1000
|
|
8
|
+
synthesizes 100 chunks on the next run, not 1000. `--no-cache` opts out,
|
|
9
|
+
`--clear-cache` empties it, and the dashboard reports and clears it too.
|
|
10
|
+
- **Folder conversion.** `pdftts shelf/` converts everything readable in a
|
|
11
|
+
directory, `-r` descends into sub-folders, and one unreadable file costs only
|
|
12
|
+
itself — the queue finishes and the failures are listed at the end.
|
|
13
|
+
- **Nine languages, 54 voices.** American and British English, Spanish, French,
|
|
14
|
+
Hindi, Italian and Brazilian Portuguese work on the base install; Japanese and
|
|
15
|
+
Mandarin need one extra each and say so instead of failing at synthesis time.
|
|
16
|
+
`--lang` picks one, `--list-languages` shows them, and the dashboard groups the
|
|
17
|
+
voice picker by language.
|
|
18
|
+
- **Cover art and book metadata in the m4b.** The EPUB jacket is attached as
|
|
19
|
+
cover art, and title, author, language and publication date are written as tags.
|
|
20
|
+
- **Kindle input.** `.mobi`, `.azw`, `.azw3` and `.prc` are unpacked into the
|
|
21
|
+
same XHTML an EPUB holds, so chapters, metadata and cover art come through one
|
|
22
|
+
code path. Pure Python — no Calibre, no external tools.
|
|
23
|
+
- **A Docker image**, built and smoke-tested in CI because the machine I develop
|
|
24
|
+
on has no daemon: it narrates a file and answers on a published port before
|
|
25
|
+
the build is called green. No OCR inside it — Vision is macOS-only.
|
|
26
|
+
- Continuous integration on Linux and macOS, including a check that the built
|
|
27
|
+
wheel actually contains the dashboard and the OCR helper.
|
|
28
|
+
- A release workflow that publishes to PyPI through a Trusted Publisher, so no
|
|
29
|
+
token is ever stored.
|
|
30
|
+
|
|
31
|
+
- **`--tunnel`: a phone on cellular, with nothing installed on it.** Publishes a
|
|
32
|
+
temporary `trycloudflare.com` address for the run and shows it as a QR on the
|
|
33
|
+
Phone tab. The address points at your own machine and dies with the process, so
|
|
34
|
+
two people running `pdftts` get two separate addresses to their own machines
|
|
35
|
+
rather than sharing anyone's server. It refuses to publish without a password,
|
|
36
|
+
generating one if you did not set it, and it ignores any `~/.cloudflared`
|
|
37
|
+
config it finds — otherwise it silently runs *that* tunnel instead.
|
|
38
|
+
- **A password for the dashboard.** `--password` (or `PDFTTS_PASSWORD`) gates
|
|
39
|
+
every route, not just the page — the audio, the library and the service worker
|
|
40
|
+
included. Any username is accepted; the password is what is checked, because a
|
|
41
|
+
single-user dashboard does not need a user list.
|
|
42
|
+
- **`--public-url`**, for when the dashboard sits behind a tunnel or reverse
|
|
43
|
+
proxy: the machine cannot discover its own public hostname, so it is told, and
|
|
44
|
+
the Phone tab offers it as the QR that works on cellular.
|
|
45
|
+
|
|
46
|
+
### Fixed
|
|
47
|
+
- **New did not start a new session.** The finished narration and its player
|
|
48
|
+
stayed on screen, and appeared under the pairing QR code on the Phone tab too.
|
|
49
|
+
New now clears the last render — it is already saved under Past — and the
|
|
50
|
+
player is scoped to the views it belongs to. A long paste is deliberately kept.
|
|
51
|
+
- **The pairing QR handed out a VPN address.** Asking the routing table for "my
|
|
52
|
+
address" returns the tunnel when a VPN owns the default route, which a phone on
|
|
53
|
+
the same wifi cannot reach. The LAN address and the mesh address are now told
|
|
54
|
+
apart and offered separately — the first for a phone in the house, the second
|
|
55
|
+
for one out on cellular.
|
|
56
|
+
- **The dashboard was dead in every browser.** The service-worker registration
|
|
57
|
+
referenced `refreshOffline` twelve lines before its `const` declaration, which
|
|
58
|
+
throws `ReferenceError` in the temporal dead zone and aborts the entire script:
|
|
59
|
+
no engine list, no voice list, and every button on the page inert. It looked
|
|
60
|
+
like a slow network. The callback is now a hoisted function declaration, and
|
|
61
|
+
the script is executed in a browser-shaped environment in the test suite so
|
|
62
|
+
this class of error cannot ship again.
|
|
63
|
+
- EPUB content is XHTML and was being parsed with the HTML parser, which warned
|
|
64
|
+
on every book and is the less reliable of the two.
|
|
65
|
+
- A Piper-style voice id passed to Kokoro selected a pipeline from its first
|
|
66
|
+
letter alone, so `en_US-amy-medium` narrated English text through the Spanish
|
|
67
|
+
phonemizer instead of being rejected.
|
|
68
|
+
- `language` was written as a container tag, where MP4 ignores it, and as a
|
|
69
|
+
two-letter code, which MP4 does not accept. It is now an ISO 639-2 code on the
|
|
70
|
+
audio stream.
|
|
71
|
+
- Chapter titles were taken from the first line of body text, so an illustrated
|
|
72
|
+
edition put eighty characters of prose — or a plate's caption — in the player's
|
|
73
|
+
chapter menu. They now come from the chapter's own heading, with any caption
|
|
74
|
+
ahead of the chapter marker trimmed off.
|
|
75
|
+
- Tables of contents and lists of illustrations were narrated as prose, arriving
|
|
76
|
+
mid-book as "Chapter: one, two, three, four...". They are recognised and
|
|
77
|
+
skipped.
|
|
78
|
+
- The dashboard re-probed the hardware on every page load — about a second of
|
|
79
|
+
"Checking what this machine can run…" before it could offer a choice. The
|
|
80
|
+
survey is computed once and warmed at startup.
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# Contributing
|
|
2
|
+
|
|
3
|
+
Bug reports and patches are welcome. This is a small project, so the bar is
|
|
4
|
+
simple: it should keep working on a laptop with no GPU and no network.
|
|
5
|
+
|
|
6
|
+
## Getting set up
|
|
7
|
+
|
|
8
|
+
```sh
|
|
9
|
+
git clone https://github.com/JosiahMcj/pdftts.git && cd pdftts
|
|
10
|
+
uv sync
|
|
11
|
+
uv run pytest
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
That is the whole loop. There is no build step and nothing to configure.
|
|
15
|
+
|
|
16
|
+
## What I look for in a change
|
|
17
|
+
|
|
18
|
+
- **A test that fails without it.** The tests here are mostly small and
|
|
19
|
+
text-only; they run in a couple of seconds and do not synthesize audio. If a
|
|
20
|
+
change needs a real engine to test, fake the engine — `tests/test_cache.py` has
|
|
21
|
+
one that counts calls.
|
|
22
|
+
- **Measurements over adjectives.** If a change makes something faster, say how
|
|
23
|
+
much faster on what hardware. I would rather ship a blank number than an
|
|
24
|
+
estimate presented as a measurement; the README leaves MisoTTS's speed empty
|
|
25
|
+
for exactly this reason.
|
|
26
|
+
- **No new required dependency** for something that only some users need. Put it
|
|
27
|
+
behind an extra, like `piper`, `chatterbox`, `ja` and `zh` already are, and
|
|
28
|
+
make the failure message name the extra.
|
|
29
|
+
- Comments that explain *why*. The code says what it does.
|
|
30
|
+
|
|
31
|
+
## Things that are deliberate, not oversights
|
|
32
|
+
|
|
33
|
+
- **OCR is macOS-only.** It calls Apple's Vision framework through a small Swift
|
|
34
|
+
helper. On other platforms a scanned PDF is refused with a clear message rather
|
|
35
|
+
than silently producing an empty file. A Tesseract path would be welcome as
|
|
36
|
+
long as it stays optional.
|
|
37
|
+
- **Column detection assumes one body column.** True two-column journal layouts
|
|
38
|
+
extract in visual order. Fixing that properly means real reading-order
|
|
39
|
+
detection, not a heuristic.
|
|
40
|
+
- **The dashboard has no authentication.** It binds to `127.0.0.1` by default and
|
|
41
|
+
`--lan` says plainly what it exposes. Adding accounts would be a bigger project
|
|
42
|
+
than this is.
|
|
43
|
+
|
|
44
|
+
## Reporting a bug in the audio
|
|
45
|
+
|
|
46
|
+
Extraction bugs are hard to see and easy to hear. If something reads wrong,
|
|
47
|
+
`pdftts yourfile.pdf --dry-run` prints the exact text that would have been
|
|
48
|
+
spoken — that output, plus the page it came from, is the most useful bug report.
|
pdftts-0.2.0/Dockerfile
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# The dashboard and CLI, minus OCR.
|
|
2
|
+
#
|
|
3
|
+
# OCR calls Apple's Vision framework through a Swift helper, so it cannot cross
|
|
4
|
+
# to Linux; a scanned PDF in this image is refused with a clear message rather
|
|
5
|
+
# than silently producing nothing. Everything else — every input format, every
|
|
6
|
+
# output format, Kokoro and Piper, the resume cache, the phone connector — works.
|
|
7
|
+
#
|
|
8
|
+
# docker build -t pdftts .
|
|
9
|
+
# docker run --rm -p 8765:8765 -v "$PWD:/books" pdftts --serve --lan
|
|
10
|
+
# docker run --rm --user "$(id -u):$(id -g)" -v "$PWD:/books" pdftts /books/novel.epub --m4b
|
|
11
|
+
#
|
|
12
|
+
# `--user` matters whenever output is written back to a bind mount: the files
|
|
13
|
+
# belong to whoever runs the container, and without it they are written as this
|
|
14
|
+
# image's own user — which usually cannot write to your directory at all.
|
|
15
|
+
#
|
|
16
|
+
# Models and finished chunks live in named volumes, so a rebuilt container does
|
|
17
|
+
# not re-download Kokoro or re-synthesize what it already has:
|
|
18
|
+
# docker run --rm --user "$(id -u):$(id -g)" \
|
|
19
|
+
# -v pdftts-models:/cache/huggingface -v pdftts-cache:/cache/pdftts \
|
|
20
|
+
# -v "$PWD:/books" pdftts /books/novel.epub
|
|
21
|
+
|
|
22
|
+
FROM python:3.12-slim AS build
|
|
23
|
+
|
|
24
|
+
# Torch ships a CUDA build by default and it is gigabytes of driver this image
|
|
25
|
+
# will never use. The CPU index keeps the layer to a fraction of that.
|
|
26
|
+
ENV UV_EXTRA_INDEX_URL=https://download.pytorch.org/whl/cpu \
|
|
27
|
+
UV_INDEX_STRATEGY=unsafe-best-match \
|
|
28
|
+
UV_LINK_MODE=copy \
|
|
29
|
+
UV_PYTHON_DOWNLOADS=never
|
|
30
|
+
|
|
31
|
+
COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv
|
|
32
|
+
|
|
33
|
+
WORKDIR /src
|
|
34
|
+
COPY pyproject.toml README.md ./
|
|
35
|
+
COPY src ./src
|
|
36
|
+
COPY web ./web
|
|
37
|
+
COPY vendor ./vendor
|
|
38
|
+
RUN uv venv /opt/venv \
|
|
39
|
+
&& VIRTUAL_ENV=/opt/venv uv pip install --no-cache . \
|
|
40
|
+
# Kokoro's English phonemiser calls spacy.cli.download() the first time it runs,
|
|
41
|
+
# which needs pip or uv and a network. Neither belongs in the runtime image, so
|
|
42
|
+
# the model is baked in here and the download never happens.
|
|
43
|
+
&& VIRTUAL_ENV=/opt/venv uv run --no-project python -m spacy download en_core_web_sm \
|
|
44
|
+
&& VIRTUAL_ENV=/opt/venv uv run --no-project python -c \
|
|
45
|
+
"import spacy.util, sys; sys.exit(0 if spacy.util.is_package('en_core_web_sm') else 'spacy model missing')" \
|
|
46
|
+
&& find /opt/venv -name '__pycache__' -type d -prune -exec rm -rf {} +
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
FROM python:3.12-slim
|
|
50
|
+
|
|
51
|
+
# espeak-ng is Kokoro's fallback phonemiser; ffmpeg is needed for anything that
|
|
52
|
+
# is not a WAV. Both are small next to the model weights.
|
|
53
|
+
RUN apt-get update \
|
|
54
|
+
&& apt-get install -y --no-install-recommends ffmpeg espeak-ng \
|
|
55
|
+
&& rm -rf /var/lib/apt/lists/*
|
|
56
|
+
|
|
57
|
+
# Not root: this container is handed a directory of the user's own books.
|
|
58
|
+
# The caches sit outside any home directory and are world-writable, so the image
|
|
59
|
+
# still works when it is run as the caller's own uid — which is how anyone
|
|
60
|
+
# writing output back to a bind mount has to run it.
|
|
61
|
+
RUN useradd --create-home --uid 1000 reader \
|
|
62
|
+
&& mkdir -p /cache/huggingface /cache/pdftts /data \
|
|
63
|
+
&& chmod 1777 /cache /cache/huggingface /cache/pdftts /data
|
|
64
|
+
COPY --from=build /opt/venv /opt/venv
|
|
65
|
+
ENV PATH="/opt/venv/bin:$PATH" \
|
|
66
|
+
HF_HOME=/cache/huggingface \
|
|
67
|
+
XDG_CACHE_HOME=/cache \
|
|
68
|
+
XDG_DATA_HOME=/data \
|
|
69
|
+
HOME=/cache
|
|
70
|
+
|
|
71
|
+
USER reader
|
|
72
|
+
WORKDIR /books
|
|
73
|
+
EXPOSE 8765
|
|
74
|
+
|
|
75
|
+
# --serve alone binds 127.0.0.1, which is unreachable from outside a container.
|
|
76
|
+
# --lan is what makes a published port work, and it says what it exposes.
|
|
77
|
+
ENTRYPOINT ["pdftts"]
|
|
78
|
+
CMD ["--serve", "--lan"]
|
pdftts-0.2.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Josiah McJunkin
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|