Library / SDK
oomol-lab/epub-translator avatar
oomol-lab/epub-translator

epub-translator: bilingual EPUB books by appending, not by replacing

Translate EPUB books using Large Language Models while preserving the original text. The translated content is displayed side-by-side with the original, creating bilingual books perfect for language learning and cross-reference reading.

875 stars71 forksPythonMIT

At a glance

What is it?
epub-translator is a MIT licensed Python package that takes an EPUB and produces a bilingual edition, with the translation appended beside the original text rather than substituted for it. Its design is built around three insertion modes, a configurable token budget per group, and the option to drive translation and XML filling with two different models.
Who is it for?
Adopt epub-translator if you read foreign language books and want the original text kept intact next to a machine translation, or you are producing bilingual editions for study, because the three submit modes let you choose between a single-language result and two different bilingual layouts without rewriting the pipeline.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 40 days ago.
What is it written in?
Mainly Python, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 25, 2026, and from our analysis. They are not legal advice.

Editorial analysis

Keeping the original text is the whole point

EPUB Translator exists to answer a specific reading problem: you want to read a book in a foreign language without losing the original context. It transforms an EPUB into a bilingual edition in which the AI translation is displayed side by side with the original text, and the README is explicit that all formatting, images and structure are preserved.

That is a different product from machine translation, and the difference shows up in the interface. The translate function takes a submit parameter that controls how the translated content is inserted, and the three modes are the design centre of the library.

SubmitKind.REPLACE replaces the original content with the translation, producing a single-language book. SubmitKind.APPEND_TEXT appends the translation as inline text immediately after the original, so both languages sit in the same paragraph and the reading flow is continuous. SubmitKind.APPEND_BLOCK appends the translation as separate block elements after the original, and the README marks this one as recommended, because the clear visual separation between languages is what makes it suitable for side-by-side bilingual reading.

The target audience is stated in three ways in the README: people learning a new language, people doing academic research, and people who simply enjoy foreign literature. That is a wide spread of users with a common need, and it is a good sign for a library that the two output shapes cover both the study case and the reading case.

The project is MIT licensed, published on PyPI as epub-translator, and the last push was on 2026-09-18. The releases are small version numbers, v0.1.8 and v0.1.9 on 2026-02-07 and v0.1.10 on 2026-05-08, so this is a young package with a young API.

Two objects make up the whole API

The public surface is small enough to read in a minute, and the README documents both objects in full.

The LLM class is the client configuration. It takes the API key, the endpoint URL, the model name, the token encoding, and then a set of operational options: a cache directory, a request timeout, top_p, temperature, the number of retries on failure with a default of 5, the interval between those retries at 6.0 seconds, and a log directory.

python
LLM(
    key: str,                          # API key
    url: str,                          # API endpoint URL
    model: str,                        # Model name (e.g., "gpt-4")
    token_encoding: str,               # Token encoding (e.g., "o200k_base")
    cache_path: PathLike | None = None,           # Cache directory path
    timeout: float | None = None,                  # Request timeout in seconds
    top_p: float | tuple[float, float] | None = None,
    temperature: float | tuple[float, float] | None = None,
    retry_times: int = 5,                         # Number of retries on failure
    retry_interval_seconds: float = 6.0,          # Interval between retries
    log_dir_path: PathLike | None = None,         # Log directory path
)

The token encoding parameter is the one that shows the author has thought about model churn. A different model family counts tokens differently, and getting that wrong means the budget you think you set is not the budget the provider sees.

The translate function then takes the source and target paths, the target language, the submit mode, and the model or models. The full signature also carries a custom user prompt, the maximum retries for failed translations, the maximum tokens per translation group, the concurrency, the progress callback, and a callback for fill failures.

python
translate(
    source_path: PathLike | str,       # Source EPUB file path
    target_path: PathLike | str,       # Output EPUB file path
    target_language: str,              # Target language (e.g., "English", "Chinese")
    submit: SubmitKind,                # How to insert translations (REPLACE, APPEND_TEXT, or APPEND_BLOCK)
    user_prompt: str | None = None,    # Custom translation instructions
    max_retries: int = 5,              # Maximum retries for failed translations
    max_group_tokens: int = 2600,      # Maximum tokens per translation group
    concurrency: int = 1,              # Number of concurrent translation tasks (default: 1)
    llm: LLM | None = None,            # Single LLM instance for both translation and filling
    translation_llm: LLM | None = None,  # LLM instance for translation (overrides llm)
    fill_llm: LLM | None = None,       # LLM instance for XML filling (overrides llm)
    on_progress: Callable[[float], None] | None = None,  # Progress callback (0.0-1.0)
    on_fill_failed: Callable[[FillFailedEvent], None] | None = None,  # Error callback
)

