Live demo · 中文说明 · Installation · Usage · Translation · Limitations
Builds facing-text bilingual EPUBs. Each paragraph is followed by its translation, blurred until tapped, so the original can be read first and the translation consulted only when needed.
Works with any standard EPUB and any language pair. Chapter structure and paragraph pairing are derived from the files themselves — no per-book configuration. All processing is local.
- merge two monolingual editions into one bilingual book
- split a bilingual book back into separate languages
- remerge an existing bilingual book with different settings
- translate the missing side via a model API, or via a coding agent
- Command line, terminal wizard, and local web interface, in English or Chinese
Installation
pip install bilingual-epub-toolkit
Chinese Traditional ↔ Simplified conversion requires opencc:
pip install "bilingual-epub-toolkit[chinese]"
Requires Python 3.9 or later. The only runtime dependency is lxml.
Three commands are installed:
| Command | Interface |
|---|---|
bilingual-epub |
command line |
bilingual-epub-tui |
terminal wizard, prompts instead of flags |
bilingual-epub-web |
local web page with drag-and-drop |
Interface language follows the system locale and can be set with --lang en
or --lang zh.
Quick start
Two sample books ship with the repository:
bilingual-epub merge --a examples/sample-en.epub --b examples/sample-fr.epub --out demo.epub
Open demo.epub in Apple Books or any EPUB 3 reader. The samples are an
original short story written for this project, so no third-party rights are
involved. A hosted instance is available at
epub.starry-files.duckdns.org for
trying the tool without installing it; uploads there are capped at 25 MB and
deleted after 30 minutes.
Usage
merge
Combine two monolingual editions:
bilingual-epub merge \
--a english.epub --b french.epub --out bilingual.epub \
--blur-side b --blur 0.25em
| Option | Effect |
|---|---|
--blur-side a|b |
which side is hidden until tapped (default b) |
--no-blur |
plain facing text, nothing hidden |
--blur |
CSS length; em units scale with the reader's font size |
--title, --author |
override the combined metadata |
--convert-side, --convert |
opencc script conversion, e.g. --convert-side b --convert tw2sp |
A per-chapter table is printed after each merge, reporting how many paragraphs paired one to one.
split
Separate a bilingual book by language:
bilingual-epub split --in bilingual.epub --out-dir ./split/ --langs en,fr
Language is read from each block's lang attribute. Omitting --langs writes
out every language found.
remerge
Re-render an existing bilingual book with different settings:
bilingual-epub remerge --in old.epub --out new.epub --blur-side a --blur 0.35em
Equivalent to a split followed by a merge. Also converts bilingual books from other sources into this tap-to-reveal format.
Translation
When only one edition exists, the second can be generated. Both routes produce a translation with the same block count and order as the source, so the subsequent merge pairs every paragraph exactly.
With a coding agent
Installs a skill that lets an agent such as Claude Code drive the toolkit, translating on an existing subscription rather than a metered API:
bilingual-epub skill # writes .claude/skills/bilingual-epub/SKILL.md
The skill file is also available from the hosted instance.
The underlying commands can be used directly:
bilingual-epub export-text --in book.epub --out book.json
# translate book.json into translated.json
bilingual-epub import-text --export book.json --text translated.json \
--out book.zh.epub --lang zh
import-text accepts a JSON array of strings, a JSON object with a blocks
list, or plain text with one block per line. Files whose block count differs
from the source are rejected.
With a model API
bilingual-epub translate --in book.epub --out book.zh.epub --to zh \
--base-url https://api.deepseek.com/v1 --api-key "$KEY" --model deepseek-chat
| Option | Effect |
|---|---|
--dialect openai |
/chat/completions; OpenAI, DeepSeek, Moonshot, Zhipu, SiliconFlow, OpenRouter, Groq, Together, Ollama, LM Studio, vLLM (default) |
--dialect anthropic |
/v1/messages |
--dry-run |
report block, character and request counts without sending anything |
--batch-size |
paragraphs per request (default 20) |
--cache |
progress file; an interrupted run resumes from it |
Credentials may also be supplied through BILINGUAL_API_KEY, OPENAI_API_KEY
or ANTHROPIC_API_KEY. Batches returning the wrong number of blocks are
retried, then subdivided, and are never accepted as-is.
Self-hosting
The web interface defaults to a single trusted user on localhost. --public
adapts it for a shared host:
bilingual-epub-web --public --port 8799
Public mode rejects server-side file paths, isolates each visitor's files,
rate-limits by address, caps uploads at 25 MB, and removes idle sessions after
30 minutes (--ttl).
Cloudflare Turnstile can be enabled to filter automated traffic while keeping the service open to anyone:
export TURNSTILE_SITEKEY=0x...
export TURNSTILE_SECRET=0x...
bilingual-epub-web --public
Jobs are then verified before running, and refused if Cloudflare is unreachable. Without keys, no widget is rendered and no verification occurs.
How it works
Chapter boundaries are located by reading the EPUB container, OPF, manifest and spine, then selecting the heading level that recurs at chapter frequency.
Paragraph pairing uses Gale–Church alignment: a length-based statistical model solved by dynamic programming, weighted so that co-occurring headings anchor the sequence. It handles one-to-one pairs as well as paragraphs split or merged in translation.
Every output block carries a lang attribute, which is what allows split to
reverse a merge. The tap-to-reveal behaviour uses CSS :target with a small
progressive-enhancement script, degrading to plain visible text where neither
is supported.
Limitations
| Limitation | Behaviour |
|---|---|
| DRM-protected files | Encrypted EPUBs yield no extractable text; the tool reports this and stops. |
| Books without headings | Degrade to a single chapter. Text is preserved; chapter divisions are not. |
| Statistical alignment | Editions that add, cut or restructure text pair imperfectly. The printed table reports the rate. |
| Untagged bilingual books | split requires per-paragraph lang attributes. Books lacking them cannot be separated. |
No EPUB files are included in this repository apart from the generated samples,
and *.epub is gitignored.
Related projects
BookAlign attacks the same pairing problem from the other side: multilingual embeddings (LaBSE) plus dynamic programming, comparing what paragraphs mean rather than how long they are. That reaches the case the table above cannot — a pairing that is wrong but unambiguous, where the lengths happen to line up and a length model stays confident. It is aimed at novels, particularly Japanese originals against Chinese translations, and it runs a review-first workflow rather than one pass end to end.
The two are complementary. This one needs no model and runs anywhere; that one reads the text, at the cost of a local LaBSE model and realistically a GPU. If your two editions differ enough that length alone mispairs them, start there.
Development
git clone https://github.com/StarryGuli/bilingual-epub-toolkit.git
cd bilingual-epub-toolkit
pip install -e ".[dev,chinese]"
pytest && ruff check .
Test fixtures are synthetic EPUBs constructed in code rather than checked-in
book files; see tests/conftest.py. Coverage includes
the three operations end to end, both cover image formats, the translation
round trip against a stub API, public-mode isolation and rate limiting, and
the error paths.
Coverage and conventions are described in CONTRIBUTING.md.
License
Apache-2.0. See LICENSE.
Metadata
Release files for bilingual-epub-toolkit 0.1.2
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| bilingual_epub_toolkit-0.1.2.tar.gz | 3.6 MB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| bilingual_epub_toolkit-0.1.2-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 3.7 MB
Release files / bilingual_epub_toolkit-0.1.2.tar.gz
| Download URL | bilingual_epub_toolkit-0.1.2.tar.gz |
|---|---|
| Size | 3.6 MB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
aef096aa42626cb52673694090fea1d8c8639c63992430789eab0da5d7a3f9ae
|
|
BLAKE2b-256 checksum How to use checksums |
605b458994ef459498b9d6786e7730aa177b454cdf50342f047be7ddc374ac04
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 13, 2026.
Transparency logRelease files / bilingual_epub_toolkit-0.1.2-py3-none-any.whl
| Download URL | bilingual_epub_toolkit-0.1.2-py3-none-any.whl |
|---|---|
| Size | 96.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
3d4c251f757dd02cc4d89e118c77131bb32aa56bf27aaed2233dca69e868ac77
|
|
BLAKE2b-256 checksum How to use checksums |
9141dc3c035a5ac8fef175d95aff56efe5609a0ae2c5c39472df8b089e9fcc34
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 13, 2026.
Transparency log