The two-model option is the most interesting feature here. Translation and XML filling are different jobs, and the README says that using separate LLMs allows for task-specific optimization. Filling is the step that puts the translated text back into the document structure, and it is arguably the part where a stronger, more expensive model pays off, while the translation step might run on a cheaper one.

Grouping, token budgets and the math problem

Translating a book is not translating a sentence. The library has to decide what to send to the model in one request, and the parameter that governs that is max_group_tokens, defaulting to 2600.

The dependency list shows the machinery behind that choice. The resource-segmentation package is what breaks an EPUB document into translatable units, and tiktoken provides the token counting that the budget is measured in. Together they let the library group smaller elements up to a token ceiling and then translate each group, which is what keeps the surrounding text coherent instead of translating each paragraph in isolation.

A group of 2600 tokens is a deliberate middle ground, and it is the number you will want to change first for a different model. A group that is too small loses context, so the model cannot resolve a pronoun or a term that was introduced two paragraphs earlier. A group that is too large risks a slower response, a higher cost per request and, on some providers, a hard failure that the retry logic cannot fix.

The other dependency worth naming is mathml2latex, which converts MathML to LaTeX. That is a small detail with a large audience, because mathematical notation in EPUB is stored as MathML, and a language model asked to translate a paragraph containing mathematics will usually do the wrong thing with it unless the markup is rewritten first. Handling that conversion before translation is the kind of domain detail that separates a library that has been used on real books from a demo.

Jinja2 is also in the dependency list, which is how the prompt templates are rendered, and the openai client is the transport. There is no provider abstraction beyond the URL and key parameters, so a non-OpenAI endpoint is a matter of pointing the client at a compatible server.

Retries, caching and the two failure paths

Long-running LLM jobs fail in two distinguishable ways, and the library treats them differently.

The first is a request failure: a timeout, a rate limit, a server error. That is handled by the LLM configuration, with a retry count defaulting to 5 and an interval between attempts of 6.0 seconds. The cache path is part of the same object, so a re-run of a chapter that already succeeded can reuse the earlier result instead of paying for it again, which matters when a long book is interrupted halfway.

The second is a fill failure, and it is the one that produces the on_fill_failed callback with a FillFailedEvent. Filling is the step where the translated text is written into the XML structure, and it can fail on a document that the translation step handled fine, because the structure may contain markup the model returned in a form the writer cannot place. A separate callback means a caller can log those, count them, or abort, rather than discovering a silently broken chapter at the end of a long run.

The retry count for failed translations, max_retries, is a separate parameter on the translate function with the same default of 5, and it covers the case where the model returns something unusable for a group rather than a transport level error.

The log directory parameter rounds this out. A book translation is a long job that will be run more than once while the prompts are tuned, and having the raw request and response logs on disk is what makes a bad translation diagnosable instead of mysterious. The presence of both a cache and a log directory suggests the author expects exactly that iteration.

One thing the library does not do is judge quality. There is no validation pass and no quality score, which is a reasonable boundary for a tool whose output is meant to be read by a person who can check the original text sitting next to it.

Progress reporting and concurrency, deliberately serial by default

The progress callback is the piece that makes the tool usable in a pipeline, and its shape is simple: on_progress takes a float between 0.0 and 1.0.

The README shows the intended integration with tqdm, where the callback is wrapped to update a progress bar:

python
from tqdm import tqdm

with tqdm(total=100, desc="Translating", unit="%") as pbar:
    last_progress = 0.0

    def on_progress(progress: float):
        nonlocal last_progress
        increment = (progress - last_progress) * 100
        pbar.update(increment)
        last_progress = progress

Two details in that snippet are worth noting. The progress is a fraction, and the example converts it to a percentage delta, which means the callback is cumulative rather than per group, and a consumer has to track the previous value itself. And the callback is optional, so the same call works in a script where nobody is watching.

The concurrency parameter defaults to 1, which means translation requests are issued one at a time unless you ask for more. For a rate-limited provider that is the right default, and for a self-hosted model on a local machine it leaves throughput on the table. A book is a lot of groups, so raising concurrency is the obvious speedup, at the cost of the retry logic having to cope with several failures at once.

The user_prompt parameter is the other lever worth mentioning. It takes custom translation instructions, which is where you would tell the model what to do with footnotes, front matter, dialogue markers or a glossary of terms. Since the prompt is applied per translation, that is a per-book configuration rather than a per-model one.

Packaging, the release mismatch, and the two ways in

The install is a single command from PyPI, and the supported interpreters are Python 3.11, 3.12 and 3.13.

bash
pip install epub-translator

The project metadata reflects a careful package. The build backend is poetry-core, the version and description are static in the project file, the licence is MIT, and the development status is marked as beta. The dev group carries pylint, tqdm, ruff, pytest and pyright, so linting, formatting, typing and tests are all configured, and the repository holds a poetry.lock for reproducible environments, a .pylintrc and a format.template.json that presumably holds the prompt template used for the translation calls.

There is a version discrepancy worth knowing about. The releases listed for the repository are v0.1.10 from 2026-05-08 and v0.1.9 and v0.1.8 from February 2026, while the project file declares version 0.1.11. So the version in the source tree is ahead of the newest tagged release, which usually means the tag is what you get from PyPI and the number in the repository is the work in progress. If you pin epub-translator, pin the release, not the file.

There are two ways to use the tool that the README recommends and offers. The first is OOMOL Studio, the visual environment the project is published into, which has a tutorial video. The second is the online version at pdfcraft.ai/epub-translator, described as the official online app for the same bilingual EPUB translation workflow, where you upload an EPUB and try the main experience in the browser. The library is the third option, and the one to use if you want to run it over a collection of books rather than one at a time.

The README also has a Chinese translation of its own, which is worth noting for a package whose purpose is reading books in other languages.

Editorial conclusion

Adopt epub-translator if you read foreign language books and want the original text kept intact next to a machine translation, or you are producing bilingual editions for study, because the three submit modes let you choose between a single-language result and two different bilingual layouts without rewriting the pipeline. Do not adopt it for high-stakes published translation, since the output is produced by a language model and the tool applies no quality gate beyond its own retry and fill logic, and do not assume it handles every EPUB structure, since the README does not describe its coverage of the format. Before you run it on a full book, check the token budget against your model, since a group of 2600 tokens that suits one model may be too large for another, and read the changelog for the version you install, because the package version in the project file is ahead of the latest tagged release.

Frequently asked questions

How do I install and use the epub-translator package?

The README gives pip install epub-translator, with Python 3.11, 3.12 or 3.13 required. You then build an LLM object with your key, URL, model and token encoding, and call translate with a source path, a target path, a target language and a submit mode.

What are the three submit modes in epub-translator?

SubmitKind.REPLACE replaces the original content with the translation to make a single-language book, SubmitKind.APPEND_TEXT appends the translation inline in the same paragraph, and SubmitKind.APPEND_BLOCK appends it as separate block elements, which the README recommends for side-by-side bilingual reading.

Can I use different models for translation and for filling the EPUB XML?

Yes. The translate function takes a translation_llm and a fill_llm in addition to a single llm parameter, and the README notes that using separate LLMs allows for task-specific optimization. Either llm, or both translation_llm and fill_llm, must be provided.

How does epub-translator handle failures and long jobs?

The LLM object configures a retry count of 5 with a 6.0 second interval, plus a cache directory, a timeout and a log directory. The translate function adds max_retries of 5, an on_progress callback taking a value from 0.0 to 1.0, and an on_fill_failed callback that reports failures in the XML filling step.

Is there a hosted version of epub-translator?

Yes. The README points to an online app at pdfcraft.ai/epub-translator for uploading an EPUB and trying the bilingual translation workflow in the browser, and it also links an OOMOL Studio package with a tutorial video. The project itself is MIT licensed and published on PyPI.

Official sources

  1. License: MIT
  2. oomol-lab/epub-translator on GitHub
  3. Project website
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/oomol-lab-epub-translator.svg)](https://hysenlabs.com/projects/oomol-lab-epub-translator)

Was this useful?