Technical Documentation: yihong0618/bilingual_book_maker
ℹ️ Provenance: Multi-Tier Ingestion · CodeWiki Reference
Overview (`README.md`)
Bilingual Book Maker
中文 | English
The bilingual_book_maker is an AI translation tool that uses ChatGPT to assist users in creating multi-language versions of epub/txt/md/srt/pdf files and books. Use it only with material you have the right to translate — works for which you hold the necessary rights, suitably licensed or permitted works, public-domain books, or uses otherwise allowed by applicable law. Before using this tool, please review the project's disclaimer.
Guides for every file type and option, and the measurements behind the defaults, are on the wiki. If you use a coding agent (Codex, Claude Code), the repository's skill translates a book for you: Translate with an agent.
Supported endpoints
OpenAI and Anthropic format endpoints are supported.
Usually it comes with three fields, two if you are using the official endpoints, such as gpt-6-luna (the default)
or claude-sonnet-4-6.
Specify openai, or anthropic at --api_format for API request formats.
This argument also supports selecting some translation services (google, caiyun, deepl, deeplfree,tencent, customapi — not an OpenAI format) or codex
if you want to use your Codex quota instead.
--provider is an alternative way to pass credentials, through a JSON config filebbm_providers.json.
Epub tags classification is auto enabled on JSON-schema endpoints, and on any endpoint that can hold a conversation — the codex route and plain reseller proxies included — where the model is asked for exact skip/translate verdicts instead. Only routes with no conversation at all (the MT engines) fall back to translating p tags only, so some poetry or verse may be omitted there. See Plan mode for details.
Older flags (--model gpt4o,--model gemini, --openai_key, …) still work: see
Models and languages.
Preparation
- ChatGPT or OpenAI token [^token]
- epub/txt/md/pdf books
- Environment with internet access or proxy
- Python 3.10+
Quick Start
A sample book, test_books/animal_farm.epub, is provided for testing purposes.--test translates only its first few paragraphs.
pip install -r requirements.txt # or: pip install -U bbook_makerThen:
cp bbm_providers.example.json bbm_providers.json
# edit base_url, default_models and env_key in ./bbm_providers.json
python3 make_book.py --book_name test_books/animal_farm.epub --provider openai --test --use_context sessionYou can also pass the key on the command line:
python3 make_book.py --book_name test_books/animal_farm.epub \
--key sk-... --model gpt-6-luna --api_base https://api.openai.com/v1 --test --use_context sessionTo spend a Codex subscription:
python3 make_book.py --book_name test_books/animal_farm.epub --model gpt-6-luna --api_format codex --testOr hand it to a coding agent
git clone https://github.com/yihong0618/bilingual_book_maker.git
cd bilingual_book_maker
codex "Hi, please use bbm-plan to translate this book: test_books/animal_farm.epub into a bilingual Chinese-English edition, thanks."Endpoint flags
--api_formatnames the API the endpoint speaks:openai,anthropic,gemini,qwen,groq,xai,litellm,codex, or one of the
translation services (google,caiyun,deepl,deeplfree,tencent,customapi). A format that belongs to one vendor already
knows that vendor's address, so the format and a--keyare a whole
command.- Any other OpenAI-compatible API:
--api_base(ending in/v1),--keythe API key, and the model id in--model. Omit--api_basefor
OpenAI's own API, and--modelforgpt-6-luna. - Or translate through
--provider:bbm_providers.example.jsonhas an
entry for each vendor below (Gemini, Qwen, xAI, Groq, OrcaRouter, Ollama,
LiteLLM, SiliconFlow, OpenRouter). Copy it tobbm_providers.json, set the key in it, and--provider geminiuses the
Gemini API from it. --use_context sessiontranslates in session mode; the history compacts
at 8k by default (--context-compact-atoverrides). It keeps one cached
history for consistency, so recurring names stay stable across the book,
and can also learn a glossary from its own handoff reports
(--glossary-auto on, off by default) — the recommended mode on
OpenAI-compatible endpoints, and what the examples below use.- The old preset names and key flags still work, see
Migrating from the old flags.
Supported translation services
DeepL
Support DeepL model DeepL Translator need pay to get the tokentextpython3 make_book.py --book_name test_books/animal_farm.epub --api_format deepl --key ${deepl_key}DeepL free
shellpython3 make_book.py --book_name test_books/animal_farm.epub --api_format deeplfree-
A
claude-*model id selects the anthropic format on its own.shellpython3 make_book.py --book_name test_books/animal_farm.epub --model claude-sonnet-4-6 --key ${claude_key} Google Translate
shellpython3 make_book.py --book_name test_books/animal_farm.epub --api_format googleCaiyun Translate
shellpython3 make_book.py --book_name test_books/animal_farm.epub --api_format caiyun --key ${caiyun_key}Gemini
Google Gemini, over the Gemini
API itself. Name any Gemini model id; without--modelit isgemini-flash-latest.--intervalsets the pause between requests, which
is how the free tier's rate limit is stayed under.shellpython3 make_book.py --book_name test_books/animal_farm.epub --api_format gemini --key ${gemini_key} --model gemini-flash-latestQwen
Qwen-MT on DashScope, a
translation model: the request states a source and a target language.qwen-mt-turbo(the default) andqwen-mt-plusare supported, and--source_langstates the source language when auto-detection is not
wanted.shellpython3 make_book.py --book_name test_books/animal_farm.epub --api_format qwen --key ${qwen_key} --model qwen-mt-turbo --language "Simplified Chinese"-
shell
python3 make_book.py --book_name test_books/animal_farm.epub --api_format tencent -
shell
python3 make_book.py --book_name test_books/animal_farm.epub --api_format xai --key ${xai_key} --model grok-4.3 --use_context session -
The OrcaRouter gateway, defaulting to its
orcarouter/autosmart routing. The address comes with the route, so there
is no--api_base; the key is--keyorBBM_ORCAROUTER_API_KEY.--provider orcarouterreaches the same place.shellpython3 make_book.py --book_name test_books/animal_farm.epub --model orcarouter --key ${orcarouter_key} --use_context sessionTo name one model instead:
--provider orcarouter --model. -
Translate with Ollama self-hosted models.
If the ollama server is not local, point--api_base http://x.x.x.x:port/v1at it.shellpython3 make_book.py --book_name test_books/animal_farm.epub --api_base http://localhost:11434/v1 --model ${ollama_model_name} --use_context session -
--modelis required: GroqCloud's catalogue turns over, so pick a current
id from Supported Models.shellpython3 make_book.py --book_name test_books/animal_farm.epub --api_format groq --key [your_key] --model llama-3.3-70b-versatile --use_context session -
A LiteLLM proxy, which fans out to whatever backends its own config names.
--modelis the name that config gives one of them. The default address is
the proxy's own, on this machine; elsewhere it is--api_base.shellpython3 make_book.py --book_name test_books/animal_farm.epub --api_format litellm --model ${name_in_your_litellm_config} --use_context session -
Spend your ChatGPT/Codex plan. Install the
Codex CLI. The default model isgpt-6-luna;--api_format codex --modelnames another. One session is reused for the whole book and compacted at--context-compact-at;
it runs sandboxed, with shell, MCP servers and browsing off, but hooks may still fire.shellpython3 make_book.py --book_name test_books/animal_farm.epub --api_format codex --language zh-hans
Custom API Provider
When the built-in models do not cover your needs, define a provider in a JSON config file. Without a code change, any OpenAI-compatible or Anthropic-format API (SiliconFlow, a local proxy, ...) becomes usable.
Create bbm_providers.json in the current directory (or ~/.bbm/providers.json):
{
"providers": {
"siliconflow": {
"api_style": "openai",
"base_url": "https://api.siliconflow.cn/v1",
"default_models": ["Qwen/Qwen2.5-72B-Instruct"],
"env_key": "BBM_SILICONFLOW_API_KEY"
},
"openai": {
"api_style": "openai",
"base_url": "https://api.openai.com/v1",
"default_models": ["gpt-6-luna"],
"env_key": "OPENAI_API_KEY",
"prices": {
"gpt-6-luna": {"input": 0.10, "output": 0.50, "cached_input": 0.01}
}
}
}
}Config fields:
| Field | Required | Description |
|---|---|---|
api_style |
Yes | API request format: openai, anthropic, gemini, qwen, groq, xai or litellm |
base_url |
No | The API address. Omitted means the api_style's default address |
default_models |
No | Default model list. Required if --model is not provided |
env_key |
No | Environment variable name for API key. Required if --key is not provided |
prices |
No | Prices per million tokens, per model: {"<model id>": {"input": …, "output": …, "cached_input": …}}. When every model in the run has a price, the progress bar shows money spent (spent=$0.012) instead of token counts, and the closing line shows both. Without cached_input, cache reads are charged at the input price. A model without a price puts the bar back on tokens, and the closing line names it |
currency |
No | Currency code for the prices, default USD. USD, EUR, GBP, CNY and JPY print with their symbol; any other code prints after the amount, as in 0.500 CHF |
The spent amount and the token counts are estimates, accumulated from the usage each request reports — close enough to steer by, but the vendor's bill is the number that counts.
Priority: project-level ./bbm_providers.json overrides global ~/.bbm/providers.json.
--model names a model at that provider; without it the first of default_models is used.
python3 make_book.py --provider siliconflow --key sk-xxx --book_name test_books/animal_farm.epub --use_context session
export BBM_SILICONFLOW_API_KEY=sk-xxx
python3 make_book.py --provider siliconflow --book_name test_books/animal_farm.epub --use_context sessionUsage
- Once the translation is complete, a bilingual book named
${book_name}_bilingual.epubwould be generated for EPUB inputs; for TXT, MD and SRT inputs a bilingual file named${book_name}_bilingual.txt,${book_name}_bilingual.mdor${book_name}_bilingual.srtwill be generated. For PDF inputs the tool will produce a bilingual.txtfallback and will also attempt to create${book_name}_bilingual.epub— if EPUB creation fails, the TXT fallback remains so you do not need to retranslate. - If there are any errors or you wish to interrupt the translation by pressing
CTRL+C, a temporary bilingual file (for example{book_name}_bilingual_temp.epubor{book_name}_bilingual_temp.txt) would be generated. You can simply rename it to any desired name.
Params
--model:The model id, exactly as the endpoint spells it. On the OpenAI format the default is
gpt-6-luna. The second column is the--api_formatthe id needs:model --api_formatnotes gpt-6-lunaopenaithe default, at OpenAI's own address claude-sonnet-4-6anthropicAnthropic's own address gpt-4o-miniopenaiOpenAI gemini-flash-latestgeminithe default there, at Google's own address qwen-mt-turboqwenthe default there, on DashScope llama-3.3-70b-versatilegroqGroq's own address The old preset values still parse and are rewritten to a real model id with a note; Migrating from the old flags lists them. Anything else is an endpoint:
--api_base --key <key> --model, or a--providerentry (see the Custom API Provider section).--key:API key for the endpoint. Without the flag the key is read from
$BBM_API_KEY, then from the format's own variable. Same flag as--api_key.--api_format:The API the endpoint speaks. When omitted it is inferred: an
anthropic.comhost, or a model id containingclaudewith no--api_base, meansanthropic; anything else meansopenai. Pass it when the guess is wrong, to reach a vendor without typing its address, or to pick an engine.format key notes openai(default)required: --key, else$BBM_API_KEY,$OPENAI_API_KEY; not for a local address such as Ollamaany OpenAI-compatible endpoint: OpenAI itself, OpenRouter, Ollama and the rest, the address in --api_baseanthropicrequired: --key, else$BBM_API_KEY,$ANTHROPIC_API_KEYAnthropic itself, and gateways that speak the Messages API geminirequired: --key, else$BBM_API_KEY,$BBM_GOOGLE_GEMINI_KEY,$GEMINI_API_KEYthe Gemini API, default gemini-flash-latest; paced by--intervalqwenrequired: --key, else$BBM_API_KEY,$BBM_QWEN_API_KEY,$DASHSCOPE_API_KEYQwen-MT on DashScope, default qwen-mt-turbo; reads--source_langgroqrequired: --key, else$BBM_API_KEY,$BBM_GROQ_API_KEY,$GROQ_API_KEYGroqCloud; --modelrequiredxairequired: --key, else$BBM_API_KEY,$BBM_XAI_API_KEY,$XAI_API_KEYxAI; --modelrequiredlitellmnone for a proxy on this machine, else --keyor$LITELLM_MASTER_KEYa LiteLLM proxy, http://localhost:4000unless--api_basesays otherwise;--modelrequiredcodexnone: codex login(Codex CLI)the local codex app-serversidecar on a ChatGPT/Codex plan, defaultgpt-6-lunaorcarouterrequired: --keyor$BBM_ORCAROUTER_API_KEYOrcaRouter googlenone Google Translate, free caiyunrequired: --keyor$BBM_CAIYUN_API_KEYCaiyun deeplrequired: --keyor$BBM_DEEPL_API_KEYDeepL (paid) deeplfreenone DeepL free tier tencentnone Tencent TranSmart, free customapinone a {text, source_lang, target_lang}format API--interval:Seconds to wait between requests, e.g.
--interval 0.1for 100ms. Only--api_format geminipaces itself with it; every other route ignores it. Default:0.01.--test:Use
--testoption to preview the result if you haven't paid for the service. Note that there is a limit and it may take some time.--language:Set the target language: a tag (
--language zh-hant), a name (--language "Traditional Chinese"), or both at once —--language "zh-hant:Traditional Chinese". The tag names the JSON structured-output field; the name is what the model is asked for. Defaultzh-hans. See also available tags.--source_lang:Source language. Stated, it appends a prompt line ("Translate from English"), and on
--api_format qwen(whose request names a language pair) and--api_format customapiit rides in the request itself. Default: auto-detect.--proxy:Use
--proxyoption to specify proxy server for internet access. Enter a string such ashttp://127.0.0.1:7890.--resume:Use
--resumeoption to manually resume the process after an interruption. Mutually
exclusive with--parallel-workers.shellpython3 make_book.py --book_name test_books/animal_farm.epub --api_format google --resume--translate-tags:epub is made of html files. By default, we only translate contents in ``.
Use--translate-tagsto specify tags need for translation. Use comma to separate multiple tags.
For example:--translate-tags h1,h2,h3,p,div--plan-classify(epub only):Plan mode: classify epub tags with the translating model, or with codex / claude code.
The value decides how is translation decision of each tag made:
auto(default): when the book is an epub, ask the LLM what to translate. Only when the route cannot hold a conversation, and when the plan fails, translate the--translate-tagsselection instead. Rows decided over a plain session appear in_plan.jsonwith anunnamed (…)content type naming how the verdict was reached rather than what the content is.none: no plan; only the--translate-tagsselection — unselected, that defaults top, most body text.all: translate the whole partition, no classification.model: the translating LLM judges, then translates.--plan-classify-model Xpicks the model that classifies.agent: writes the classification plan for the book and prints instructions to paste into your coding tool for classification.
(or you could also do it by hand). Then run the translation with--plan-classify agentagain.--plan-dry-run: print the per-signature table, write_plan.json, and exit. Honors--only_filelist/--exclude_filelist._plan.json: the translation plan; delete it to classify again.--plan-min-coverage(default 0.5, range 0–1): plan mode aborts if the plan covers less than this fraction of the text.0disables the guard and values above0.9usually abort after classification is already paid for — both warn.--max-batch-units: the most units one grouped request may carry. Raise it together with--accumulated_numfor fewer, larger (cheaper) requests; lower them once the run prints degradation warnings such as the misalignment-recovery hint. Content is also bounded by the token budget (--accumulated_num).
shell# let the model judge which tags need translating python3 make_book.py --book_name my_book.epub --key ${key} --plan-classify model # or hand it to an agent: stops, prints instructions, then you give them to your AI python3 make_book.py --book_name my_book.epub --key ${key} --plan-classify agent--exclude-translate-tags:Use
--exclude-translate-tagsto exclude content within specified HTML tags from translation. This is useful for preserving code blocks, preformatted text, or other special content. Use comma to separate multiple tags.
Default:sup,code.
For example:--exclude-translate-tags code,preTip: Use
--exclude-translate-tags ""to translate all content including code blocks (overrides the default exclusion).--api_base:If you want to change api_base like using Cloudflare Workers, use
--api_baseto support it.
Note: the api url should be 'https://xxxx/v1'. Quotation marks are required.--allow_navigable_strings:If you want to translate strings in an e-book that aren't labeled with any tags, you can use the
--allow_navigable_stringsparameter. This will add the strings to the translation queue.--prompt:To tweak the prompt, use the
--promptparameter. The placeholders theusertemplate may use are{text}(required),{language}and{crlf}(a newline, for the shapes — a JSON string, a table cell — that cannot carry one). Anything else in braces is refused before the run starts rather than raising mid-book; write{{and}}for a literal brace. It supports a few ways to configure the prompt:If you don't need to set the
systemrole content, you can simply set it up like this:--prompt "Translate {text} to {language}."or--prompt prompt_template_sample.txtIf you need to set the
systemrole content, you can use the following format:--prompt '{"user":"Translate {text} to {language}", "system": "You are a professional translator."}'or--prompt prompt_template.json.A third key,
style, is a standing instruction about how to write — register, tone, vocabulary. It is said once where a window starts, not repeated on every request.--promptworks on every LLM route, and on srt books too — there its sections sit on top of the subtitle loader's own prompt, section by section. Replacing theusertemplate on an srt book means saying yourself that the block number and the timeline must come back unchanged; the run warns about it. The fixed translation-service routes (google, deepl, caiyun, tencent, qwen, customapi) send text and nothing else, so they carry no prompt at all and the run says so at start.A
.mdfile is read as the PromptDown block form — the format is theirs, the reader is ours, so no extra package is installed:--prompt prompt_md.prompt.md(example at ./prompt_md.prompt.md).## System Message, an optional## Style, and a## Conversationwhose**User:**turn is the template. The table form of a conversation is refused. Example:markdown# Translation Prompt ## System Message You are a professional translator who specializes in accurate translations. ## Conversation **User:** Please translate the following text into {language}: {text}You can also set the
userandsystemrole prompt by setting environment variables:BBM_CHATGPTAPI_USER_MSG_TEMPLATEandBBM_CHATGPTAPI_SYS_MSG.An example JSON file can be found at ./prompt_template.json.
--batch_size:Use the
--batch_sizeparameter to specify the number of lines for batch translation (default is 10, currently only effective for txt files).--accumulated_num:Wait for how many tokens have been accumulated before starting the translation.
For example, if you use--accumulated_num 1600, maybe openai will output 2200 tokens and maybe 200 tokens for other messages in the system messages user messages, 1600+2200+200=4000 — on some local models that is close to the limit. You have to choose your own value, there is no way to know if the limit is reached before sending.In EPUB plan mode this is a per-request token budget: consecutive units of any length share one request up to
Ntokens. Pass1to turn grouping off — every unit is sent on its own.--use_context:Translate with context.
--context_paragraph_limit:Use
--context_paragraph_limitto set a limit on the number of context paragraphs when using the--use_contextoption. This applies to window mode only.--use_context session:Session mode keeps one append-only history and re-reads it at the cache
price, so on endpoints that support caching the context can grow to about
a chapter. When the history reaches the compact budget, the model writes
a short handoff report (the run asks for ~300 tokens and truncates
anything runaway), whose summary seeds the next window;_handoff.md
holds the latest snapshot, overwritten at each compaction.
Watch the progress bar'scached=: if it is still zero after a dozen
requests, the endpoint may not have a cache; Ctrl+C and switch to window
mode.--context-compact-at:Session mode only. The estimated-token budget the whole window — the
inherited seed included — may reach before it is compacted into a
handoff report. Default8192, minimum1500: a window shorter than
that is mostly seed and seams, so below it use window mode instead.--no-context-compact:Session mode only. Skip the handoff report. The window still rolls over at the budget, but the next one starts empty instead of inheriting a summary. Cheaper, at the cost of continuity across the seam.
--glossary/--terminology:A file of
term → translationlines — one per line,#starts a note or a
comment, plain text.
Read by the openai- and codex-shaped routes for EPUB, Markdown, and PDF books.A pinned term makes the translation say what you pinned, so pin only
renderings you can stand behind.--glossary-auto on|off:Keep the renderings the handoff reports establish, so recurring names
stay unified across window seams. Session mode only, and off by
default: it relies on the model reporting its own renderings
accurately, which takes a capable model — most runs don't need it,
since the handoff summary already carries the recurring names.
--parallel-workers:Use
--parallel-workersto process EPUB chapters or Markdown batches/sections in
parallel. Values greater than1spin up multiple workers (recommended:2-4) and
automatically fall back to sequential mode when there is only one unit of work. Other
input loaders currently accept this shared CLI option but do not parallelize their work.--temperature:Sampling temperature for the openai and anthropic formats (the codex
format has none). For example:--temperature 0.7.--block_size:Use
--block_sizeto merge multiple paragraphs into one block. This may increase accuracy and speed up the process.
For example:--block_size 5.--single_translate:Use
--single_translateto output only the translated book without creating a bilingual version.--no_disclosure:An epub output adds "Translated by gpt-6-luna, 2026." below the book intro; this flag leaves it out. It also turns off the translation metadata (
--translation-metadata: the model, the date and the glossary).--translation_style:Apply custom CSS to translated EPUB text, for example
--translation_style "color: #808080; font-style: italic;".--translation_color:Shorthand for setting only the translated EPUB text color, for example
--translation_color "#1e90ff". If--translation_styleis also present, the full style
takes precedence.--pdf_layout {none,top-bottom,side-by-side,all}:Select additional bilingual PDF outputs for PDF inputs. The default
nonecreates no
extra PDF;allattempts both top-bottom and side-by-side layouts. The bilingual TXT and
EPUB outputs are unaffected.--to-epub,--pdf-ocr,--ocr-lang,--ocr-engine,--ocr-replace-layer,--pages,--device,--pdf-image-dpi(PDF only):Turn a PDF into a bilingual EPUB with a table of contents. See PDF to bilingual EPUB.
--sentence_mode:Translate EPUB text sentence by sentence instead of translating each paragraph as one
unit. It is incompatible with EPUB plan mode.--batch/--batch-use:Two-stage translation through the ChatGPT Batch API. Currently refused on EPUB
inputs: the queue path is unreachable there, so such a run would translate live at
full price and then submit an empty batch job instead of writing the book. Also refused
on routes that do not implement the Batch API.--quiet:Suppress EPUB progress bars and per-paragraph source/translation echoes while retaining
reports and errors. Recommended for log files and non-interactive agent runs.--retranslate "$translated_filepath" "file_name_in_epub" "start_str" "end_str":Retranslate from start_str to end_str's tag:
shellpython3 "make_book.py" --book_name "test_books/animal_farm.epub" --retranslate 'test_books/animal_farm_bilingual.epub' 'index_split_002.html' 'in spite of the present book shortage which' 'This kind of thing is not a good symptom. Obviously'To retranslate only the tag containing
start_str, pass an empty fourth argument:shellpython3 "make_book.py" --book_name "test_books/animal_farm.epub" --retranslate 'test_books/animal_farm_bilingual.epub' 'index_split_002.html' 'in spite of the present book shortage which' ''--no-thinking:Ask a reasoning model not to think before answering; on a paragraph of
prose it buys nothing and costs tokens and time.shellpython3 make_book.py --book_name test_books/animal_farm.epub --key ${openai_key} --no-thinking--extra_body:Pass additional JSON parameters on the routes built on the OpenAI request
path —openaiand the OpenAI-format custom providers — and on theanthropicroute. Provide a JSON object with the desired parameters.
Example:shellpython3 make_book.py --book_name test_books/animal_farm.epub --key ${openai_key} --extra_body '{"chat_template_kwargs": {"enable_thinking": false}}'--extra_headers:Extra HTTP headers sent with every request, on the same routes. Values
must be strings.shellpython3 make_book.py --book_name test_books/animal_farm.epub --key ${openrouter_key} --api_base https://openrouter.ai/api/v1 --model anthropic/claude-haiku-4.5 --extra_headers '{"HTTP-Referer": "https://example.com", "X-Title": "bilingual_book_maker"}'Common forms, for reference:
shell# openai route — disable a local/vLLM chat template's thinking block --extra_body '{"chat_template_kwargs": {"enable_thinking": false}}' # openai route (chat completions) — reasoning effort and a token ceiling, # neither of which has its own flag (both model-dependent) --extra_body '{"reasoning_effort": "low", "max_completion_tokens": 2000}' # anthropic route — keep extended thinking off; for translation it mostly # buys deviation from the source, not quality --extra_body '{"thinking": {"type": "disabled"}}' # OpenRouter attribution (shown on its dashboard) --extra_headers '{"HTTP-Referer": "https://example.com", "X-Title": "bilingual_book_maker"}' # a gateway's own auth or routing header (the value stays out of the logs) --extra_headers '{"X-API-Key": "sk-gateway-..."}'--provider:Use a custom provider defined in
bbm_providers.json;--modelpicks a model at it. See the "Custom API Provider" section above.--api_key:Same as
--key.
Examples
Note if use pip install bbook_maker all commands can change to bbook_maker args
/* Detailed source-code truncated for AI context efficiency. */More understandable example
python3 make_book.py --book_name 'animal_farm.epub' --key sk-XXXXX --api_base 'https://xxxxx/v1' --use_context session
# Or python3 is not in your PATH
python make_book.py --book_name 'animal_farm.epub' --key sk-XXXXX --api_base 'https://xxxxx/v1' --use_context sessionMicrosoft Azure Endpoints
python3 make_book.py --book_name 'animal_farm.epub' --key XXXXX --api_base 'https://example-endpoint.openai.azure.com/openai/v1' --model 'deployment-name' --use_context session
# Or python3 is not in your PATH
python make_book.py --book_name 'animal_farm.epub' --key XXXXX --api_base 'https://example-endpoint.openai.azure.com/openai/v1' --model 'deployment-name' --use_context sessionFeatures
Plan mode
An EPUB is translated through a plan by default: the book is split into units, the model decides by tag signature what to translate, so verse, lists and table cells are not missed, and neighbouring units share one request. --plan-classify chooses who decides: auto (the default), agent (you or your coding agent, through the plan file), all or none.
# preview what would be translated and what skipped (no key needed)
python3 make_book.py --book_name my_book.epub --plan-dry-run
# decide the plan yourself or with a coding agent, then rerun the same command to translate
python3 make_book.py --book_name my_book.epub --key ${key} --plan-classify agent--plan-dry-run honours --only_filelist / --exclude_filelist. --classify-model lets another model classify, jev for the Jev classifier; --classify-base-url, --classify-key and --classify-min-confidence go with it. More: Plan mode, Recommended settings for EPUB.
Session mode
--use_context session keeps one conversation for the whole book, so names and style stay consistent. The history is re-read at the cache price and, once it reaches --context-compact-at (default 8192 tokens), compacted into a short handoff report; --no-context-compact starts the next window empty instead. Best on endpoints with prompt caching. More: Session mode.
PDF to **bilingual** EPUB
--to-epub turns a PDF into a reflowable bilingual EPUB with a table of contents: every paragraph followed by its translation, figures and display formulas kept as pictures. It needs the PDF extra, installed from a clone, and Pandoc 3.1.12 or newer; see Installing the PDF extra.
pip install ".[pdf]"
# two pages first; check the headings in paper_pages-1-2_book/source.md
python3 make_book.py --book_name paper.pdf --to-epub --pages 1-2 --key ${key} --use_context session
# then the whole file
python3 make_book.py --book_name paper.pdf --to-epub --key ${key} --use_context sessionScans (OCR)
--pdf-ocr reads pages that have no text layer. --ocr-lang names the languages to read (iso:zh, iso:ja, …), --ocr-engine picks the engine (auto, rapidocr, ocrmac, easyocr, tesseract), and --ocr-replace-layer reads every page again when the embedded text is wrong. More: Which OCR engine.
python3 make_book.py --book_name scan.pdf --to-epub --pdf-ocr --ocr-lang iso:zh --key ${key}Pages, figures and formulas
--pages 12-30 (or 1,3,5-7) translates only those pages, into paper_pages-12-30_bilingual.epub. Figures are drawn at 200 DPI; --pdf-image-dpi 300 sharpens tiny labels. Display formulas are kept as pictures; --no-formula-images turns that off.
Layout and hardware
--img-model MODEL shows each page to a vision model that corrects what the layout detector got wrong, such as headings, captions and code; --img-base-url and --img-key point it at another endpoint. --device picks where the extraction models run (auto, cpu, cuda, mps, xpu).
More: PDF to bilingual EPUB, Recommended settings for PDF.
Docker
You can use Docker if you don't want to deal with setting up the environment. Prebuilt images are published to GitHub Container Registry on every merge to main (as latest) and on every release tag:
docker pull ghcr.io/yihong0618/bilingual_book_maker:latestMount the folder containing your book at /book and pass the usual flags — the container accepts every make_book.py option, and the translated book is written back into the same folder:
# Linux / macOS
export folder_path=/path/to/your/books
export book_name=animal_farm.epub
export openai_key=sk-XXX
export language=zh-hans # see the language list in book_maker/utils.py
docker run --rm -v "${folder_path}":/book ghcr.io/yihong0618/bilingual_book_maker:latest --book_name "/book/${book_name}" --key "${openai_key}" --language "${language}"# Windows PowerShell
$folder_path="C:\Users\user\mybook"
$book_name="animal_farm.epub"
$openai_key="sk-xxx"
$language="zh-hans"
docker run --rm -v ${folder_path}:/book ghcr.io/yihong0618/bilingual_book_maker:latest --book_name "/book/$book_name" --key $openai_key --language $languageFor example, a quick test needing no key at all, over the free Google route:
docker run --rm -v /home/user/my_books:/book ghcr.io/yihong0618/bilingual_book_maker:latest --book_name /book/animal_farm.epub --api_format google --test --test_num 1 --language zh-hantThe container runs as root, so writing into the mounted folder always works; on Linux the files it writes there belong to root (chown them afterwards, or add --user $(id -u)). API keys can also be passed as environment variables (-e OPENAI_API_KEY=sk-XXX) instead of --key.
The pdf tag adds Pandoc and the PDF packages for --to-epub, and runs to several gigabytes:
docker run --rm -v "${folder_path}":/book -v bbm-models:/root/.cache ghcr.io/yihong0618/bilingual_book_maker:pdf --book_name /book/paper.pdf --to-epub --key "${openai_key}" --use_context sessionFor a GPU, find your machine:
- Linux with an NVIDIA card: the
pdf-cudatag; install the NVIDIA Container Toolkit and add--gpus all. - Windows with an NVIDIA card: the same
pdf-cudatag, through Docker Desktop's WSL2 backend. - Mac (Apple silicon): Docker cannot reach the GPU; install the tool natively instead.
- arm64 Linux with an NVIDIA card: the
pdftag, on the processor (pdf-cudais amd64 only).
More: Docker.
To build the image yourself instead of pulling:
docker build --tag bilingual_book_maker .
docker run --rm -v /path/to/your/books:/book bilingual_book_maker --book_name /book/animal_farm.epub --key sk-XXX --language zh-hansNotes
- API token from free trial has limit. If you want to speed up the process, consider paying for the service or use multiple OpenAI tokens
- PR is welcome
Thanks
Contribution
- Any issues or PRs are welcome.
- TODOs in the issue can also be selected.
- Please run
black make_book.py[^black] before submitting the code.
Others better
- 书译 BookTranslator -> Book Translator
Appreciation
Thank you, that's enough.
[^token]: You can get a token from OpenAI or Anthropic.
[^black]: https://github.com/psf/black
En Index (`docs/en/index.md`)
bilingual book maker
bilingual book maker translates a book with a language model and writes a bilingual edition: every paragraph of the original, followed by its translation. It reads EPUB, TXT, Markdown, SRT and PDF files. It talks to OpenAI, Anthropic, Gemini, Qwen, any OpenAI-compatible endpoint (a gateway, a reseller, or a model on your own machine), a Codex subscription, and a few translation services.
pip install -U bbook_maker
export OPENAI_API_KEY=sk-...
bbook_maker --book_name my_book.epub --use_context sessionThat writes my_book_bilingual.epub next to my_book.epub. The default model is gpt-6-luna; the default target language is Simplified Chinese (--language zh-hans).
Where to go next
- Start here: Quick start (one EPUB, one TXT, one PDF) and Installation (pip, a checkout, Docker).
- Have a coding agent? Translate with an agent: Codex or Claude Code with the repository's skill asks what you have, picks the flags and runs the tool for you.
- EPUB: the command most people need, then recommended settings for textbooks, small models and local servers; plan mode explains what happens.
- PDF: PDF to bilingual EPUB (two pages first, then the whole file), recommended settings per document type and system, and installing the PDF extra.
- Other formats: TXT, SRT and Markdown, one command each; which page for which file.
- Endpoints and models: Translating with an LLM (model, key, endpoint, retries, on-device models), session mode (one conversation for the whole book, on every format that sends context), the provider file, translation services, prompt files and environment variables.
- Evaluation: the measurements behind the parameters, one parameter per page.
- Reference: every command line option.
Use it on material you may translate
Use it only with material you have the right to translate — works for which you hold the necessary rights, suitably licensed or permitted works, public-domain books, or uses otherwise allowed by applicable law. Before using this tool, please review the project's disclaimer.
En Quickstart (`docs/en/quickstart.md`)
Quick start
This page takes you from nothing to three bilingual books: an EPUB, a TXT and a PDF. It uses OpenAI's API and the default model, gpt-6-luna. Other endpoints are on Translating with an LLM.
1. Install
You need Python 3.10 or newer. A virtual environment is a good idea.
pip install -U bbook_makerAfter the install, the command bbook_maker is on your PATH. From a checkout of the repository, python make_book.py is the same program; see Installation.
2. Give it a key
Put the key in the environment, so it stays off the command line.
export OPENAI_API_KEY=sk-...You can pass --key instead. The lookup order is on Translating with an LLM.
3. Try it on a few paragraphs first
--test translates only the first few paragraphs, so a mistake costs almost nothing. --test_num sets how many (default 10).
bbook_maker \
--book_name test_books/animal_farm.epub \
--test \
--test_num 8The sample books are in the repository's test_books/ folder. Use your own file if you installed from pip.
4. An EPUB
bbook_maker \
--book_name my_book.epub \
--language zh-hans \
--use_context session- Output:
my_book_bilingual.epub, beside the input. --use_context sessionkeeps one conversation for the whole book, so names and terms stay consistent. See Session mode.- The EPUB is translated through a plan by default: the tool finds every block of text (verse, tables and captions included) and asks the model which kinds are worth translating. See Plan mode.
5. A TXT file
bbook_maker \
--book_name test_books/the_little_prince.txt \
--language zh-hans- Output:
the_little_prince_bilingual.txt, beside the input. - Lines are sent in groups of
--batch_size(default 10).
Markdown and SRT work the same way and give <name>_bilingual.md and <name>_bilingual.srt. See Formats.
6. A PDF
A PDF is best turned into a bilingual EPUB with a table of contents. That route needs the PDF extra and Pandoc 3.1.12 or newer; install them first with PDF extra. Then read two pages before you pay for the whole book:
python make_book.py \
--book_name paper.pdf \
--to-epub \
--pages 1-2 \
--testOpen paper_pages-1-2_book/source.md and read the headings. They become the table of contents. If they look right, run the whole file:
python make_book.py \
--book_name paper.pdf \
--to-epub \
--use_context session- Output: the working folder
paper_book/(withsource.md, the images,book_bilingual.mdand a manifest) and a copy of the book,paper_bilingual.epub, beside the PDF. - A scanned PDF needs
--pdf-ocr. The run tells you when it does. - Without
--to-epub, a PDF takes the older route and givespaper_bilingual.txt. See PDF.
If a run stops
Press Ctrl+C at any time. The run saves what it has; an EPUB run leaves my_book_bilingual_temp.epub beside the input. Run the same command again with --resume to continue:
bbook_maker \
--book_name my_book.epub \
--use_context session \
--resumeA --to-epub run needs no --resume: rerun the same command and it picks up the extraction and the translation where they stopped.
Zh Index (`docs/zh/index.md`)
bilingual book maker
bilingual book maker 用语言模型翻译一本书,生成双语版:原文的每一段后面紧跟它的译文。它能读取 EPUB、TXT、Markdown、SRT 和 PDF 文件,可以对接 OpenAI、Anthropic、Gemini、Qwen、任何 OpenAI 兼容端点(网关、转售商,或跑在你自己机器上的模型)、Codex 订阅,以及几家翻译服务。
pip install -U bbook_maker
export OPENAI_API_KEY=sk-...
bbook_maker --book_name my_book.epub --use_context session这会在 my_book.epub 旁边生成 my_book_bilingual.epub。默认模型是 gpt-6-luna,默认目标语言是简体中文(--language zh-hans)。
接下来看哪里
- 从这里开始:快速开始(一本 EPUB、一个 TXT、一个 PDF)和安装(pip、仓库源码、Docker)。
- 有编程智能体?用智能体翻译:Codex 或 Claude Code 加载本仓库的技能后,会问清你手头有什么,替你选好参数并运行工具。
- **EPUB:**先看大多数人需要的那条命令,再看面向教材、小模型和本地服务器的推荐设置;计划模式讲解其中发生了什么。
- PDF:PDF 转双语 EPUB(先翻两页,再翻整个文件)、按文档类型和系统给出的推荐设置,以及安装 PDF 扩展。
- 其他格式:TXT、SRT 和 Markdown,各只需一条命令;另见各类文件对应的页面。
- 端点与模型:用大模型翻译(模型、key、端点、重试、本地模型)、会话模式(整本书一段对话,适用于每种会传上下文的格式)、提供方文件、翻译服务、提示词文件和环境变量。
- 评测:参数背后的测量,每个参数一页。
- 参考:全部命令行参数。
只用于你有权翻译的内容
请仅将本工具用于您有权翻译的内容——您持有必要权利的作品、许可或授权允许您翻译的作品、公有领域图书,或适用法律另行允许的使用方式。使用本工具之前,请阅读项目的 免责声明。
Zh Quickstart (`docs/zh/quickstart.md`)
快速开始
本页带你从零开始做出三本双语书:一个 EPUB、一个 TXT 和一个 PDF。这里用的是 OpenAI 的 API 和默认模型 gpt-6-luna。其他端点见用大模型翻译。
1. 安装
需要 Python 3.10 或更新版本,最好用一个虚拟环境。
pip install -U bbook_maker安装后,bbook_maker 命令就在 PATH 上了。在仓库源码里,python make_book.py 是同一个程序,见安装。
2. 提供 key
把 key 放进环境变量,这样它就不会出现在命令行上。
export OPENAI_API_KEY=sk-...也可以改用 --key 传入。查找顺序见用大模型翻译。
3. 先拿几段试试
--test 只翻译开头几段,出了错也几乎不花钱。--test_num 设定翻译多少段(默认 10)。
bbook_maker \
--book_name test_books/animal_farm.epub \
--test \
--test_num 8示例书放在仓库的 test_books/ 文件夹里。如果你是用 pip 安装的,就换成你自己的文件。
4. 一本 EPUB
bbook_maker \
--book_name my_book.epub \
--language zh-hans \
--use_context session- 输出:
my_book_bilingual.epub,就在输入文件旁边。 --use_context session整本书只保持一段对话,人名和术语因此前后一致。见会话模式。- EPUB 默认按计划翻译:工具找出每一块文字(包括诗行、表格和图题),再问模型哪几类值得翻译。见计划模式。
5. 一个 TXT 文件
bbook_maker \
--book_name test_books/the_little_prince.txt \
--language zh-hans- 输出:
the_little_prince_bilingual.txt,就在输入文件旁边。 - 每次请求发送
--batch_size行(默认 10)。
Markdown 和 SRT 的用法相同,分别生成 <name>_bilingual.md 和 <name>_bilingual.srt。见格式。
6. 一个 PDF
PDF 最好转成带目录的双语 EPUB。这条路线需要 PDF 扩展和 Pandoc 3.1.12 或更新版本,请先按 PDF 扩展装好。然后在为整本书付费之前,先读两页:
python make_book.py \
--book_name paper.pdf \
--to-epub \
--pages 1-2 \
--test打开 paper_pages-1-2_book/source.md,看一遍标题,它们会成为目录。如果标题没问题,就翻译整个文件:
python make_book.py \
--book_name paper.pdf \
--to-epub \
--use_context session- 输出:工作文件夹
paper_book/(里面有source.md、图片、book_bilingual.md和一份清单文件),以及 PDF 旁边的一份成书副本paper_bilingual.epub。 - 扫描版 PDF 需要
--pdf-ocr,需要时运行会告诉你。 - 不加
--to-epub时,PDF 走旧路线,生成paper_bilingual.txt。见 PDF。
运行中断时
随时可以按 Ctrl+C。运行会保存已完成的部分;EPUB 运行会在输入文件旁留下 my_book_bilingual_temp.epub。加上 --resume 再运行同一条命令即可继续:
bbook_maker \
--book_name my_book.epub \
--use_context session \
--resume--to-epub 运行不需要 --resume:重新运行同一条命令,它会从提取和翻译停下的地方接着做。
En Evaluation Index (`docs/en/evaluation/index.md`)
Measurements behind the parameters
Each page here measures one parameter: what was compared, on which sample, and the numbers that came out. The default is stated with the measurement it rests on, and each page ends with Limits: the sample size and what was not measured. Every number is copied from the measurement's own record.
Translation
[Units and tokens per request](grouping-batch-size.md)
--max-batch-units, --accumulated_num. Where grouped requests start to shift translations into the wrong slot (measured onset: 64 units), and the cost of the retry overhead at each size. Default 16 units and 1200 to 1600 tokens per request; 8 units and 800 tokens without a strict schema.
[Session compact budget](session-compact-budget.md)
--context-compact-at. Session cost at 4096 and 8192 over 300 units, compactions counted. Default 8192, chosen for continuity on local models; 4096 used fewer tokens.
[Jev as the plan classifier](plan-classifier-jev.md)
--classify-model, --classify-min-confidence. TypeSafe's Jev against gpt-5.6-luna on the 31 signatures of one book (27 of 31 agree), Jev's prompt tokens before and after the lean requests, and the confidence gate measured over 662 signatures in 45 EPUBs (0.95). Default: the translating model.
PDF to EPUB
[OCR engines on scanned pages](pdf-ocr-engines.md)
--ocr-engine. rapidocr, ocrmac, easyocr and tesseract on the same 20 scanned pages, character error rate and time per page, on the CPU. tesseract and ocrmac for English, rapidocr for simplified Chinese, ocrmac or rapidocr for traditional. Default auto.
[Region roles with an image model](pdf-structure-llm-roles.md)
--img-model. 123 structure faults on 85 pages, and which of them a vision model relabelling regions fixes (40 of 66 wrong labels) and which it does not (running headers, shattered OCR pages). About 3,000 prompt tokens a page. Off unless a model is named.
Zh Evaluation Index (`docs/zh/evaluation/index.md`)
参数背后的测量
这里每一页测量一个参数:比较了什么、用的是哪个样本、得出了哪些数字。默认值与它所依据的测量一起给出,每页最后是“局限”:样本规模,以及没有测量的内容。每个数字都照抄自该测量自己的记录。
翻译
[每次请求的单元数与 token 数](grouping-batch-size.md)
--max-batch-units、--accumulated_num。合并请求从哪里开始把译文错放到别的位置(测得的起点:64 个单元),以及每种大小下重试开销的代价。默认每个请求 16 个单元、1200 到 1600 token;没有严格 schema 时为 8 个单元、800 token。
[会话压缩预算](session-compact-budget.md)
--context-compact-at。300 个单元上 4096 与 8192 两档的会话成本,并统计了压缩次数。默认 8192,是为本地模型上的连续性而选;4096 用的 token 更少。
[用 Jev 做计划分类器](plan-classifier-jev.md)
--classify-model、--classify-min-confidence。TypeSafe 的 Jev 与 gpt-5.6-luna 在一本书的 31 个签名上的对比(31 个中有 27 个一致),精简请求前后 Jev 的提示 token,以及在 45 本 EPUB 的 662 个签名上测得的置信度闸门(0.95)。默认:翻译模型。
PDF 转 EPUB
[扫描页上的 OCR 引擎](pdf-ocr-engines.md)
--ocr-engine。rapidocr、ocrmac、easyocr 和 tesseract 在同样 20 张扫描页上的字符错误率和每页耗时,均在 CPU 上。英文用 tesseract 和 ocrmac,简体中文用 rapidocr,繁体用 ocrmac 或 rapidocr。默认 auto。
[用图像模型判定区域角色](pdf-structure-llm-roles.md)
--img-model。85 页上的 123 个结构错误,其中哪些能靠视觉模型重新标注区域修正(66 个错误标签中的 40 个),哪些不能(页眉、支离破碎的 OCR 页面)。每页约 3,000 个提示 token。除非指定模型,否则关闭。
En Agent Skill (`docs/en/agent-skill.md`)
Translate with an agent
You have a book and want it translated well. The shortest path is to hand it to a coding agent that carries this repository's bbm-plan skill. The agent asks what you have, picks the flags, shows you the command and the cost, runs the tool quietly, and hands back the bilingual book. You are never asked to learn a flag.
What you need
A checkout of this repository. The skill runs
make_book.pyfrom the repository root, so apip install bbook_makeralone is not enough:bashgit clone https://github.com/yihong0618/bilingual_book_maker.git cd bilingual_book_maker pip install -r requirements.txt # pip install ".[pdf]" if you have PDFsCredentials for the model you want: an API key in
.envor the environment, a model on your own machine, or a ChatGPT plan through the codex route. The agent asks which; see Translating with an LLM for what each needs.A coding agent that reads skills: Codex, Claude Code, or any agent the
skillsCLI installs into.
Codex
Codex reads .agents/skills/ from the repository, so nothing is installed. Start codex in the checkout and describe the job, or name the skill:
$bbm-plan translate ~/Books/my_book.epub to JapaneseClaude Code and other agents
Install the skill once. The command finds the agents on your machine and asks which to install into:
npx skills add yihong0618/bilingual_book_maker --skill bbm-planThen start the agent in the checkout and describe the job. In Claude Code the skill also answers to its name:
/bbm-plan translate ~/Books/my_book.epub to JapaneseWhat the agent does
- Asks what you have. The route (a key, a local model, a ChatGPT plan), the target language, a prompt file if you keep one, and anything you want changed from the defaults. Bilingual output is assumed: every original paragraph followed by its translation. Say "just the Chinese" if you want the translation alone.
- Probes the endpoint with a request costing less than a cent, before anything is paid for.
- States the command, one line per flag with the reason, the checkpoint you will see, and whose allowance it spends. Then it waits for your approval.
- Follows the flow for the file.
- An EPUB goes through plan mode: the tool partitions the book, the agent itself decides what a running head, a page number or a footnote marker is against the real samples, and shows you the plan before the full run.
- A PDF becomes a bilingual EPUB: two pages first, the agent reads what came out, then the whole file.
- TXT, SRT and Markdown take one command each.
- Runs quietly, logging to
run.log, and resumes where it stopped after a crash or a new session. Everything it needs is on disk, nothing lives only in the conversation. - Delivers the
_bilingualfile next to your book.
Reading the skill
The skill is the router .agents/skills/bbm-plan/SKILL.md plus one reference file per flow (route setup, EPUB plan mode, the PDF route, plain formats, prompt files, providers), loaded only when that step runs. Read it on GitHub: .agents/skills/bbm-plan. It points at the same pages as this wiki for anything it does not spell out.
En Book Source (`docs/en/book_source.md`)
Which page for which file
The file's extension picks how it is read. Find yours:
| if your file is… | start with | then read |
|---|---|---|
| an EPUB | bbook_maker --book_name book.epub --use_context session |
EPUB, Recommended settings for EPUB |
| a PDF | python make_book.py --book_name book.pdf --to-epub --pages 1-2 --test |
PDF to bilingual EPUB, Recommended settings for PDF |
| a plain-text file | bbook_maker --book_name book.txt --batch_size 20 |
TXT |
| subtitles | bbook_maker --book_name talk.srt --accumulated_num 400 |
SRT |
| a Markdown document | bbook_maker --book_name doc.md --prompt prompt_md.json --use_context session |
Markdown |
A PromptDown .md file is a prompt, not a book: it goes to --prompt, and a Markdown book goes to --book_name.
An EPUB without a plan
If you want only chosen tags translated, turn the plan off and name the tags:
bbook_maker --book_name test_books/animal_farm.epub --plan-classify none --translate-tags div,pIf a book keeps text outside any tag, --allow_navigable_strings adds it too. A plan already covers both, so these are for books where you want exact control. A well-formed EPUB gives better results either way.
En Cmd (`docs/en/cmd.md`)
Command Line Options
book_maker/cli.py and python3 make_book.py --help are the runtime source of truth.
The inventory below is checked against every long option registered with argparse; the
sections after it provide additional notes for selected workflows.
Complete option inventory
Input, scope, and output
| Option | Purpose |
|---|---|
--book_name PATH |
Input EPUB, TXT, Markdown, SRT, or PDF path (required). |
--language LANGUAGE |
Target language: a tag (zh-hant), a name ("Traditional Chinese"), or TAG:NAME to pin both when the tables miss the language — tag → output stamps and field names, name → the prompt. Default zh-hans; list in docs/en/languages.md. |
--source_lang LANGUAGE |
Source language. Stated, it reaches every LLM route's prompt, and the request body on qwen/customapi; default auto (states nothing). |
--single_translate |
Output translation only instead of bilingual text. |
--no_disclosure |
Do not mark the epub as an AI translation: leaves out the one-line credit ("Translated by <model>, <year>.") below the book intro. Turns off --translation-metadata too. |
--translation-metadata |
Write a small bbm_translation_metadata.json into the book: the model, the date, and the sha256 of a --glossary file (whose text is embedded alongside; terms a run learned by itself are not). Nothing else — no command line, endpoint, languages or paths, and no package metas: what could have shaped the translation is recorded, who ran it is not. Format conversions keep manifest files, so the record survives them. Plan-mode and session runs write it by themselves; this flag adds it to a plain tag-mode run. --no_disclosure turns it off too. |
--translate-tags TAGS |
Comma-separated EPUB tags; default p, ignored in plan mode. |
--exclude-translate-tags TAGS |
EPUB ancestor tags to exclude; default sup,code; "" clears it. |
--allow_navigable_strings |
Include otherwise untagged EPUB strings; redundant in plan mode. |
--only_filelist FILES |
Include only these comma-separated internal EPUB files. |
--exclude_filelist FILES |
Exclude these comma-separated internal EPUB files. |
--translation_style CSS |
CSS applied to translated EPUB entries. |
--translation_color COLOR |
Color-only shorthand; --translation_style takes precedence. |
--pdf_layout MODE |
Additional PDF output: none, top-bottom, side-by-side, or all. |
--to-epub |
PDF only: read the PDF with docling, translate the Markdown, write <name>_bilingual.epub beside it; figures stay pictures; the bundle stays in <name>_book/ for editing and resume. Needs the pdf extra and Pandoc 3.1.12+ on PATH; no Java. See installation-pdf.md. |
--pdf-ocr |
PDF only, with --to-epub: read pages that carry no text layer, which are refused without it. Off by default; OCR costs several times the time and changes nothing on a born-digital PDF. Layout, headings and tables are detected either way. |
--ocr-replace-layer |
PDF only, with --to-epub --pdf-ocr: OCR every page and replace the PDF's own text layer instead of keeping it. Off by default; measured worse than a sound layer, for a layer that is wrong. A page read empty stays empty and is named. |
--no-formula-images |
PDF only, with --to-epub: leave display formulas as `` placeholders instead of cropping each one from the page as an image. The parser finds equations but never reads them, so the pictures are the only reason the mathematics reaches the book at all; they cost no model and no measurable time. Inline mathematics inside a paragraph is not a formula region and is not covered either way. |
--pdf-image-dpi N |
PDF, --to-epub: figure sharpness in DPI of the PDF's own page size; default 200, 300 for tiny labels; a rerun at another value redraws the figures only. |
--img-model MODEL |
PDF only, with --to-epub: a vision model that corrects the layout detector's region roles (text, heading, title, caption, footnote, code) from a page image before export. Off unless named here or as the provider entry's img_model; none turns that off. Never the run's own model by fallback. About 3k prompt tokens per page. |
--img-base-url URL |
Endpoint for --img-model when it is not the run's (OpenAI-compatible only). |
--img-key KEY |
Key for --img-base-url. Default: the run's key when the endpoint is the run's own; else the provider entry's img_env_key when the endpoint is the entry's own; else the key the endpoint's format reads from the environment. A key is never sent to an address it was not given for. |
--device DEVICE |
PDF only, with --to-epub: where the extraction models run — auto (default; detects CUDA or MPS, falls back to CPU), cpu, cuda, mps, xpu. CPU is fully supported and produces the same output, only slower. |
--ocr-lang LANGS |
PDF only, with --to-epub --pdf-ocr: the languages the OCR engine reads on pages with no text layer (every page with --ocr-replace-layer), comma separated, in the engine's own codes (rapidocr: ch, en, latin; easyocr: ch_sim, ja, ko; ocrmac: zh-Hans, ja-JP), or portable BCP-47 tags behind iso: (iso:zh-Hans, iso:zh-Hant, iso:ja, iso:ko), which docling 2.129 maps onto whichever engine runs. rapidocr reads one language per run and uses the first. The engine is the one docling selects on the install (with the PDF extra, ocrmac on a Mac, whose default reads English, Spanish, French and German, and rapidocr elsewhere, whose default reads Chinese and English), and the run prints the engine and languages it used. A scan in another script needs the flag; the run says so when it meets a scanned page without it. An unknown code is refused before any page is read. The codes per engine, and which engine to choose: Which OCR engine. |
--ocr-engine ENGINE |
PDF only, with --to-epub --pdf-ocr: the OCR engine for pages with no text layer (every page with --ocr-replace-layer). auto (default) takes the first installed of ocrmac, rapidocr, easyocr; the pdf extra installs rapidocr with onnxruntime, models included, and on macOS also ocrmac (Apple's Vision framework); neither downloads anything, so auto reads with ocrmac on a Mac and rapidocr elsewhere; easyocr downloads its models on first use (pip install easyocr); tesseract uses the tesseract program and its language data from PATH. An engine that is not installed, or ocrmac off macOS, is refused before any page is read. Language codes differ by engine (--ocr-lang); the run names the engine it used. Which to choose, measured: Which OCR engine. |
--pages PAGES |
PDF only, with --to-epub: the pages to read, numbered from 1 (12-30, 1,3,5-7); the rest is left out. The selection goes into the bundle and book names (<name>_pages-12-30_…), so a chapter never overwrites the whole book. A selection that starts inside a section gets a Page N heading above its first prose, in source.md. |
--retranslate OUT FILE START END |
Retranslate an EPUB range in an existing output. EPUB only — refused elsewhere. |
EPUB plan mode
| Option | Purpose |
|---|---|
--plan-dry-run |
Build and print the EPUB plan, write _plan.json with every action still null, and exit. No credentials needed. |
--plan-classify {auto,none,all,model,agent} |
No plan, the whole partition, model triage, or coding-agent triage. Default auto: model triage on any epub endpoint that can answer — over structured output where a strict JSON schema is verified, over a plain conversation (exact skip/translate replies; anything else translates) elsewhere, codex included; tag mode only where no conversation exists. |
--classify-model MODEL |
Model for every classification step (old name --plan-classify-model), or a Jev-compatible classifier (TypeSafe's Jev by default; a gateway at its URL); implies model mode on an epub. Asked over a JSON schema where its endpoint verifies one, else over a plain conversation. Default: the provider entry's classify_model, else the run's model. |
--classify-base-url URL |
Endpoint for --classify-model when it is not the run's: an OpenAI-compatible endpoint, or a Jev-compatible classifier's URL (a path ending in /systemone is used as is). |
--classify-key KEY |
Key for --classify-base-url; same default rule as --img-key. JEV_API_KEY/TYPESAFE_API_KEY is read implicitly only at a typesafe.ai address, and a Cloudflare AI Gateway's CF_AIG_TOKEN only at gateway.ai.cloudflare.com; a provider entry may still bind a variable to its own address. |
--classify-min-confidence P |
Confidence gate for a Jev-compatible classifier, 0 to 1: a skip whose probability is below it becomes translate; translate is never gated. Default 0.95, measured; below 0.5 the gate is off. BBM_JEV_MIN_CONFIDENCE sets it without the flag. |
--plan-min-coverage FRACTION |
Fail if selected planned text is below this fraction; default 0.5, must be between 0 and 1 (0 disables the guard, values above 0.9 usually abort — both warn). |
--poetry-group-size N |
Deprecated — general grouping and the session handoff give short lines their neighbours now, and the units cap is --max-batch-units. Still works (default 8, minimum 1) but warns. |
Translation and execution
| Option | Purpose |
|---|---|
--test |
Translate only a preview sample. |
--test_num N |
Number of test units; default 10. |
--resume |
Continue from the loader's saved checkpoint. An EPUB checkpoint records the run's language, prompt and model; a mismatch stops the resume (older checkpoints warn once and continue). Refused together with --parallel-workers and --accumulated_num above 1, where no checkpoint is ever written. |
--prompt VALUE_OR_FILE |
Prompt config: user (must contain {text}), system, and style. A style is a standing instruction said once where a window starts (the system message on the API routes, the thread instructions on codex), never repeated per request; a user-written style replaces the model's own style notes in handoff reports. A section with no native slot on a route (system on the codex format) is appended to the user message instead of dropped; a run with --prompt prints which sections it adopted and where. Sample: prompt_template.json (style shipped empty). |
--temperature FLOAT |
Sampling temperature, on the formats that take one; default 1.0. The anthropic format always sends it. The openai format leaves it out, and the API default applies, when it equals that default and when the model rejects an explicit one (gpt-5.x, the o-series). The codex format has no such setting and ignores it. |
--use_context [window|session] |
Send earlier paragraphs as context. Bare or window: re-send the last few source/translation pairs (the long-standing behaviour). session: one append-only history, re-read at the endpoint's prompt-cache rate. |
--context_paragraph_limit N |
Window mode only: context history limit. Parser default 0 means the translator default (3 paragraphs for ChatGPT), not zero history. |
--context-compact-at N |
Estimated-token budget for a rolling history. In session mode the history is compacted into a handoff report at this size; minimum 1500. When unset, every session run — grouped or not, the codex format included — compacts at 8192, printed at start. That default is chosen for continuity (the fewest window seams), not for price: a lower value is cheaper in session mode, because each request re-reads the carried history, so lower it if session cost matters more to you than seams, or set it to your model's input limit when that is smaller. Past about 16000 the cost climbs steeply. The measurements are on Session compact budget. Also bounds the plan classifier's conversation on endpoints that classify over a plain session (restart there, no handoff), with or without --use_context. An explicit value always wins. |
--no-context-compact |
Session mode only: skip the handoff report. The window still rolls over at the budget, but the next one starts empty. |
--glossary FILE / --terminology FILE |
A file of term → translation lines (one per line; # starts a note or a comment) this run must render that way. Two names for one flag. Only the terms that occur in a request are sent with it. A missing file stops the run at parse time. Read by the openai- and codex-shaped routes for EPUB, Markdown, and PDF books; other routes warn and ignore it. |
--glossary-auto on|off |
Whether a session run also keeps the renderings its own handoff reports establish. Off unless you ask for it. It needs a session to learn from (--use_context session, or the codex format's one thread) and a model that answers with names rather than prose; off asks the compact turn for a summary only. Learned terms stay in this run and in _handoff.md, and nowhere else. |
--accumulated_num N |
EPUB token/character accumulation and SRT subtitle-block character batching (capped at 512 for SRT). In EPUB plan mode it is a per-request token budget: consecutive units of any length share one request up to N tokens (at most --max-batch-units units per request; half that when the endpoint verifies JSON mode but not a strict schema). Untyped, every plan run derives a default from the run's own prompt overhead — 1200 with the stock prompts, up to 1600 under a long custom --prompt — halved per request on an endpoint without a strict-schema verdict, but never below the 800 floor (so the per-request budget there is 800 whatever the prompt costs); session runs (codex included) keep the un-halved value. These are chosen safety margins, below the range any measurement found fault-free; see Units and tokens per request. The run narrates the number and the route class. Pass 1 to turn grouping off. Minimum 1. |
--max-batch-units N |
EPUB plan mode only: the most units --accumulated_num's token budget may put in one request. Default 16, a quarter of the level where content faults first appeared (64 units per request), as a safety margin. An endpoint that verifies JSON mode but not a strict schema carries half this many (8), where reply miscounts actually live. Raise it if you trust your model and want fewer requests; lower it if the run keeps printing misalignment recoveries. |
--batch_size N |
Lines or paragraphs sent in one request by the TXT, Markdown and PDF (text route) loaders. Default 10. |
--block_size N |
Merge paragraphs into delimiter-translated blocks. |
--sentence_mode |
Translate EPUB paragraphs sentence by sentence; incompatible with plan mode. |
--parallel-workers N |
Parallel EPUB chapters or Markdown batches/sections; default 1. Refused with --use_context session (one history) and on the codex format (one thread). |
--batch |
Submit a ChatGPT Batch API job. Refused on EPUB (the queue path is unreachable there: the run would translate live at full price and submit an empty job instead of writing the book) and on routes without the Batch API. The TXT, SRT and Markdown loaders do not implement it either (a run translates live), so no format uses it at present. |
--batch-use |
Consume a previously submitted batch job. Refused on EPUB, like --batch. |
--no-thinking |
Ask the model not to reason before answering. On the OpenAI-shaped routes the request field is negotiated from the endpoint's own rejections and cached per endpoint and model; if every spelling is refused the run warns once and continues without one. On anthropic it is thinking: {"type": "disabled"}. Refused on codex (a subprocess has no request body); warned as inert on the routes that build their own request. A field set in --extra_body wins. |
--extra_body JSON |
Extra fields on every request body, for the routes that build one (openai, groq, xai, litellm, --model orcarouter, anthropic); the others ignore it and say so. Reaches the capability probe and the JSON rungs too, so the endpoint is graded on the request the run makes. Merged over the named parameters, so a field here beats the flag for it. |
--extra_headers JSON |
Extra HTTP headers on every request, same routes. Set on the client, so the capability probe, the model check and the model listing carry them. Values must be strings. |
--quiet |
Suppress EPUB progress bars and paragraph echoes, not reports/errors. |
--proxy URL |
Set HTTP/HTTPS proxy environment variables for the run. |
Endpoint and credentials
A route is an endpoint, not a model name.
| Option | Purpose |
|---|---|
--model MODEL |
The model id, exactly as the endpoint names it (gpt-5-mini, claude-sonnet-4-6, openai/gpt-5-mini). Defaults to gpt-6-luna on the openai format; the anthropic format needs one. Old alias values are rewritten with a note. |
--api_base URL |
The endpoint. Defaults to the format's official host. A pasted …/v1/chat/completions or a trailing slash is trimmed. |
--key KEY |
API key; comma-separate several to rotate them. Prefer BBM_API_KEY or the format's own variable. |
--api_format FORMAT |
The API the endpoint speaks: openai (default), anthropic, gemini, qwen, groq, xai, litellm, codex, google, caiyun, deepl, deeplfree, tencent, customapi. Inferred from the --api_base host, else from a claude/anthropic model id — the vendor formats are never inferred, so they are named. |
--api_format gemini | qwen |
Google's and Alibaba's own protocols, each with its own translator: Gemini's native constrained decoding and chat history, and Qwen-MT's language-pair request. Defaults gemini-flash-latest and qwen-mt-turbo. |
--api_format groq | xai | litellm |
The OpenAI shape at Groq, xAI and a LiteLLM proxy (http://localhost:4000). Each carries its address, so the format and a key are the whole route. --model is required: those catalogues turn over, so none is assumed. |
--model codex |
The Codex CLI sidecar on a ChatGPT plan, the same as --api_format codex. It runs gpt-6-luna; --api_format codex --model names another (the sidecar also offers gpt-5.6-luna, gpt-5.6-sol, gpt-5.6-terra, gpt-5.5, gpt-5.2). |
--model orcarouter |
The OrcaRouter gateway and its smart-routing model orcarouter/auto. Needs no --api_base; one you pass wins. The key comes from BBM_ORCAROUTER_API_KEY. Not a legacy alias: nothing is rewritten. |
--model_list IDS |
Several model ids to rotate across, comma-separated. A single model belongs in --model; naming a model in both flags is an error. Refused with --use_context session: rotation makes every request a full-price cache miss and mixes models in one conversation. |
--source_lang LANG |
Source language. Stated, it reaches every LLM route's prompt as evidence, and the request itself on qwen/customapi; default auto. |
--interval SECONDS |
Pause between requests, default 0.01. Only the gemini route paces itself with it. |
--provider NAME |
A named endpoint from bbm_providers.json (this directory) or ~/.bbm/providers.json; the project file wins on a shared name, and a name in neither falls back to the shipped bbm_providers.example.json, with a warning naming the address and key variable it used (its FILL-ME templates excluded). Its base_url, api_style (openai, anthropic, gemini, qwen, groq, xai or litellm), default_models and env_key stand in for --api_base, --api_format, --model/--model_list and the key; its img_model/img_base_url/img_env_key and classify_model/classify_base_url/classify_env_key stand in for the --img-model/--img-base-url/--img-key and --classify-model/--classify-base-url/--classify-key flags (see Provider file and extra models). Flags you pass yourself win. |
A gateway that serves Claude models speaks the OpenAI shape, and a gateway--api_base is taken to be that shape. The anthropic format is inferred only
on Anthropic's own host, or from a claude model id with no --api_base. A
gateway asked for the anthropic shape it does not serve answers 404, and the
run stops naming --api_format openai as the fix.
Key lookup order: --key (--api_key is the same flag), then BBM_API_KEY,
then the format's own: OPENAI_API_KEY, ANTHROPIC_API_KEY,BBM_GOOGLE_GEMINI_KEY, BBM_QWEN_API_KEY, BBM_GROQ_API_KEY,BBM_XAI_API_KEY, BBM_CAIYUN_API_KEY, BBM_DEEPL_API_KEY (each with the
vendor's own conventional variable after it). Endpoints on localhost need no
key.
The old --model preset names, the per-vendor --*_key flags,--ollama_model and --deployment_id are no longer in the parser, but old
command lines still run: book_maker/legacy_cli.py rewrites
them into the flags above before the run starts and prints each rewrite. The
table is in Migrating from the old flags. The old
per-vendor key variables (BBM_GROQ_API_KEY, BBM_GOOGLE_GEMINI_KEY, …) are
still read for the route that used them.
Do not put secrets directly on a shared command line. Environment variables are safer for
agent and CI use. The CLI does not load .env files itself: export the variables first,
or source a local git-ignored file before running, for exampleset -a; source .env; set +a; bbook_maker ....
Test translate
--test
Use this option to preview the result if you haven't paid for the service or just want to test. Note that there is a limit and it may take some time.
bbook_maker --book_name test_books/Lex_Fridman_episode_322.srt --key ${openai_key} --model gpt-5-mini --testbbook_maker --book_name test_books/animal_farm.epub --key ${openai_key} --model gpt-5-mini --test --language zh-hans--test_num <TEST_NUM>
Use this option to set how many paragraph you want to translate for testing. Default is 10.
Resume
--resume
Use this option to manually resume the process after an interruption.
Retranslate (epub only)
--retranslate <file_name_in_epub> <start_str> <end_str>
If a file in an EPUB is not translated well, this re-translates part of it separately.
Argparse requires all four values. Use an empty end_str to retranslate only the starting
tag; an empty file_name_in_epub enables automatic filename lookup.
Retranslate from start_str to end_str's tag:
textbbook_maker --book_name "test_books/animal_farm.epub" --retranslate 'test_books/animal_farm_bilingual.epub' 'index_split_002.html' 'in spite of the present book shortage which' 'This kind of thing is not a good symptom. Obviously'Retranslate the
start_strtag (empty fourth value):textbbook_maker --book_name "test_books/animal_farm.epub" --retranslate 'test_books/animal_farm_bilingual.epub' 'index_split_002.html' 'in spite of the present book shortage which' ''Retranslate the
start_strtag and auto-find the filename (empty second and fourth values):textbbook_maker --book_name "test_books/animal_farm.epub" --retranslate 'test_books/animal_farm_bilingual.epub' '' 'in spite of the present book shortage which' ''
Warning:
It deletes from the tag at start_str of the finished book to the next tag at end_str, and then re-translates.
Therefore, make sure the tag after end_str is translated content. When end_str is an empty string, the tag after start_str is used. There can be missing translations between the two strings, but a non-translated end boundary will cause problems.
Customize output style (epub only)
--translation_style
Support changing the output style of epub files.
bbook_maker --book_name test_books/animal_farm.epub --translation_style "color: #4a4a4a; font-style: normal; background-color: #f7f7f7; padding: 5px; margin: 10px 0; border-radius: 5px;"Proxy
--proxy
Use this option to specify proxy server for internet access. Enter a string such as http://127.0.0.1:7890 .
API base
--api_base <API_BASE_URL>
If you want to change api_base like using Cloudflare Workers, use this option to support it.
bbook_maker --book_name 'animal_farm.epub' --key sk-XXXXX --model gpt-5-mini --api_base 'https://xxxxx/v1'Note: the api url should be 'https://xxxx/v1'. Quotation marks are required.
Microsoft Azure Endpoints
Azure has no flag of its own. Point --api_base at the deployment's
OpenAI-compatible URL and name the deployment in --model:
bbook_maker --book_name 'animal_farm.epub' --key XXXXX --api_base 'https://example-endpoint.openai.azure.com/openai/v1' --model 'deployment-name'Batch size (TXT, Markdown, PDF text route)
--batch_size
Use this parameter to specify the number of lines for batch translation. Default is 10. Read by the TXT, Markdown and PDF (text route) loaders; EPUB uses --accumulated_num.
python3 make_book.py --book_name test_books/the_little_prince.txt --test --batch_size 20Accumulated Num
--accumulated_num <ACCUMULATED_NUM>
Wait for how many tokens have been accumulated before starting the translation. gpt3.5 limits the total_token to 4090.
In EPUB plan mode you rarely need it: the run derives a per-request budget (1200 with the stock prompts, 800 on an endpoint without a strict schema) and prints it. See the --accumulated_num row above and Units and tokens per request.
For example, if you use --accumulated_num 1600, maybe openai will
output 2200 tokens and maybe 200 tokens for other messages in the system messages user messages. 1600+2200+200=4000, so you are close to the limit.
You have to choose your own
value, there is no way to tell if the limit is reached before sending request.
En Disclaimer (`docs/en/disclaimer.md`)
Disclaimer
bilingual_book_maker helps users create multilingual versions of books and other documents.
Use only material you are entitled to translate. This includes works for which you hold the necessary rights, works whose license or the rights holder's permission allows your intended use, works in the public domain under applicable law, and uses otherwise permitted by applicable law. Owning or buying a copy does not itself grant translation rights.
Check translation and distribution rights separately. Translation is generally an adaptation (a derivative work). Personal use is not automatically exempt from copyright restrictions. If you share a translation, ensure that your license, permission, or applicable law also allows distribution of everything included in the output, including any original text and images. The same license or permission may cover both translation and distribution. For Creative Commons works, check the specific license and version, including attribution, NonCommercial, ShareAlike, and NoDerivatives conditions where applicable. For example, NoDerivatives licenses do not authorize public sharing of translations. Public-domain status varies by jurisdiction; modern translations, editorial contributions, forewords, and illustrations may be protected separately from an older original.
The MIT license does not license book content. The project's MIT license covers its software and associated documentation; it grants no rights to input books or other third-party content and does not automatically apply to generated files. Use of the output remains subject to applicable law and any relevant licenses or permissions. The tool does not determine copyright ownership or verify that your intended use is permitted.
Preservation is not rights verification. For EPUB output, the tool carries over source attribution and rights metadata where it can. This does not establish that translation or distribution is permitted. Check the output and retain or supply attribution, copyright notices, license information, and indications of changes as required by the applicable license or permission.
No warranty; limitation of liability. The software is provided “as is,” without warranty, as set out in the project's MIT license. To the extent permitted by applicable law, the authors and copyright holders disclaim liability arising from use of the software, as set out in that license.
For questions or suggestions about the project, please open an issue.
免责声明
bilingual_book_maker 帮助用户制作图书及其他文档的多语言版本。
请仅翻译您有权翻译的内容。 这包括您持有必要权利的作品、许可协议或权利人授权允许您按预期方式使用的作品、依据适用法律已进入公有领域的作品,以及适用法律另行允许的使用情形。持有或购买一本书或一个副本,本身并不授予翻译权。
分别核实翻译和分发的权利依据。 翻译通常属于改编(演绎作品)。个人使用并不自动免受著作权限制。如果您分享译本,请确认许可协议、授权或适用法律也允许分发输出文件中包含的全部内容,包括原文和图片。同一许可协议或授权可以同时涵盖翻译与分发。使用知识共享(CC)许可的作品时,请核实具体许可及版本,包括其中适用的署名、非商业性使用、相同方式共享和禁止演绎等条件。例如,禁止演绎许可不授权公开分享译本。作品是否进入公有领域因司法辖区而异;现代译本、编辑新增内容、序言及插图可能独立于古旧原文受到保护。
MIT 许可证不为图书内容授予许可。 本项目的 MIT 许可证覆盖软件及随附文档;它不授予输入图书或其他第三方内容的任何权利,也不会自动适用于生成的文件。输出内容的使用仍须遵守适用法律及相关许可或授权。工具不会判定著作权归属,也不会核实您的预期使用方式是否获准。
保留信息不等于核实权利。 生成 EPUB 时,工具会尽量保留源图书的署名与权利元数据。这并不代表翻译或分发已获许可。请检查输出内容,并按适用许可或授权的要求,保留或补充署名、版权声明、许可信息及修改说明。
不提供担保;责任限制。 软件按本项目 MIT 许可证的规定“按原样”提供,不附带任何担保。在适用法律允许的范围内,作者及著作权人按该许可证的规定,不承担因使用软件而产生的责任。
如对项目有问题或建议,请通过 issue 联系我们。
En Docker (`docs/en/docker.md`)
Docker
Use Docker if you do not want to set up Python yourself. Images are published to the GitHub Container Registry on every merge to main and on every release tag. The container runs python make_book.py, so every flag on this site works unchanged.
Images and tags
There are three images, built from one Dockerfile.
| tag | what is in it | platforms | use it for |
|---|---|---|---|
latest (also basic) |
the translator and its Python packages on python:3.12-slim; about 280 MB on amd64 |
amd64, arm64 | EPUB, TXT, Markdown, SRT, and PDFs on the older text route |
pdf |
latest plus Pandoc 3.11 and the PDF packages, with PyTorch's CPU build; about 2.4 GB on amd64 |
amd64, arm64 | --to-epub on the processor: macOS, arm64 Linux, and any machine without an NVIDIA card |
pdf-cuda |
the official PyTorch CUDA runtime image plus Pandoc 3.11, the translator and the PDF packages; its base alone is a 3.4 GB download | amd64 | --to-epub on an NVIDIA GPU, on Linux or on Windows |
<version>, <version>-pdf, <version>-pdf-cuda |
the same three images at a release | pinning a release | |
sha-<commit>, sha-<commit>-pdf, sha-<commit>-pdf-cuda |
the same three images at one commit | pinning a build |
docker pull ghcr.io/yihong0618/bilingual_book_maker:latestMounts
- Your book folder at
/book. Pass the book as/book/<file>. The translated book is written back into the same folder. - A named volume at
/root/.cache(thepdfandpdf-cudaimages). The docling models download there on the first--to-epubrun, about 500 MB. Without the volume every run downloads them again.
The container runs as root, so writing into the mounted folder always works. On Linux the files it writes belong to root: chown them afterwards, or add --user $(id -u).
Pass the key as an environment variable rather than on the command line: -e OPENAI_API_KEY.
An EPUB, per system
=== "Linux / macOS"
```bash
docker run --rm \
-v "$PWD":/book \
-e OPENAI_API_KEY \
ghcr.io/yihong0618/bilingual_book_maker:latest \
--book_name /book/my_book.epub \
--language zh-hans \
--use_context session
```=== "Windows PowerShell"
```powershell
docker run --rm `
-v "${PWD}:/book" `
-e OPENAI_API_KEY `
ghcr.io/yihong0618/bilingual_book_maker:latest `
--book_name /book/my_book.epub `
--language zh-hans `
--use_context session
```A test that needs no key at all, over the free Google route:
docker run --rm \
-v "$PWD":/book \
ghcr.io/yihong0618/bilingual_book_maker:latest \
--book_name /book/animal_farm.epub \
--api_format google \
--test \
--test_num 1 \
--language zh-hantA PDF, per system
The latest image has no Pandoc and no PDF packages, so it cannot run --to-epub. With an NVIDIA card use the pdf-cuda tag; everywhere else use pdf.
=== "Linux with NVIDIA"
Install the NVIDIA driver and the NVIDIA Container Toolkit on the host. Nothing else: the image carries the CUDA runtime.
```bash
docker run --rm --gpus all \
-v "$PWD":/book \
-v bbm-models:/root/.cache \
-e OPENAI_API_KEY \
ghcr.io/yihong0618/bilingual_book_maker:pdf-cuda \
--book_name /book/paper.pdf \
--to-epub \
--use_context session
```=== "Windows with NVIDIA"
Use Docker Desktop on the **WSL2 backend**. Install the WSL-capable NVIDIA driver on Windows itself, not inside WSL. Windows-containers mode cannot reach the GPU.
```powershell
docker run --rm --gpus all `
-v "${PWD}:/book" `
-v bbm-models:/root/.cache `
-e OPENAI_API_KEY `
ghcr.io/yihong0618/bilingual_book_maker:pdf-cuda `
--book_name /book/paper.pdf `
--to-epub `
--use_context session
```=== "CPU only"
The `pdf` image carries PyTorch's CPU build, for amd64 and arm64. `--device cpu` skips the accelerator detection. The output is the same as on a GPU; it is slower.
```bash
docker run --rm \
-v "$PWD":/book \
-v bbm-models:/root/.cache \
-e OPENAI_API_KEY \
ghcr.io/yihong0618/bilingual_book_maker:pdf \
--book_name /book/paper.pdf \
--to-epub \
--device cpu \
--use_context session
```=== "macOS"
The container is CPU-only whatever you pass: Docker runs a Linux VM that cannot see Metal. The `pdf` image runs natively on Apple silicon (arm64). The [native install](installation-pdf.md) is faster there (MPS).
```bash
docker run --rm \
-v "$PWD":/book \
-v bbm-models:/root/.cache \
-e OPENAI_API_KEY \
ghcr.io/yihong0618/bilingual_book_maker:pdf \
--book_name /book/paper.pdf \
--to-epub \
--device cpu \
--use_context session
```An NVIDIA card on arm64
pdf-cuda is built for amd64 only: the official PyTorch CUDA image it starts from is published for nothing else. On an arm64 Linux host with a card (GH200, Jetson), pdf runs on the processor.
What is not in any image
The Codex route (--api_format codex). It drives a codex binary signed in on your machine; neither the binary nor the login is in the container. Use an API route in Docker.
Build it yourself
A plain build gives the small image:
docker build --tag bilingual_book_maker .The PDF images are the pdf and pdf-cuda stages:
docker build --target pdf --tag bilingual_book_maker:pdf .
docker build --platform linux/amd64 --target pdf-cuda --tag bilingual_book_maker:pdf-cuda .En Env Settings (`docs/en/env_settings.md`)
Environment Settings
You can write credentials into the environment to skip --key.
API key
BBM_API_KEY covers every format, so one variable is usually enough:
export BBM_API_KEY=${your_api_key}If it is not set, each format also looks at the variable people already
export for that vendor:
--api_format |
Fallback variables |
|---|---|
openai |
OPENAI_API_KEY, BBM_OPENAI_API_KEY |
anthropic |
ANTHROPIC_API_KEY, BBM_CLAUDE_API_KEY |
gemini |
BBM_GOOGLE_GEMINI_KEY, GEMINI_API_KEY |
qwen |
BBM_QWEN_API_KEY, DASHSCOPE_API_KEY |
groq |
BBM_GROQ_API_KEY, GROQ_API_KEY |
xai |
BBM_XAI_API_KEY, XAI_API_KEY |
litellm |
BBM_LITELLM_API_KEY, LITELLM_MASTER_KEY |
caiyun |
BBM_CAIYUN_API_KEY |
deepl |
BBM_DEEPL_API_KEY |
google, deeplfree, tencent and customapi need no key, and neither
does an endpoint on localhost — a LiteLLM proxy there included, which is
where --api_format litellm points unless --api_base says otherwise.
The CLI does not read .env files. Export the variables first, or source a
git-ignored file before running: set -a; source .env; set +a; bbook_maker ...
A provider's own variable
--provider NAME reads the endpoint from bbm_providers.json in the working
directory, else ~/.bbm/providers.json. When that entry has an env_key,
the named variable is consulted for the key ahead of BBM_API_KEY and the
fallbacks above — it names the endpoint being called, so its own key is the
right one. It is consulted only while the run still calls that endpoint: an--api_base elsewhere, or an --api_format override on an entry that names
no base_url, moves the request to another host, and the entry's key is not
sent there. The run says so; pass --key when you meant to reuse it. The file holds the address and the variable name, never a secret.
See Endpoints, models and languages.--model orcarouter reads BBM_ORCAROUTER_API_KEY the same way.
Classifier variables
A Jev-compatible classifier reads its key only for its own host:
| variable | read for |
|---|---|
JEV_API_KEY, TYPESAFE_API_KEY |
TypeSafe's Jev at a typesafe.ai address |
CF_AIG_TOKEN |
an authenticated Cloudflare AI Gateway at gateway.ai.cloudflare.com |
Through a gateway, name the key with --classify-key or a provider entry's classify_env_key.
BBM_JEV_MIN_CONFIDENCE, a number from 0 to 1, sets Jev's gate for a run without --classify-min-confidence (the flag wins): a skip below it is translated. The default is 0.95. A value that is not a number from 0 to 1 stops the run.
Old per-vendor variables
The four vendor variables above are also read when an old-style command
implies that route — --model gemini consults BBM_GOOGLE_GEMINI_KEY even
though the rewritten command names the format (see Migrating from the old
flags). What they are never consulted for is a command whose--api_base names some other endpoint: there the key must match the address,
and only BBM_API_KEY and the format's own variables apply.
Prompt overrides
export BBM_CHATGPTAPI_USER_MSG_TEMPLATE=${your_prompt_template}
export BBM_CHATGPTAPI_SYS_MSG=${your_system_message}En Installation Pdf (`docs/en/installation-pdf.md`)
Installing the PDF extra
If you want to turn a PDF into a bilingual EPUB (--to-epub), install the PDF extra from a clone of the repository:
pip install ".[pdf]"--to-epub reads a PDF with docling's layout and table models, so it needs more than the base install: PyTorch, the models, and Pandoc to build the EPUB. None of it is installed by default, because most people translate EPUBs and never open a PDF. No Java is needed; instructions that mention a JRE or Adoptium are out of date.
1. Get the code
The route is not in the published package yet, so you install from a clone.
git clone https://github.com/yihong0618/bilingual_book_maker.gitcd bilingual_book_makerA virtual environment is recommended.
2. Pandoc
You need Pandoc 3.1.12 or newer on PATH. It builds the EPUB and its navigation. Older releases point the table of contents at files instead of headings, and the tool refuses them before the PDF is opened. The apt packages on Ubuntu 24.04 and Debian 13 are older than this, so take a release from pandoc.org/installing.html.
pandoc --version3. The PDF extra
Install the extra rather than the locked requirements files: the locked files pin exact versions of PyTorch and everything else, so they download again a torch you may already have; the extra reuses what is installed.
Which PyTorch build you get is decided by the index it comes from, not by the version. Pick your system:
=== "macOS (Apple silicon)"
```bash
pip install ".[pdf]"
```
You get MPS acceleration. There is nothing to choose.=== "Linux with NVIDIA"
```bash
pip install ".[pdf]"
```
PyPI's Linux wheel *is* the CUDA build, so this is all you need. The CUDA Toolkit is not needed; the wheel carries the runtime.=== "Linux, CPU only"
```bash
pip install ".[pdf]" \
--extra-index-url https://download.pytorch.org/whl/cpu
```
Without the CPU index you get about 3.2 GB of CUDA you cannot use. With it, about 380 MB, and no `nvidia-*` packages.=== "Windows with NVIDIA"
```bat
pip install ".[pdf]" ^
--extra-index-url https://download.pytorch.org/whl/cu126
```
**PyPI's Windows wheel is CPU-only.** Windows CUDA builds are published only on PyTorch's own index, so the plain line leaves you on the processor, silently. `cu128` is there for newer cards and drivers. The CUDA wheel is about 2.7 GB.
You also need the NVIDIA driver. PyTorch bundles the CUDA runtime, so you do **not** need the CUDA Toolkit, but the driver is yours to install:
- Driver download: <https://www.nvidia.com/en-us/drivers/> (Game Ready or Studio both work).
- PyTorch's installer matrix, to confirm the channel for your card: <https://pytorch.org/get-started/locally/>.
Check the driver before installing PyTorch:
```bat
nvidia-smi
```
It prints the driver version and the highest CUDA version it supports. If that number is below the channel you chose (12.6 for `cu126`), update the driver. An old driver is the usual reason `torch.cuda.is_available()` says `False` on a machine that has a card.=== "Windows, CPU only"
```bat
pip install ".[pdf]"
```
On Windows this gives you the CPU build, because that is what PyPI ships there.Use --extra-index-url, never --index-url. --index-url replaces PyPI, and everything else this tool needs would stop resolving.
If you want exactly the versions the project tests, requirements-pdf-gpu.txt and requirements-pdf-cpu.txt are the pinned sets the Docker images install (pdf the CPU file, pdf-cuda the GPU file minus the PyTorch its base already has; pip install -r requirements-pdf-cpu.txt names the CPU index inside the file). Expect them to replace the PyTorch you have.
With uv
uv pip install ".[pdf]"When you want the CPU build:
uv pip install ".[pdf]" --torch-backend=cpu4. Run it
Read two pages first. --test translates only a few blocks.
python make_book.py \
--book_name paper.pdf \
--to-epub \
--pages 1-2 \
--testThen the full run:
python make_book.py \
--book_name paper.pdf \
--to-epub \
--use_context sessionThe models (about 500 MB) download on the first run, so the first PDF takes noticeably longer than the second. The progress line keeps running while they download. They come from Hugging Face, so HF_HOME moves the cache:
export HF_HOME=/path/with/roomA scan read with --pdf-ocr downloads nothing more: the pdf extra installs rapidocr with its models, and on a Mac also Apple's engine, ocrmac, which auto picks there. Which OCR engine compares them.
Once cached, extraction needs no network. Recommended settings for PDF has the command for each kind of document.
Check what you got
python -c "import torch; print(torch.__version__, torch.version.cuda)"- A version ending in
+cpu, andNone: the CPU build. Correct for the CPU route. - A CUDA version like
12.6: a CUDA build.torch.cuda.is_available()then says whether this machine can use it.Falsemeans the build has CUDA but there is no usable card, or the driver is too old. - On Apple silicon,
Noneis correct: MPS is not CUDA. Check it withpython -c "import torch; print(torch.backends.mps.is_available())". - On Windows with an NVIDIA card,
Nonemeans you are on the CPU build, which is what PyPI ships there. Go back to step 3.
--device cuda tells these two failures apart, because they have different fixes: a CPU-only build is a reinstall; a machine without a card is not.
Installing without a clone: not yet
pip install "bbook_maker[pdf]" does not install the route, and it does not fail either. pip treats an unknown extra as a warning, installs the last release without docling, and exits 0:
WARNING: bbook-maker 1.2.1 does not provide the extra 'pdf'
Successfully installed bbook-maker-1.2.1The next PDF run refuses with the missing-extra message. Until a release carries the route, clone the repository (step 1) and install ".[pdf]" from it.
Keeping the CPU build
torch==…+cpu satisfies any torch>=… requirement, so nothing forces it to be replaced. But the next pip install -U that touches PyTorch without the index will fetch the CUDA wheel and pull in about 3 GB. Make the index stick to the environment:
export PIP_EXTRA_INDEX_URL=https://download.pytorch.org/whl/cpuOr in pip.conf / pip.ini:
[global]
extra-index-url = https://download.pytorch.org/whl/cpuThe uv equivalent is UV_TORCH_BACKEND=cpu.
Or skip all of it: Docker
The pdf image tag carries Pandoc and the whole docling runtime with PyTorch's CPU build: docker pull ghcr.io/yihong0618/bilingual_book_maker:pdf. With an NVIDIA card, the pdf-cuda tag. See Docker, including why a Mac should install natively instead.
Sizes
What the PDF step downloads with PyTorch 2.7.1, resolved on each platform (102 packages):
| download | |
|---|---|
| macOS, Apple silicon | ~270 MB |
| Linux x86_64, CPU build | ~380 MB |
| Linux x86_64, CUDA build | ~3.2 GB |
| Windows x86_64, PyPI — the CPU build | ~420 MB |
Windows x86_64, cu126 |
~2.9 GB |
Plus ~500 MB of models on the first run, on every platform.
PyTorch is most of the variation. The rest of the tree is about 200 MB everywhere (opencv 48 MB, scipy 29 MB, rapidocr 27 MB, transformers, numpy, pandas…). The PyTorch wheel alone:
| torch 2.7.1 wheel | |
|---|---|
| macOS arm64 | 68.6 MB |
Linux x86_64, +cpu |
175.8 MB, and its metadata declares no nvidia-* requirements |
| Linux x86_64, PyPI default | 821.0 MB, plus ~2.16 GB of nvidia-* and triton wheels |
| Windows x86_64, PyPI | 216.0 MB — the CPU build |
Windows x86_64, cu126 |
2.72 GB |
The two platforms are opposites, which is the trap: on Linux the default is CUDA and you opt out; on Windows the default is CPU and you opt in. macOS has no CUDA variant; its small PyTorch carries Metal kernels and no CUDA, and nothing is missing.
If it does not work
These are the lines the tool prints, and what to do.
reading a PDF needs the pdf extra, which is not installed.Step 3 was not done. If you ranpip install "bbook_maker[pdf]"and it said it succeeded, that is the trap described above.Pandoc is required for --to-epub. Install it and make sure pandoc is on PATH.Do step 2.… is too old for EPUB export; Pandoc 3.1.12 or newer is requiredYour Pandoc came from apt. Install the release from pandoc.org (step 2) and put it first on PATH; the message names the harnesstools/pdf_to_book.py --pandoc PATHas the other way.--device cuda was asked for, but the installed PyTorch is a CPU-only build.Reinstall the extra with the CUDA build: the plain line on Linux, thecu126index on Windows (step 3).--device cuda was asked for, but this machine has no cuda accelerator available.The build has CUDA; the machine or driver cannot provide it. Runnvidia-smi: no output means no driver; a CUDA version lower than your channel means the driver is too old. Otherwise use--device cpu.- On Windows,
torch.version.cudaisNonealthough the machine has a card. The plain line was used. Reinstall naming thecu126index (step 3). … selected pages have no text layer …; rerun with --pdf-ocr …The PDF is a scan. Add--pdf-ocr, and--ocr-langif the scan is not in Chinese or English. See Recommended settings for PDF.- In Docker,
--gpus allseems ignored on an ARM machine. It is: the arm64 image has a CPU-only PyTorch. Add--platform linux/amd64. - In Docker on a Mac, the GPU is never used. Correct and unfixable: the Linux VM cannot see Metal. Install natively (steps 1 to 4) for MPS.
En Installation (`docs/en/installation.md`)
Installation
bilingual book maker is a Python program. It needs Python 3.10 or newer. A virtual environment keeps it apart from your other packages.
From PyPI
pip install -U bbook_makerThis gives you the bbook_maker command. It translates EPUB, TXT, Markdown and SRT files, and PDFs on the older text route.
The published package does not carry the PDF-to-EPUB route yet. If you want that, install from a checkout (below) and add the PDF extra.
From a checkout
Use a checkout if you want the newest code or the PDF-to-EPUB route.
git clone https://github.com/yihong0618/bilingual_book_maker.gitcd bilingual_book_makerpip install -r requirements.txtrequirements.txt is the pinned, tested set. pip install . also works and takes whatever versions resolve today. From a checkout the command is python make_book.py, with the same flags as bbook_maker.
The PDF extra
If you want to turn PDFs into bilingual EPUBs (--to-epub), add the PDF extra from the same checkout. It brings docling and PyTorch; the models (about 500 MB) download on the first run, and you also need Pandoc 3.1.12 or newer.
pip install ".[pdf]"The extra reuses a PyTorch you already have instead of downloading a pinned one. That line is right on Apple silicon, on Linux with an NVIDIA GPU, and on Windows without one. Linux without an NVIDIA GPU adds PyTorch's CPU index, or it downloads about 3 GB of CUDA it cannot use; Windows with an NVIDIA GPU adds PyTorch's CUDA index. Installing the PDF extra has the line for every case.
Do not run pip install "bbook_maker[pdf]". The published package does not carry the route yet: pip only warns and installs the release without it.
Docker
If you would rather not install Python packages, use the published image. See Docker.
Check the install
bbook_maker --helpFrom a checkout:
python make_book.py --helpThe help text is the authority on every flag. Command line options lists them in one table.
En Languages (`docs/en/languages.md`)
Languages
--language takes any row of this table either way: the tag
(--language zh-hant) or the name (--language "traditional chinese",
case-insensitive). The tag is the mechanical half — stamped on the
inserted markup and dc:language, recorded in the translation metadata record,
and it names the structured-output reply field (zh_hant_translation).
The name is the prose half: what the model is actually asked for.
For a language this table misses, pass both halves yourself asTAG:NAME, split on the first colon — for example--language "ain:Ainu". The tag then drives the stamps and
field names exactly as if it were listed here; the name goes to the
model. A free-typed name matching nothing still runs — the run prints
one Note: line saying nothing will be stamped on the output.
The source language is never part of --language; --source_lang
states it when auto-detection isn't enough.
Tags
| Tag | Name |
|---|---|
aa |
afar |
ab |
abkhaz |
ace |
acehnese |
af |
afrikaans |
ak |
akan |
akk |
akkadian |
am |
amharic |
an |
aragonese |
ang |
old english |
apc |
levantine arabic |
ar |
arabic |
arc |
aramaic |
arn |
mapudungun |
ary |
moroccan arabic |
arz |
egyptian arabic |
as |
assamese |
ast |
asturian |
av |
avar |
ay |
aymara |
az |
azerbaijani |
ba |
bashkir |
ban |
balinese |
bcl |
bikol |
be |
belarusian |
bg |
bulgarian |
bho |
bhojpuri |
bi |
bislama |
bm |
bambara |
bn |
bengali |
bo |
tibetan |
br |
breton |
brx |
bodo |
bs |
bosnian |
bug |
buginese |
ca |
catalan |
ce |
chechen |
ceb |
cebuano |
ch |
chamorro |
chr |
cherokee |
ckb |
central kurdish |
co |
corsican |
cop |
coptic |
cr |
cree |
cs |
czech |
csb |
kashubian |
cu |
church slavonic |
cv |
chuvash |
cy |
welsh |
da |
danish |
de |
german |
de-at |
austrian german |
de-ch |
swiss standard german |
doi |
dogri |
dsb |
lower sorbian |
dv |
dhivehi |
dz |
dzongkha |
ee |
ewe |
egy |
ancient egyptian |
el |
greek |
en |
english |
en-au |
australian english |
en-ca |
canadian english |
en-gb |
british english |
en-in |
indian english |
en-us |
american english |
enm |
middle english |
eo |
esperanto |
es |
spanish |
es-419 |
latin american spanish |
es-ar |
argentine spanish |
es-es |
european spanish |
es-mx |
mexican spanish |
et |
estonian |
eu |
basque |
fa |
persian |
ff |
fulah |
fi |
finnish |
fil |
filipino |
fj |
fijian |
fo |
faroese |
fr |
french |
fr-ca |
canadian french |
fro |
old french |
fur |
friulian |
fy |
western frisian |
ga |
irish |
gd |
scottish gaelic |
gl |
galician |
gn |
guarani |
goh |
old high german |
got |
gothic |
grc |
ancient greek |
gsw |
swiss german |
gu |
gujarati |
gv |
manx |
ha |
hausa |
hak |
hakka |
haw |
hawaiian |
he |
hebrew |
hi |
hindi |
hil |
hiligaynon |
hit |
hittite |
hr |
croatian |
hsb |
upper sorbian |
ht |
haitian creole |
hu |
hungarian |
hy |
armenian |
ia |
interlingua |
id |
indonesian |
ie |
interlingue |
ig |
igbo |
ilo |
ilocano |
io |
ido |
is |
icelandic |
it |
italian |
iu |
inuktitut |
ja |
japanese |
jam |
jamaican patois |
jbo |
lojban |
jw |
javanese |
ka |
georgian |
kab |
kabyle |
kam |
kamba |
kbd |
kabardian |
kg |
kongo |
ki |
kikuyu |
kk |
kazakh |
kl |
greenlandic |
km |
khmer |
kn |
kannada |
ko |
korean |
kok |
konkani |
ks |
kashmiri |
ku |
kurdish |
kv |
komi |
kw |
cornish |
ky |
kyrgyz |
la |
latin |
lad |
ladino |
lb |
luxembourgish |
lez |
lezgian |
lg |
ganda |
lij |
ligurian |
lld |
ladin |
lmo |
lombard |
ln |
lingala |
lo |
lao |
lt |
lithuanian |
lu |
luba-katanga |
luo |
luo |
lv |
latvian |
lzh |
literary chinese |
mad |
madurese |
mai |
maithili |
mer |
meru |
mg |
malagasy |
mi |
maori |
min |
minangkabau |
mk |
macedonian |
ml |
malayalam |
mn |
mongolian |
mni |
manipuri |
mr |
marathi |
ms |
malay |
mt |
maltese |
mwl |
mirandese |
my |
myanmar |
myv |
erzya |
nah |
nahuatl |
nan |
hokkien |
nap |
neapolitan |
nb |
norwegian bokmal |
nds |
low german |
ne |
nepali |
new |
newari |
nl |
dutch |
nl-be |
belgian dutch |
nn |
nynorsk |
no |
norwegian |
non |
old norse |
nr |
southern ndebele |
nso |
northern sotho |
nv |
navajo |
ny |
chichewa |
nyn |
nyankole |
oc |
occitan |
oj |
ojibwe |
om |
oromo |
or |
odia |
os |
ossetian |
pa |
punjabi |
pal |
middle persian |
pam |
kapampangan |
pap |
papiamento |
pi |
pali |
pl |
polish |
pms |
piedmontese |
ps |
pashto |
pt |
portuguese |
pt-br |
brazilian portuguese |
pt-pt |
european portuguese |
qu |
quechua |
rm |
romansh |
rn |
kirundi |
ro |
romanian |
ru |
russian |
rue |
rusyn |
rw |
kinyarwanda |
sa |
sanskrit |
sah |
yakut |
sat |
santali |
sc |
sardinian |
scn |
sicilian |
sco |
scots |
sd |
sindhi |
se |
northern sami |
sg |
sango |
sga |
old irish |
shn |
shan |
si |
sinhala |
sk |
slovak |
sl |
slovenian |
sm |
samoan |
sn |
shona |
so |
somali |
sq |
albanian |
sr |
serbian |
sr-cyrl |
serbian cyrillic |
sr-latn |
serbian latin |
srn |
sranan tongo |
ss |
swati |
st |
southern sotho |
su |
sundanese |
sux |
sumerian |
sv |
swedish |
sw |
swahili |
syr |
syriac |
szl |
silesian |
ta |
tamil |
te |
telugu |
tg |
tajik |
th |
thai |
ti |
tigrinya |
tk |
turkmen |
tl |
tagalog |
tn |
tswana |
to |
tongan |
tpi |
tok pisin |
tr |
turkish |
ts |
tsonga |
tt |
tatar |
ty |
tahitian |
tyv |
tuvan |
tzm |
central atlas tamazight |
udm |
udmurt |
ug |
uyghur |
uk |
ukrainian |
umb |
umbundu |
ur |
urdu |
uz |
uzbek |
ve |
venda |
vec |
venetian |
vi |
vietnamese |
vo |
volapuk |
wa |
walloon |
war |
waray |
wo |
wolof |
wuu |
wu chinese |
xcl |
classical armenian |
xh |
xhosa |
yi |
yiddish |
yo |
yoruba |
yua |
yucatec maya |
zh |
chinese |
zh-cn |
mainland chinese |
zh-hans |
simplified chinese |
zh-hant |
traditional chinese |
zh-hk |
hong kong chinese |
zh-sg |
singapore chinese |
zh-tw |
taiwan mandarin |
zh-yue |
cantonese |
zu |
zulu |
Other spellings
Names that are not the one printed above, accepted for the same tag.
| You may type | Tag |
|---|---|
bahasa indonesia |
id |
bahasa melayu |
ms |
bokmal |
nb |
brazilian |
pt-br |
burmese |
my |
castilian |
es |
chewa |
ny |
classical chinese |
lzh |
divehi |
dv |
farsi |
fa |
flemish |
nl |
frisian |
fy |
haitian |
ht |
irish gaelic |
ga |
isixhosa |
xh |
isizulu |
zu |
jamaican creole |
jam |
judeo-spanish |
lad |
kalaallisut |
kl |
kirghiz |
ky |
letzeburgesch |
lb |
maldivian |
dv |
mandarin |
zh |
mandarin chinese |
zh |
min nan |
nan |
modern standard arabic |
ar |
moldavian |
ro |
moldovan |
ro |
norwegian nynorsk |
nn |
nyanja |
ny |
old church slavonic |
cu |
oriya |
or |
panjabi |
pa |
pilipino |
fil |
pushto |
ps |
putonghua |
zh |
scots gaelic |
gd |
sesotho |
st |
setswana |
tn |
shanghainese |
wuu |
sinhalese |
si |
sorani |
ckb |
standard chinese |
zh |
swiss high german |
de-ch |
taiwanese hokkien |
nan |
twi |
ak |
uighur |
ug |
valencian |
ca |
En Llm Args (`docs/en/llm-args.md`)
Translating with an LLM
If you have an OpenAI key, the default route needs nothing else:
export OPENAI_API_KEY=sk-...
bbook_maker --book_name my_book.epub --use_context sessionThis page covers the flags that decide where the requests go and what they ask for. They work the same for every input format. What each format adds is on the Formats pages.
The route: model, endpoint, format
A route is an endpoint, not a model name. Three flags name it.
| flag | what it does |
|---|---|
--model (-m) |
The model id, exactly as the endpoint names it: gpt-5-mini, claude-sonnet-4-6, openai/gpt-5-mini. Default gpt-6-luna on the openai format. The anthropic format needs one. |
--api_base |
The endpoint URL. Defaults to the format's official host. An OpenAI-shaped endpoint ends in /v1. A pasted …/v1/chat/completions or a trailing slash is trimmed. |
--api_format |
The API the endpoint speaks: openai, anthropic, codex, gemini, qwen, groq, xai, litellm, or a translation service. Inferred from --api_base when you leave it out: an Anthropic host means anthropic, everything else openai. A claude-* model id with no --api_base also selects anthropic. |
Examples, one per common case:
=== "OpenAI"
```bash
bbook_maker \
--book_name my_book.epub \
--model gpt-6-luna \
--use_context session
```=== "Anthropic"
```bash
bbook_maker \
--book_name my_book.epub \
--model claude-sonnet-4-6 \
--use_context session
```=== "Any OpenAI-compatible endpoint"
```bash
bbook_maker \
--book_name my_book.epub \
--api_base https://gateway.example.com/v1 \
--model provider/model-id \
--use_context session
```=== "Gemini"
```bash
bbook_maker \
--book_name my_book.epub \
--api_format gemini \
--model gemini-flash-latest \
--use_context
```
Gemini keeps its own chat history, so it takes window mode (`--use_context`), not a session. `--interval` sets the pause between requests; this is how you stay under the free tier's rate limit.=== "Codex (ChatGPT plan)"
```bash
python make_book.py \
--book_name my_book.epub \
--api_format codex
```
Spends your ChatGPT plan through the [Codex CLI](https://developers.openai.com/codex/cli), which must be installed and signed in (`codex login`). It runs `gpt-6-luna`; add `--model ` for another. The thread is the context, so no `--use_context` is needed. It ignores `--api_base` and `--key`. When Codex answers that the selected model is at capacity, the run prints a `codex: … retrying in 60 s` line and asks again; that answer is usually Codex rate-limiting a network it distrusts, and another network or account helps.A gateway that serves Claude models usually speaks the OpenAI shape. If a gateway answers 404 to the anthropic shape, the run stops and names --api_format openai as the fix.
--api_format groq, xai and litellm are the OpenAI shape at Groq, xAI and a LiteLLM proxy (http://localhost:4000). Each knows its address, so the format, a key and --model are the whole route. --model is required there.
--model orcarouter sends the run to the OrcaRouter gateway and its orcarouter/auto model. The key comes from --key or BBM_ORCAROUTER_API_KEY.
One command per vendor
If your vendor is listed here, this is the whole command (the key can also come from the environment, see Keys):
/* Detailed source-code truncated for AI context efficiency. */Translation services (Google, DeepL, Caiyun, Tencent, a custom API) are on Translation services. A vendor you use often belongs in the provider file.
Keys
The key is looked up in this order:
--key(--api_keyis the same flag). Several comma-separated keys are rotated, to go past per-key rate limits.BBM_API_KEY.- The format's own variable:
OPENAI_API_KEY,ANTHROPIC_API_KEY,BBM_GOOGLE_GEMINI_KEY,BBM_QWEN_API_KEY,BBM_GROQ_API_KEY,BBM_XAI_API_KEY.
An endpoint on localhost needs no key. A key on the command line is visible to other users of the machine in the process list; prefer the environment. The tool does not read .env files itself. Load one first:
set -a; source .env; set +aMore variables are on Environment variables.
The provider file and extra models
--provider NAME takes the route from a JSON file instead of flags: bbm_providers.json in the directory you run from, or ~/.bbm/providers.json. A name in neither falls back to the shipped bbm_providers.example.json, and the run says so.
bbook_maker \
--book_name my_book.epub \
--provider siliconflow \
--use_context sessionTwo steps can use a model other than the translating one:
| flag | what it does |
|---|---|
--classify-model MODEL |
The model that decides an EPUB's plan, or a Jev-compatible classifier (jev for TypeSafe's). Default: the entry's classify_model, else the translating model. --plan-classify-model is its old name. |
--classify-base-url URL, --classify-key KEY |
Where that model is served and its key, when it is not the run's endpoint: an OpenAI-compatible address, or a Jev-compatible classifier's URL. |
--img-model MODEL |
A vision model for the steps that look at a page image (today: correcting region roles on the PDF route). Default: the entry's img_model, else off. Never the translating model by fallback. none turns an entry's image model off. |
--img-base-url URL, --img-key KEY |
Where that model is served and its key (OpenAI-compatible only). |
The fields, the shipped entries, which key is sent where, and Jev and Jev-compatible classifiers are on Provider file and extra models. Note that the shipped openai entry names an image model, so --provider openai turns the PDF route's image step on.
Language
| flag | what it does |
|---|---|
--language |
The target language. A tag (zh-hant), a name ("Traditional Chinese"), or TAG:NAME when the tables miss the language (--language "zh-hant:Traditional Chinese"). The tag is stamped on the output; the name is what the model is asked for. Default zh-hans. The full list is on Language tags. |
--source_lang |
The source language, stated rather than detected. Named in the prompt on every LLM route. Default: detect. |
The prompt
--prompt takes a template string, a JSON string, or a .json, .txt or .md file. It has three sections:
user: the template. Required. It must contain{text};{language}and{crlf}are filled in too. Any other placeholder is refused when the run starts.system: the system message.style: a note on register and voice. It is said once where a context window starts, and handed on to every window in session mode.
A bare string or a .txt file is the user template. The run prints which sections it used and where. The format is on Prompt files.
bbook_maker \
--book_name my_book.epub \
--prompt prompt_template.json \
--use_context sessionTest runs and resuming
| flag | what it does |
|---|---|
--test |
Translate only the first paragraphs, to check the setup. |
--test_num N |
How many (default 10). |
--resume |
Continue an interrupted run from its checkpoint. An EPUB checkpoint records the language, prompt and model; a mismatch stops the resume. |
Always run --test once on a new endpoint. It costs almost nothing and shows you the output format and the route the run chose.
Retries: what "patient" means
Many people run this tool against providers that are slow or rate-limited, where the first token or a 429 backoff can take minutes. So the tool never gives up on an error that could clear by waiting.
- A timeout, a dropped connection, a 429 or a 5xx is retried with no attempt limit. The wait starts at 1 second, doubles, and stops growing at 5 minutes.
- Every retry prints one line, so a long wait does not look like a hang:
retrying after RateLimitError (…) — attempt 4, waiting 8s - An error that will not clear by waiting stops the run at once: a rejected key, a permission error, a request the endpoint refuses, a model that does not exist.
- Ctrl+C always stops the run, even during a long wait. The progress is saved; rerun with
--resume.
If you see the same retry line for a long time, the provider is down or rate-limiting you. The run will continue by itself when it comes back.
On-device models (Ollama, llama.cpp, LM Studio)
A local model is an OpenAI-compatible endpoint on your own machine. Point --api_base at it; no key is needed.
bbook_maker \
--book_name my_book.epub \
--api_base http://localhost:11434/v1 \
--model qwen3:8b \
--use_context sessionWhat to expect from an 8B or 16B model:
- No strict JSON schema. Most local servers do not enforce one. The run notices this with a one-request probe and switches to a delimiter format, printing a line such as
doesn't apply JSON schema … using delimiter method. This is not an error. - Smaller requests. On an endpoint without a strict schema, each request carries at most 8 units, half of the default 16, and outside session mode about 800 tokens of text. The run prints the numbers it chose. The defaults are chosen so that a small model can hold them.
- Plan classification by one word. Plan mode asks the model
skiportranslatefor five kinds of block per turn. If the model misses the reply format twice in a row, the run asks three per turn, then one. If it still misses, classification stops and the rest is translated. Anything unreadable is treated astranslate, so a weak model translates too much rather than too little. You can also hand classification to a hosted model with--classify-modeland keep translating locally; see Provider file and extra models. - Short context. Set
--context-compact-atto the model's input limit (minimum 1500) so a session window never overflows it. - Thinking models. A model that reasons before it answers can be told not to with
--no-thinking. The run finds the request field the server accepts from the server's own rejections. If your server needs a field of its own,--extra_bodywins, for example--extra_body '{"chat_template_kwargs": {"enable_thinking": false}}'. - No image step. The PDF route's image step runs only on a model you name with
--img-model, so a local model is never asked to read a page.
Flags to avoid on a small model:
--glossary-auto on. It needs a model that answers with names, not prose.- Raising
--accumulated_numor--max-batch-units. Lower them instead when the run printsmisaligned batches this run. --model_list. Each model keeps its own cache, and mixing models in one conversation hurts consistency.
If plan classification keeps failing on your model, --plan-classify all skips it and translates every block. See Plan mode.
Other request options
| flag | what it does |
|---|---|
--temperature |
Sampling temperature. The anthropic format always sends it; the openai format leaves it out when it equals the API default or when the model rejects one; codex ignores it. |
--no-thinking |
Ask the model not to reason before answering; thinking buys nothing on a paragraph and costs tokens and time. On the OpenAI-shaped routes the request field is found from the endpoint's own rejections and remembered; if the endpoint refuses every spelling, the run warns once and goes on without it. On anthropic it is thinking: {"type": "disabled"}. Refused on codex. Image requests carry it too. |
--extra_body JSON |
Extra fields on every request body (openai and anthropic routes). A field here beats the flag for it, --no-thinking included. |
--extra_headers JSON |
Extra HTTP headers on every request (openai and anthropic routes). |
--model_list IDS |
Several models to rotate across, to spread rate limits. Refused with --use_context session. |
--interval SECONDS |
Pause between requests. Only the gemini format uses it. |
-p, --proxy URL |
An HTTP proxy for the run, for example http://127.0.0.1:7890. |
--batch, --batch-use |
OpenAI's Batch API: submit a job, then build the book from it later. Not on EPUB. |
Common --extra_body and --extra_headers values:
# openai route: turn off a local/vLLM chat template's thinking block
--extra_body '{"chat_template_kwargs": {"enable_thinking": false}}'
# openai route (chat completions): reasoning effort and a token ceiling, neither of which has a flag
--extra_body '{"reasoning_effort": "low", "max_completion_tokens": 2000}'
# anthropic route: keep extended thinking off
--extra_body '{"thinking": {"type": "disabled"}}'
# OpenRouter attribution, shown on its dashboard
--extra_headers '{"HTTP-Referer": "https://example.com", "X-Title": "bilingual_book_maker"}'
# a gateway's own auth or routing header
--extra_headers '{"X-API-Key": "sk-gateway-..."}'En Machine Args (`docs/en/machine-args.md`)
Translation services
These routes call a translation service: text goes in, a translation comes back. Some of these services run a language model behind their API, but none takes a prompt from this tool. They need no model and often no key. They also cannot answer a question, so everything in this tool that asks the model something is off on them.
The routes
Pick one with --api_format.
--api_format |
service | key | target languages |
|---|---|---|---|
google |
Google Translate's public web endpoint | none | most; the source is detected |
deepl |
DeepL through the RapidAPI DeepL Translator | --key, or BBM_DEEPL_API_KEY |
DeepL's list; others are refused with DeepL do not support … |
deeplfree |
DeepL's free web endpoint | none | DeepL's list |
caiyun |
Caiyun | --key, or BBM_CAIYUN_API_KEY |
Simplified Chinese, English, Japanese |
tencent |
Tencent TranSmart | none | Chinese or English |
customapi |
your own HTTP service | none; the address goes in --api_base |
whatever your service does |
Both DeepL routes send the source as English. Use them for English books.
Caiyun publishes a test token, 3975l6lr5pcbvidl6jl2, for trying the route; for your own, follow this tutorial.
Commands
=== "Google"
```bash
bbook_maker \
--book_name my_book.epub \
--api_format google \
--language zh-hant
```=== "DeepL"
```bash
bbook_maker \
--book_name my_book.epub \
--api_format deepl \
--key ${deepl_key} \
--language ja
```=== "DeepL free"
```bash
bbook_maker \
--book_name my_book.epub \
--api_format deeplfree \
--language de
```=== "Caiyun"
```bash
bbook_maker \
--book_name my_book.epub \
--api_format caiyun \
--key ${caiyun_key} \
--language zh-hans
```=== "Tencent"
```bash
bbook_maker \
--book_name my_book.epub \
--api_format tencent \
--language zh-hans
```=== "Custom API"
```bash
bbook_maker \
--book_name my_book.epub \
--api_format customapi \
--api_base https://your.host/translate \
--language ja
```The custom API contract
The tool sends one POST per paragraph to --api_base, form-encoded, with three fields: text, source_lang and target_lang. It reads the translation from the data field of the JSON answer. Each request times out after 10 seconds, and the tool waits 5 seconds between requests. --source_lang reaches your service here.
What these routes cannot do
- No prompt.
--prompthas no place to go; the text is sent alone. - No session.
--use_context sessionis refused: there is no conversation to keep. - No plan classification by the engine itself. On an EPUB the run falls back to the
--translate-tagsselection (pby default), so verse or tables outside `` stay untranslated. Name an LLM classifier and the book gets a full plan while the engine translates:--classify-model gpt-6-luna(it readsOPENAI_API_KEY), or a provider entry'sclassify_model. This was run end to end on Google Translate. Without one,--plan-classify modelis refused, and--plan-classify allstill translates every block the plan finds without asking anyone. See Plan mode. - No glossary.
--glossaryis read only by the OpenAI- and Codex-shaped routes; here it is ignored with a warning. - No stated source language on
google,deepl,deeplfree,caiyunandtencent.--source_langis ignored with a warning; onlycustomapi(and theqwenLLM route) send it.
Everything else works: --test, --resume, --single_translate, the per-format flags on the Formats pages.
Rate limits
Caiyun prints will sleep 60s for the time limit when it is rate-limited, and waits. DeepL free and Tencent pause between requests on their own. The patient retry policy applies to every route.
En Migration (`docs/en/migration.md`)
Migrating from the old flags
Commands written for the old CLI keep working. Each removed flag is rewritten
into the new flags before the run starts, and a line says what it became:
$ bbook_maker --book_name book.epub --model gpt4omini --openai_key sk-...
deprecated: --openai_key is now --key
deprecated: --model gpt4omini is now --model gpt-4o-mini| Old | Rewritten to |
|---|---|
--model gpt4o / gpt4omini / o3mini |
--model with that model's id |
--model chatgptapi / openai |
dropped: the openai format is the default, and --model names a model when you want one |
--model openai --model_list X |
--model_list X |
--model claude |
--model claude-haiku-4-5-20251001 |
an exact claude-* id |
unchanged; the anthropic format is inferred from the id |
--model gemini / geminipro |
--api_format gemini --model gemini-flash-latest / gemini-pro-latest |
--model qwen / qwen-mt-turbo / qwen-mt-plus |
--api_format qwen --model qwen-mt-* |
--model groq --model_list X |
--api_format groq --model_list X |
--model xai |
--api_format xai --model grok-4.3 |
--model codex |
--api_format codex (the sidecar's default model; add --model to name one) |
--model google / caiyun / deepl / deeplfree / tencentransmart |
--api_format google / caiyun / deepl / deeplfree / tencent |
--custom_api URL |
--api_format customapi --api_base URL |
--openai_key / --claude_key / --gemini_key / --groq_key / --xai_key / --qwen_key / --caiyun_key / --deepl_key / --orcarouter_key |
--key (--api_key is the same flag and was never renamed) |
--ollama_model M |
--api_base http://localhost:11434/v1 --model M |
--deployment_id D |
--model D, with --api_base rewritten to the deployment's /openai/v1 path |
Notes:
- The old key variables still work for the route that used them:
BBM_GROQ_API_KEYfor a rewritten--model groq,BBM_GOOGLE_GEMINI_KEY
for a rewritten--model gemini, and so on. - Flags you pass yourself win.
--model gemini --api_base https://my-gateway/v1
keeps your gateway. --intervalis not a legacy flag: it is still in the parser and still
paces the gemini route, which is still a route.qwen-mt-turboandqwen-mt-plusare real model ids, so a command that
also passes--api_formatis left alone and nothing is printed about it.- The other route aliases are not model ids anywhere.
--model gemini
together with an--api_formatnaming a different route is refused rather
than resolved one way or the other: honouring the format would send that
alias's key to a host it does not belong to, and honouring the alias would
ignore what you typed. The error names both and the two ways out. - A rewritten command runs the model it used to run, taken from the old
preset list, not a newer one. Some of those models have since been
retired, and the endpoint's model check says so. - A
--modelvalue that is not an old alias passes through as a model id,
which is the normal case now. - The aliases for retired OpenAI models (
gpt4,gpt5mini,o1,o1mini,o1preview) are gone. They now pass through as model ids and the endpoint
rejects them by name, which is the same failure one step earlier and with
a clearer message.
En Model Lang (`docs/en/model_lang.md`)
Endpoints, models and languages
A route is chosen by the endpoint it talks to, not by a model name. There is
no built-in model list to keep up to date: --model takes whatever id
your endpoint serves.
bbook_maker --book_name book.epub \
--api_base https://api.openai.com/v1 --key sk-... \
--model gpt-5-mini --language jaOld --model commands still work — they are rewritten into these flags and
the substitution is printed. Migrating from the old flags
has the full table.
The route flags
| Flag | Meaning |
|---|---|
--model |
Model id, exactly as the endpoint names it. On the openai format it may be left out: it defaults to gpt-6-luna. |
--api_base |
Endpoint URL. Defaults to the format's official host; …/v1, …/v1/ and …/v1/chat/completions all work. |
--key |
API key. Comma-separate several to rotate them and spread rate limits. |
--api_format |
Wire format. Inferred; pass it only when the guess is wrong. |
--provider |
A named endpoint from a provider file, standing in for the four above. |
--model_list a,b rotates across several models and is also what older
commands used; name a model in one flag or the other, not both.
--api_format is one of openai (default), anthropic, gemini, qwen,groq, xai, litellm, codex, or the fixed translation servicesgoogle, caiyun, deepl, deeplfree, tencent, customapi.
The five vendor formats each carry their own endpoint, so the format and a
key are a whole route with no --api_base to look up:
| Format | Endpoint | --model |
|---|---|---|
gemini |
the Gemini API | optional, default gemini-flash-latest |
qwen |
Qwen-MT on DashScope | optional, default qwen-mt-turbo |
groq |
https://api.groq.com/openai/v1 |
required |
xai |
https://api.x.ai/v1 |
required |
litellm |
http://localhost:4000 |
required, the name in the proxy's config |
gemini and qwen are protocols of their own — Gemini's native
constrained decoding, its safety settings and chat history; Qwen-MT's
source/target language pair in place of a prompt — and --interval paces
the gemini route, which is how the free tier's rate limit is stayed under.
The other three are the OpenAI route at another address and keep everything
it has. None of the five is ever inferred from a host: name the format, or
give the vendor's OpenAI-compatible --api_base and get the openai route.
codex is not an endpoint at all: it drives a local codex app-server
sidecar and bills the run to your ChatGPT plan, so it takes no --key and no--api_base, and --model is optional (default gpt-6-luna). It is never
inferred; name it explicitly. See Translating with an LLM.
Inference goes in this order: an explicit --api_format wins; then the--api_base host (anthropic.com means the anthropic shape, anything else
the OpenAI shape); then, when no endpoint was named, a model id mentioningclaude or anthropic, anthropic/claude-sonnet-4-6 included.
A gateway asked for the anthropic shape it does not serve answers 404 or
405 on /v1/messages. The run stops and names the fix: rerun with--api_format openai. On Anthropic's own host a 404 means the model does
not exist and is reported as such.
Credentials come from --key, then BBM_API_KEY, then the format's
conventional variable — see Environment settings. An
endpoint on localhost needs no key.
Named endpoints: `--provider`
An endpoint used more than once can be written down instead of retyped.bbm_providers.json in the working directory is read first, then~/.bbm/providers.json; a project entry wins on a shared name.
{
"nvidia": {
"api_style": "openai",
"base_url": "https://integrate.api.nvidia.com/v1",
"default_models": ["moonshotai/kimi-k2-thinking"],
"env_key": "NVIDIA_API_KEY"
}
}bbook_maker --book_name book.epub --provider nvidia --language jaapi_style is any --api_format that names an endpoint — openai,anthropic, gemini, qwen, groq, xai, litellm — plus claude as
the older spelling of anthropic. Any other OpenAI-compatible host isopenai with its address in base_url. The shippedbbm_providers.example.json has an entry for each. Explicit flags still win over the entry, and the entry's env_key travels
exactly as far as the entry's address does. Asking for another wire format
at the entry's own gateway moves nothing, so the key is still read. An--api_base pointing somewhere else does move the run, and so does an--api_format override on an entry with no base_url, since there the
format is what supplies the address — the key is not read for another
vendor's host, and the run says which two addresses it is comparing. Pass--key when you meant to reuse it. default_models supplies --model when it
holds one id and --model_list when it holds several, and env_key names
the variable to read the key from; no secret goes in the file. Anything
passed explicitly wins, so --provider nvidia --model keeps that
model. An unknown name is an error naming both files.
OrcaRouter
bbook_maker --book_name book.epub --model orcarouter --language ja--model orcarouter sends the run to the OrcaRouter gateway and asks for
its smart-routing model, orcarouter/auto. It needs no --api_base, and
one you pass yourself wins. The key is read from BBM_ORCAROUTER_API_KEY
before the usual fallbacks. It is a supported route, not a legacy alias, so
nothing is rewritten. To pin one model at the gateway, name the endpoint
like any other: --api_base https://api.orcarouter.ai/v1 --model .
OpenAI-compatible endpoints
Everything below is the same route with a different --api_base. Structured
output, --use_context, parallel workers, async and the Batch API are
available on all of them to the extent the endpoint itself supports them —
support is probed at runtime rather than assumed from the model name.
--use_context session also needs the endpoint to charge less for cached
prompt tokens. Watch the cached= count on the progress bar: still zero
after a dozen requests means the endpoint is not caching, and window mode
is cheaper.
| Vendor | --api_base |
|---|---|
| OpenAI | https://api.openai.com/v1 (default) |
| Groq | https://api.groq.com/openai/v1, or --api_format groq |
| xAI | https://api.x.ai/v1, or --api_format xai |
| Gemini (compatibility mode) | https://generativelanguage.googleapis.com/v1beta/openai/ |
| Alibaba Qwen (DashScope) | https://dashscope.aliyuncs.com/compatible-mode/v1 |
| SiliconFlow | https://api.siliconflow.cn/v1 |
| OpenRouter | https://openrouter.ai/api/v1 |
| Azure OpenAI | the deployment's OpenAI-compatible URL; --model names the deployment |
| Ollama | http://localhost:11434/v1 |
| vLLM / LM Studio / llama.cpp | whatever host they serve |
bbook_maker --book_name book.epub \
--api_base https://api.groq.com/openai/v1 --key gsk-... \
--model llama-3.3-70b-versatile--extra_body passes vendor-specific request fields on this route.
Anthropic
bbook_maker --book_name book.epub \
--api_base https://api.anthropic.com --key sk-ant-... \
--model claude-sonnet-4-6 --language zh-hansAny model id the endpoint serves is accepted. Claude uses one model per run,
so extra --model_list entries are announced and ignored rather than
silently dropped. A gateway that serves the anthropic shape from an
OpenAI-style /v1 base is handled: the trailing /v1 is trimmed, because
the SDK appends its own.
Classification through this format uses the prompt rung — the endpoint is not
asked to compile a schema.
Translation services
These speak their own protocols and take no model, so naming one is an error
rather than a silent no-op.
--api_format |
Credential |
|---|---|
google |
none |
deeplfree |
none |
tencent |
none |
customapi |
none; the endpoint URL goes in --api_base |
caiyun |
required |
deepl |
required (RapidAPI DeepL Translator) |
They translate text and nothing else: no context window and no structured
output. They cannot classify an EPUB for plan mode themselves; with--classify-model naming a model that can, an EPUB gets plan mode on these
engines too. --source_lang reaches customapi
(it goes into the request body); the other engines detect the source
themselves. (On the LLM routes the flag reaches the prompt — see
Languages below.)
Languages
--language LANGUAGE sets the target language and defaults to zh-hans.
It takes a tag (zh-hant), a name ("Traditional Chinese"), or both at
once — --language "zh-hant:Traditional Chinese" — for a language the
built-in tables miss. The tag half is mechanical: stamped on the inserted
markup and dc:language, recorded in translation metadata, and it names the
structured-output field. The name half is what the model is asked for in
the prompt. A bare tag or name resolves through the tables as before; a
value matching no tag still runs, prints one Note: at startup, and
stamps nothing. The full table: Language tags and names.
bbook_maker --help
bbook_maker --book_name book.epub --api_format google --language ja--source_lang states the source language rather than detecting it; stated,
it reaches every LLM route's prompt and the request body onqwen/customapi. The default auto states nothing. Not every endpoint
supports every language the parser accepts.
En Prompt (`docs/en/prompt.md`)
Tweak the prompt
To tweak the prompt, use the --prompt parameter. The placeholders the user template may use are {text} (required), {language} and {crlf} (a newline, for the shapes that cannot carry one). Anything else in braces is refused before the run starts; write {{ and }} for a literal brace. It supports a few ways to configure the prompt:
If you don't need to set the
systemrole content, you can simply set it up like this:--prompt "Translate {text} to {language}."or--prompt prompt_template_sample.txttext# prompt_template_sample.txt Translate the given text to {language}. Be faithful or accurate in translation. Make the translation readable or intelligible. Be elegant or natural in translation. If the text cannot be translated, return the original text as is. Do not translate person's name. Do not add any additional text in the translation. The text to be translated is: {text}If you need to set the
systemrole content, you can use the following format:--prompt '{"user":"Translate {text} to {language}", "system": "You are a professional translator."}'or--prompt prompt_template.jsontext# prompt_template.json { "system": "You are a professional book translator. Translate the given paragraphs into {language} and be accurate, faithful, and fluent. Return translated {language} text only.", "user": "Translate the following into {language}. Text:{crlf}{crlf}{text}", "style": "" }A third key,
style, is a standing instruction about how to write. It is said once where a window starts — with the system message on the API routes, with the thread instructions on codex — not repeated on every request. The shippedprompt_template.jsoncarries all three keys withstyleleft empty: write your own voice in, or leave it blank.--promptreaches every LLM route, srt books included: there its sections sit on top of the subtitle loader's own prompt, section by section, and replacing theusertemplate means saying yourself that the block number and timeline must come back unchanged.
You can also set the user and system role prompt by setting environment variables: BBM_CHATGPTAPI_USER_MSG_TEMPLATE and BBM_CHATGPTAPI_SYS_MSG (gemini reads BBM_GEMINIAPI_USER_MSG_TEMPLATE and BBM_GEMINIAPI_SYS_MSG). --prompt outranks all of them. OPENAI_API_SYS_MSG is still read as a fallback but is deprecated; --prompt's own system message wins over it.
A
.mdfile is read as the PromptDown block form. The format is theirs; the reader is this repo's own, so no extra package is installed.--prompt prompt_md.prompt.mdtext# Translation Prompt ## System Message You are a professional translator who specializes in accurate translations. ## Conversation **User:** Please translate the following text into {language}: {text}Three sections are read:
## System Message(## Developer Messageis
accepted as another name for it), an optional## Style, and## Conversation, whose first**User:**turn is theusertemplate. A
conversation written as a| Role | Content |table is refused — the
template's newlines cannot survive a table cell.
Examples
python3 make_book.py --book_name test_books/animal_farm.epub --prompt prompt_template_sample.txt
# or
python3 make_book.py --book_name test_books/animal_farm.epub --prompt prompt_template.json
# or
python3 make_book.py --book_name test_books/animal_farm.epub --prompt "Please translate \`{text}\` to {language}"En Providers (`docs/en/providers.md`)
Provider file and extra models
A run translates through one endpoint. Two kinds of step can use a second one:
- Classification. Plan mode asks a model which kinds of block in an EPUB are worth translating. By default that is the translating model. You can name another with
--classify-model. - Steps that look at a page image. Today there is one: on the PDF route, a vision model can correct the roles docling gives the regions of a page. It runs only when you name a model with
--img-model.
You can name both on the command line or in a provider entry. This page covers the provider file, the two extra models, and which key goes where.
The provider file
--provider NAME takes the route from a JSON file instead of flags. The run reads three files. A later one wins on a shared name:
bbm_providers.example.json, shipped next tomake_book.py. The run uses it only for a name that is in neither of the other two files, and it says so every time, with the address and the key variable it used. ItsFILL-MEentries are templates and are never used.~/.bbm/providers.json.bbm_providers.jsonin the directory you run from.
{
"providers": {
"siliconflow": {
"api_style": "openai",
"base_url": "https://api.siliconflow.cn/v1",
"default_models": ["Qwen/Qwen2.5-72B-Instruct"],
"env_key": "BBM_SILICONFLOW_API_KEY"
}
}
}bbook_maker \
--book_name my_book.epub \
--provider siliconflow \
--use_context sessionFlags you pass yourself win over the entry.
Fields
| field | required | meaning |
|---|---|---|
api_style |
yes | openai, anthropic, gemini, qwen, groq, xai or litellm |
base_url |
no | the address; defaults to the style's own |
default_models |
no | models to use; required if you do not pass --model |
env_key |
no | the environment variable holding the key; required if you do not pass --key |
prices |
no | per million tokens, per model: {"<model>": {"input": …, "output": …, "cached_input": …}}. With a price for every model in the run, the progress bar shows money spent (spent=$0.012) instead of token counts |
currency |
no | default USD; EUR, GBP, CNY, JPY print with their symbol |
img_model |
no | the vision model for page-image steps. Stands in for --img-model |
img_base_url |
no | where img_model is served, when it is not this entry's own address. OpenAI-compatible only |
img_env_key |
no | the variable holding the key for img_model's address |
classify_model |
no | the model for classification. Stands in for --classify-model |
classify_base_url |
no | where classify_model is served, when it is not this entry's own address. OpenAI-compatible, or a Jev-compatible classifier's URL |
classify_env_key |
no | the variable holding the key for classify_model's address |
The spent amount is an estimate from the usage each request reports; the vendor's bill is the number that counts.
The shipped `openai` entry turns the image step on
The example file's openai entry sets "img_model": "gpt-6-luna". So --provider openai on a PDF with --to-epub runs the region-role pass on every page by default. That also holds when you have no bbm_providers.json, because the run then falls back to the example. The pass costs about 3,000 prompt tokens per page: 43,443 prompt and 4,319 completion tokens for 12 pages in the study behind it (Region roles with an image model). To run without it, pass --img-model none, or copy the file and delete the line.
The openai-jev entry is the same openai entry plus "classify_model": "jev" with JEV_API_KEY, so --provider openai-jev translates with gpt-6-luna and classifies an EPUB's plan with Jev. The jev entry is Jev on its own: it classifies and never translates, so --provider jev is refused with a hint to use --classify-model. No other entry names an image model or a classify model.
The two extra models
Where each model comes from
| step | first | then | last |
|---|---|---|---|
image (--img-model) |
the flag | the entry's img_model |
off |
classification (--classify-model) |
the flag | the entry's classify_model |
the run's own model |
The image step never falls back to the translating model. It runs only when a model is named for it, by the flag or by the entry, so no default needs a model that reads images. --img-model none turns an entry's image model off for one run.
--plan-classify-model is the old name of --classify-model. It still works and is not in --help. If you type both, --classify-model wins.
Where each model is asked
- Without a base URL, the model is asked at the run's own endpoint, with the run's format and key.
- An image model needs that endpoint to be OpenAI-shaped. On any other format the run stops before it starts:
--img-model needs an OpenAI-compatible endpoint; … resolves to the … format. - A classify model works on any LLM route that can hold a conversation, the anthropic and codex routes included.
- On a translation-service run there is no model to share an endpoint with. A classify model named without a base is then asked at the host its id implies:
--classify-model gpt-6-lunagoes to OpenAI.
- An image model needs that endpoint to be OpenAI-shaped. On any other format the run stops before it starts:
- With
--img-base-urlor--classify-base-url, the model is asked there. The address must speak the OpenAI shape, or, for a classifier, the Jev protocol; anything else is refused before anything is paid for. - A base URL without its model stops the run:
--img-base-url names where --img-model is served, and no --img-model was given.(and the same for--classify-base-url).
A named classify model always gets its own client, even on the run's address, so its tokens are counted apart from the translation's.
Before the image step runs, the run checks once that the model can read a picture. If it cannot, the run says … did not read the probe image (…); image steps are skipped this run. and goes on without the step.
Which key goes where
A key is bound to an address. It is never sent to a host it was not meant for. For the image model and the classify model, the key is found in this order:
--img-keyor--classify-key.- The run's key, but only when the model is asked at the run's own address. The run's address is the one after any
--api_base. - The entry's
img_env_keyorclassify_env_key, but only at the address that entry names for the model: itsimg_base_urlorclassify_base_url, else the entry'sbase_url. An--api_basethat moves the run elsewhere does not carry the entry's key with it. - The variable the address's format reads, as for the run (see Keys).
When none of these holds a key, the run stops and names the flag to pass.
--extra_body and --extra_headers reach the image or classify model only when it is asked at the run's own address. Headers often carry a gateway's credential, so they never travel to another host.
Examples
Translate on a local model, and let a hosted model classify the plan and read page images:
{
"providers": {
"local-with-helpers": {
"api_style": "openai",
"base_url": "http://localhost:11434/v1",
"default_models": ["qwen3:8b"],
"classify_model": "gpt-6-luna",
"classify_base_url": "https://api.openai.com/v1",
"classify_env_key": "OPENAI_API_KEY",
"img_model": "gpt-6-luna",
"img_base_url": "https://api.openai.com/v1",
"img_env_key": "OPENAI_API_KEY"
}
}
}The same with flags:
bbook_maker \
--book_name my_book.epub \
--api_base http://localhost:11434/v1 \
--model qwen3:8b \
--classify-model gpt-6-luna \
--classify-base-url https://api.openai.com/v1 \
--classify-key "$OPENAI_API_KEY" \
--use_context sessionA local server needs no key, so the run's key is empty here; the classifier's key must be named.
Jev and Jev-compatible classifiers
Jev is TypeSafe's classifier: a model built to answer typed questions, not to write. Plan mode's question for each kind of block, translate it or keep it, is that kind of question, and Jev answers a page of them in one cheap round trip. It translates nothing, so it can only be the classify model. A gateway in front of TypeSafe (Vercel's, or Cloudflare AI Gateway with TypeSafe as a custom provider) speaks the same protocol.
The commands
| classifier | flags | key |
|---|---|---|
| TypeSafe's Jev | --classify-model jev |
JEV_API_KEY or TYPESAFE_API_KEY, sent only to api.typesafe.ai |
| Jev through a gateway | --classify-model typesafe-ai/jev --classify-base-url https://ai-gateway.vercel.sh/typesafe --classify-key "$GATEWAY_KEY" |
named with --classify-key |
| Jev through Cloudflare AI Gateway | --classify-model jev --classify-base-url https://gateway.ai.cloudflare.com/v1/$ACCOUNT_ID/$GATEWAY_ID/custom-typesafe --classify-key "$JEV_API_KEY" |
named with --classify-key; CF_AIG_TOKEN for an authenticated gateway, which may store the key instead |
For example, translating with gpt-6-luna and classifying with Jev:
bbook_maker \
--book_name my_book.epub \
--model gpt-6-luna \
--classify-model jevThe rules
- A key is read from the environment only for its own host.
JEV_API_KEYandTYPESAFE_API_KEYgo only to a typesafe.ai address. Anywhere else, a gateway included, name the key with--classify-keyor the entry'sclassify_env_key. - A Cloudflare gateway's token is read only for Cloudflare.
CF_AIG_TOKENis sent ascf-aig-authorizationonly to gateway.ai.cloudflare.com. With the token set and no key named, no key is sent: the gateway supplies the one it stores. On the gateway, TypeSafe is a custom provider with base URLhttps://api.typesafe.ai. - A base URL that already ends in
/systemoneis used as it is. Any other Jev base gets/systemoneadded after a/v1. jevalone asks for TypeSafe's current model,jev-latest.
The gate: a doubtful skip is translated
Jev returns a probability with each answer. A skip below the gate is recorded as translate, so no content is lost to a skip Jev was unsure of. A translate is taken at any probability. The gate is 0.95 on the probability of the chosen answer, measured over 662 plan signatures from 45 EPUBs against gpt-5.6-luna. At that value about nine of ten of Jev's skips become translate, and what Jev still skips is apparatus: copyright lines, line numbers, note marks, index locators. So on that corpus Jev saves little over translating everything; see Jev as the plan classifier. The plan file shows each fallback on its row: unnamed (jev verdict skip at confidence 0.61, below the gate: translate). --classify-min-confidence P (0 to 1) moves the gate for a run; BBM_JEV_MIN_CONFIDENCE does the same without the flag, and the flag wins. A lower value keeps more of Jev's skips: on the measured corpus those below 0.9 agreed with the reference 7–55% of the time, so the risk is yours.
In a provider entry
If you want OpenAI to translate and Jev to classify, the shipped openai-jev entry already does it: --provider openai-jev. For any other pairing, write your own entry.
classify_model, classify_base_url and classify_env_key name a Jev classifier the same way the flags do:
{
"providers": {
"openai-with-jev": {
"api_style": "openai",
"default_models": ["gpt-6-luna"],
"env_key": "OPENAI_API_KEY",
"classify_model": "typesafe-ai/jev",
"classify_base_url": "https://ai-gateway.vercel.sh/typesafe",
"classify_env_key": "JEV_API_KEY"
}
}
}An entry that names JEV_API_KEY for its own gateway address has named the key for that address, and it is honored. Without such an entry, the Jev variables are never sent to a gateway.
Each Jev request sends the page's candidate lines once, with one short question per signature. See Jev as the plan classifier.
What the run prints
A --plan-dry-run shows where each model would be asked, without a key:
Classifier: gpt-6-luna at the openai endpoint's default host (cli)
Image model: offThe part in brackets is where the choice came from: cli, provider or run. Under --plan-classify all or agent the line reads Classifier: none (--plan-classify agent asks nothing).
At the end of the run, a model on a client of its own prints its own usage line under the translation's:
Classifier (gpt-6-luna at the endpoint's default host): tokens: in 7.6k, out 1.8k, cached 0 (3 requests)The image model's line reads Image model (<model> at <address>): … and is printed after the extraction.
These warnings mean a flag does nothing this run:
--img-model, --img-base-url and --img-key choose the vision model for the steps that look at a page image, and only the PDF route (--to-epub on a PDF) has one; …You named an image model on a book that is not a PDF on the--to-epubroute.--classify-model names a classifier, and --plan-classify all translates the whole partition without classifying anything; it is ignored this run.The same for--plan-classify agent: agent mode leaves every row to your agent and asks no model.Nothing on this route classifies yet, so --classify-model is ignored on a … book.Only an EPUB has a classification step today.
En Session Grouping Eval (`docs/en/session_grouping_eval.md`)
This report was split into Units and tokens per request and Session compact budget.
Zh Agent Skill (`docs/zh/agent-skill.md`)
用智能体翻译
你有一本书,想把它翻译好。最省事的办法是交给一个带有本仓库 bbm-plan 技能的编程智能体。智能体会问清你手头有什么、选好参数,给你看命令和费用,安静地运行工具,最后交回双语书。你不需要学任何一个参数。
你需要准备
**本仓库的源码。**技能在仓库根目录运行
make_book.py,所以只pip install bbook_maker是不够的:bashgit clone https://github.com/yihong0618/bilingual_book_maker.git cd bilingual_book_maker pip install -r requirements.txt # pip install ".[pdf]" if you have PDFs你要用的模型的凭据:放在
.env或环境变量里的 API key、跑在你自己机器上的模型,或者经由 codex 路线使用的 ChatGPT 套餐。智能体会问你用哪一种;每种各需要什么,见用大模型翻译。一个能读取技能的编程智能体:Codex、Claude Code,或者
skillsCLI 能安装到的任何智能体。
Codex
Codex 会从仓库读取 .agents/skills/,所以什么都不用装。在仓库目录里启动 codex,描述任务,或者直接点名技能:
$bbm-plan translate ~/Books/my_book.epub to JapaneseClaude Code 和其他智能体
先安装一次技能。这条命令会找出你机器上的智能体,并问你装到哪一个:
npx skills add yihong0618/bilingual_book_maker --skill bbm-plan然后在仓库目录里启动智能体,描述任务。在 Claude Code 里,技能也可以直接用名字调用:
/bbm-plan translate ~/Books/my_book.epub to Japanese智能体会做什么
- **问清你手头有什么。**路线(key、本地模型、ChatGPT 套餐)、目标语言、你常用的提示词文件(如果有),以及你想改动的默认设置。默认输出双语:原文每段后面紧跟译文。如果只想要译文,就说“只要中文”。
- 探测端点:在花任何钱之前,先发一个花费不到一美分的请求。
- 说明命令:每个参数一行并附理由,告诉你会看到的检查点,以及花的是谁的额度。然后等你批准。
- 按文件类型走对应流程。
- 安静地运行,日志写到
run.log;崩溃后或开新会话时,从停下的地方继续。它需要的一切都在磁盘上,没有任何东西只存在于对话中。 - 交付:把
_bilingual文件放在你的书旁边。
阅读技能内容
技能由路由文件 .agents/skills/bbm-plan/SKILL.md 加上每个流程一份参考文件组成(路线设置、EPUB 计划模式、PDF 路线、纯文本格式、提示词文件、提供方),只在运行到那一步时才加载。可以在 GitHub 上阅读:.agents/skills/bbm-plan。凡是它没有写明的内容,它指向的都是本 wiki 里的同一批页面。
Zh Book Source (`docs/zh/book_source.md`)
各类文件对应的页面
文件的扩展名决定它怎么被读取。找到你的那一行:
| 如果你的文件是…… | 先运行 | 再阅读 |
|---|---|---|
| EPUB | bbook_maker --book_name book.epub --use_context session |
EPUB、EPUB 推荐设置 |
python make_book.py --book_name book.pdf --to-epub --pages 1-2 --test |
PDF 转双语 EPUB、PDF 推荐设置 | |
| 纯文本文件 | bbook_maker --book_name book.txt --batch_size 20 |
TXT |
| 字幕 | bbook_maker --book_name talk.srt --accumulated_num 400 |
SRT |
| Markdown 文档 | bbook_maker --book_name doc.md --prompt prompt_md.json --use_context session |
Markdown |
PromptDown 格式的 .md 文件是提示词,不是书:它交给 --prompt,而 Markdown 格式的书交给 --book_name。
不用计划翻译 EPUB
如果只想翻译指定的标签,就关掉计划并列出这些标签:
bbook_maker --book_name test_books/animal_farm.epub --plan-classify none --translate-tags div,p如果一本书有不在任何标签里的文字,--allow_navigable_strings 会把它们也加进来。计划本来就同时覆盖这两种情况,所以这些参数是给想精确控制的书用的。无论哪种方式,结构规范的 EPUB 效果都更好。
Zh Cmd (`docs/zh/cmd.md`)
命令行参数
运行时以 book_maker/cli.py 和 python3 make_book.py --help 为准。
下面的清单已与 argparse 注册的每个长参数逐一核对过;清单之后的各节为部分工作流提供补充说明。
完整参数清单
输入、范围与输出
| 参数 | 用途 |
|---|---|
--book_name PATH |
输入的 EPUB、TXT、Markdown、SRT 或 PDF 路径(必填)。 |
--language LANGUAGE |
目标语言:标签(zh-hant)、语言名("Traditional Chinese"),或在内置表里查不到该语言时用 TAG:NAME 同时指定两者——标签用于输出标记和字段名,语言名用于提示词。默认 zh-hans;列表见 docs/en/languages.md。 |
--source_lang LANGUAGE |
源语言。明确指定时,它会写进每条大模型路线的提示词,在 qwen/customapi 上还会写进请求体;默认 auto(什么都不声明)。 |
--single_translate |
只输出译文,而不是双语文本。 |
--no_disclosure |
不把 epub 标记为 AI 翻译:去掉书籍简介下方那一行署名(“Translated by <model>, <year>.”)。同时关闭 --translation-metadata。 |
--translation-metadata |
在书里写入一个小小的 bbm_translation_metadata.json:模型、日期,以及 --glossary 文件的 sha256(文件文本一并嵌入;运行自己学到的术语不会写入)。别的都没有——没有命令行、端点、语言或路径,也没有包的元数据:记录的是可能影响译文的东西,而不是谁运行了它。格式转换会保留清单中的文件,所以这份记录能经受住转换。计划模式和会话模式的运行会自行写入它;这个参数让普通的标签模式运行也写入。--no_disclosure 同样会关闭它。 |
--translate-tags TAGS |
逗号分隔的 EPUB 标签;默认 p,计划模式下忽略。 |
--exclude-translate-tags TAGS |
要排除的 EPUB 祖先标签;默认 sup,code;传 "" 清空。 |
--allow_navigable_strings |
把原本不在标签里的 EPUB 字符串也包括进来;计划模式下多余。 |
--only_filelist FILES |
只包括这些逗号分隔的 EPUB 内部文件。 |
--exclude_filelist FILES |
排除这些逗号分隔的 EPUB 内部文件。 |
--translation_style CSS |
应用于 EPUB 译文条目的 CSS。 |
--translation_color COLOR |
只设颜色的简写;--translation_style 优先。 |
--pdf_layout MODE |
额外生成的 PDF 输出:none、top-bottom、side-by-side 或 all。 |
--to-epub |
仅限 PDF:用 docling 读取 PDF,翻译得到的 Markdown,在其旁边写出 <name>_bilingual.epub;插图保留为图片;包留在 <name>_book/ 里,便于编辑和续跑。需要 pdf 扩展和 PATH 上的 Pandoc 3.1.12+;不需要 Java。见 installation-pdf.md。 |
--pdf-ocr |
仅限 PDF,配合 --to-epub:读取没有文字层的页面,不加这个参数时这些页面会被拒绝。默认关闭;OCR 要多花好几倍的时间,而且对原生数字 PDF 没有任何改变。无论开不开,版面、标题和表格都会被检测。 |
--ocr-replace-layer |
仅限 PDF,配合 --to-epub --pdf-ocr:对每一页做 OCR,并替换 PDF 自带的文字层,而不是保留它。默认关闭;测得它比一个完好的文字层差,是给有错的文字层用的。读出为空的页面保持为空,并会被点名。 |
--no-formula-images |
仅限 PDF,配合 --to-epub:让行间公式保留为 `` 占位符,而不是把每个公式从页面上裁剪成图片。解析器能找到公式,但从不读取它们,所以这些图片是数学内容能进入书中的唯一途径;它们不花模型费用,也不花可测量的时间。段落里的行内数学不算公式区域,无论如何都不在覆盖范围内。 |
--pdf-image-dpi N |
PDF,--to-epub:插图清晰度,以 PDF 自身页面尺寸下的 DPI 计;默认 200,标注很小时用 300;换一个值重新运行只会重绘插图 |
--img-model MODEL |
仅限 PDF,配合 --to-epub:一个视觉模型,在导出前根据页面图像纠正版面检测器给出的区域角色(正文、标题、书名、图题、脚注、代码)。除非在这里或在提供方条目的 img_model 中指定,否则关闭;none 会关掉它。绝不会回退为本次运行自己的模型。每页约 3k 提示词 token。 |
--img-base-url URL |
--img-model 的端点,当它不是本次运行的端点时使用(仅限 OpenAI 兼容)。 |
--img-key KEY |
--img-base-url 使用的 key。默认:端点是本次运行自己的端点时,用本次运行的 key;否则,端点是提供方条目自己的端点时,用该条目的 img_env_key;否则,用该端点格式从环境中读取的 key。key 绝不会发往不是为它指定的地址。 |
--device DEVICE |
仅限 PDF,配合 --to-epub:提取模型在哪里运行——auto(默认;检测 CUDA 或 MPS,没有则回退到 CPU)、cpu、cuda、mps、xpu。CPU 完全受支持,输出相同,只是更慢。 |
--ocr-lang LANGS |
仅限 PDF,配合 --to-epub --pdf-ocr:OCR 引擎在没有文字层的页面上(加 --ocr-replace-layer 时是每一页)识别的语言,逗号分隔,使用引擎自己的代码(rapidocr:ch、en、latin;easyocr:ch_sim、ja、ko;ocrmac:zh-Hans、ja-JP),或加 iso: 前缀的通用 BCP-47 标签(iso:zh-Hans、iso:zh-Hant、iso:ja、iso:ko),docling 2.129 会把它映射到实际运行的引擎上。rapidocr 每次运行只识别一种语言,取第一个。引擎是 docling 在当前安装中选中的那个(装了 PDF 扩展时,Mac 上是 ocrmac,默认识别英语、西班牙语、法语和德语;其他系统上是 rapidocr,默认识别中文和英文),运行会打印它用的引擎和语言。其他文字的扫描件需要这个参数;运行遇到没加它的扫描页时会提示。未知的代码在读取任何页面之前就会被拒绝。每个引擎的代码,以及该选哪个引擎:选择 OCR 引擎。 |
--ocr-engine ENGINE |
仅限 PDF,配合 --to-epub --pdf-ocr:用于没有文字层的页面(加 --ocr-replace-layer 时是每一页)的 OCR 引擎。auto(默认)按 ocrmac、rapidocr、easyocr 的顺序取第一个已安装的;pdf 扩展会安装带 onnxruntime 的 rapidocr(模型已包含),在 macOS 上还会安装 ocrmac(Apple 的 Vision 框架);两者都不下载任何东西,所以 auto 在 Mac 上用 ocrmac 识别,在其他系统上用 rapidocr;easyocr 首次使用时下载模型(pip install easyocr);tesseract 使用 PATH 上的 tesseract 程序及其语言数据。未安装的引擎,或在 macOS 之外指定 ocrmac,都会在读取任何页面之前被拒绝。语言代码因引擎而异(--ocr-lang);运行会说明它用的引擎。该选哪个,有测量数据:选择 OCR 引擎。 |
--pages PAGES |
仅限 PDF,配合 --to-epub:要读取的页,从 1 开始编号(12-30、1,3,5-7);其余页不处理。所选页码会写进包名和书名(<name>_pages-12-30_…),所以翻译一章永远不会覆盖整本书。如果所选范围从某节中间开始,source.md 里会在它的第一段正文上方加一个 Page N 标题。 |
--retranslate OUT FILE START END |
在已有的输出中重新翻译一段 EPUB 范围。仅限 EPUB——其他格式会拒绝。 |
EPUB 计划模式
| 参数 | 用途 |
|---|---|
--plan-dry-run |
构建并打印 EPUB 计划,写出所有 action 仍为 null 的 _plan.json,然后退出。不需要凭据。 |
--plan-classify {auto,none,all,model,agent} |
不用计划、翻译整个分区、由模型筛选,或由编程智能体筛选。默认 auto:在任何能作答的 epub 端点上由模型筛选——经验证支持严格 JSON schema 时通过结构化输出,其他情况(包括 codex)通过普通对话(精确回答 skip/translate,其他回答一律翻译);只有在根本无法对话的地方才用标签模式。 |
--classify-model MODEL |
每个分类步骤使用的模型(旧名 --plan-classify-model),或一个 Jev 兼容的分类器(默认是 TypeSafe 的 Jev;网关需给出其 URL);在 epub 上意味着 model 模式。其端点经验证支持 JSON schema 时通过 schema 提问,否则通过普通对话。默认:提供方条目的 classify_model,其次是本次运行的模型。 |
--classify-base-url URL |
--classify-model 的端点,当它不是本次运行的端点时使用:一个 OpenAI 兼容端点,或一个 Jev 兼容的分类器的 URL(以 /systemone 结尾的路径原样使用)。 |
--classify-key KEY |
--classify-base-url 使用的 key;默认规则与 --img-key 相同。JEV_API_KEY/TYPESAFE_API_KEY 只在 typesafe.ai 地址上被自动读取,Cloudflare AI Gateway 的 CF_AIG_TOKEN 只在 gateway.ai.cloudflare.com 上读取;提供方条目仍然可以把某个变量绑定到它自己的地址。 |
--classify-min-confidence P |
Jev 兼容的分类器的置信度闸门,0 到 1:概率低于它的 skip 改为 translate;translate 从不受闸门限制。默认 0.95,经过测量;低于 0.5 时闸门不起作用。BBM_JEV_MIN_CONFIDENCE 不用参数也能设置它。 |
--plan-min-coverage FRACTION |
计划选中的文字比例低于此值时失败;默认 0.5,必须在 0 到 1 之间(0 关闭这道防线,高于 0.9 的值多半会中止——两种情况都会警告)。 |
--poetry-group-size N |
已弃用——现在通用分组和会话交接会让短行和相邻内容放在一起,单元数上限是 --max-batch-units。仍然可用(默认 8,最小 1),但会警告。 |
翻译与执行
| 参数 | 用途 |
|---|---|
--test |
只翻译一小段预览样本。 |
--test_num N |
测试的单元数;默认 10。 |
--resume |
从加载器保存的检查点继续。EPUB 检查点会记录本次运行的语言、提示词和模型;不一致时拒绝续跑(旧检查点只警告一次,然后继续)。与 --parallel-workers 以及大于 1 的 --accumulated_num 同用会被拒绝,因为那两种情况从不写检查点。 |
--prompt VALUE_OR_FILE |
提示词配置:user(必须包含 {text})、system 和 style。style 是常驻指令,只在窗口开始时说一次(API 路线上放在 system 消息里,codex 上放在线程指令里),从不在每个请求里重复;用户写的 style 会替换交接报告里模型自己的文风说明。某个部分在某条路线上没有原生位置时(codex 格式上的 system),会附加到 user 消息里,而不是被丢弃;带 --prompt 的运行会打印它采用了哪些部分、放在哪里。示例:prompt_template.json(随附的 style 为空)。 |
--temperature FLOAT |
采样温度,仅用于接受该参数的格式;默认 1.0。anthropic 格式总会发送它。openai 格式在它等于 API 默认值时,以及模型拒绝显式设置时(gpt-5.x、o 系列),不发送它,由 API 默认值生效。codex 格式没有这项设置,会忽略它。 |
--use_context [window|session] |
把前面的段落作为上下文发送。不带值或 window:重发最近几对原文与译文(一贯的行为)。session:一份只追加的历史,按端点的提示缓存价重读。 |
--context_paragraph_limit N |
仅窗口模式:上下文历史的上限。解析器默认值 0 表示采用翻译器的默认值(ChatGPT 为 3 段),而不是没有历史。 |
--context-compact-at N |
滚动历史的估算 token 预算。会话模式下,历史达到这个大小时会被压缩成交接报告;最小 1500。不设时,每个会话运行——无论是否分组,包括 codex 格式——都在 8192 处压缩,并在开始时打印出来。这个默认值是为连贯性(窗口接缝最少)而选的,不是为价格:会话模式下更低的值更便宜,因为每个请求都会重读携带的历史,所以如果你更在意会话成本而不是接缝,就调低它;如果模型的输入上限更小,就设成那个上限。超过约 16000 后成本会急剧上升。测量数据见会话压缩预算。它也限定计划分类器在那些通过普通对话分类的端点上的对话长度(在那里重新开始,不写交接报告),无论有没有 --use_context。显式给出的值总是优先。 |
--no-context-compact |
仅会话模式:跳过交接报告。窗口仍在达到预算时滚动,但下一个窗口从空白开始。 |
--glossary FILE / --terminology FILE |
一个由 term → translation 行组成的文件(每行一条;# 之后是备注或注释),本次运行必须照此翻译。同一个参数的两个名字。只有出现在某个请求里的术语才会随它发送。文件不存在时,运行在解析参数阶段就会中止。由 openai 系和 codex 系路线在 EPUB、Markdown 和 PDF 书上读取;其他路线会警告并忽略它。 |
--glossary-auto on|off |
会话运行是否同时保留它自己的交接报告里确立的译名。除非你要求,否则关闭。它需要一个可供学习的会话(--use_context session,或 codex 格式的那一个线程),以及一个会用名字而不是大段文字作答的模型;off 时压缩那一轮只要求写摘要。学到的术语只留在本次运行和 _handoff.md 里,不会出现在别处。 |
--accumulated_num N |
EPUB 的 token/字符累积量,以及 SRT 字幕块按字符合并的批量(SRT 上限为 512)。在 EPUB 计划模式下,它是每个请求的 token 预算:任意长度的相邻单元在 N 个 token 之内合成一次请求(每个请求最多 --max-batch-units 个单元;端点经验证支持 JSON 模式但不支持严格 schema 时为其一半)。不设时,每次计划运行都会根据本次运行自己的提示词开销推算默认值——用自带提示词时为 1200,自定义 --prompt 较长时最多 1600——在没有严格 schema 结论的端点上,每个请求的预算减半,但绝不低于 800 的下限(所以在那里,无论提示词开销多大,每个请求的预算都是 800);会话运行(包括 codex)保持未减半的值。这些是选定的安全余量,低于所有测量中未出现错误的范围;见每次请求的单元数与 token 数。运行会说明这个数字以及路线类别。传 1 关闭合并。最小 1。 |
--max-batch-units N |
仅 EPUB 计划模式:--accumulated_num 的 token 预算最多能放进一个请求的单元数。默认 16,即首次出现内容错误的水平(每个请求 64 个单元)的四分之一,作为安全余量。端点经验证支持 JSON 模式但不支持严格 schema 时,携带其一半(8),回复数错的情况实际就出在这类端点上。如果你信任自己的模型、想减少请求,就调高它;如果运行一直打印错位恢复的提示,就调低它。 |
--batch_size N |
TXT、Markdown 和 PDF(纯文本路线)加载器每个请求发送的行数或段落数。默认 10。 |
--block_size N |
把段落合并成用分隔符方式翻译的块。 |
--sentence_mode |
把 EPUB 段落逐句翻译;与计划模式不兼容。 |
--parallel-workers N |
并行处理 EPUB 章节或 Markdown 批次/分节;默认 1。与 --use_context session 同用(只有一份历史)以及在 codex 格式上(只有一个线程)会被拒绝。 |
--batch |
提交一个 ChatGPT Batch API 任务。在 EPUB 上会被拒绝(那里走不到队列路径:运行会按全价实时翻译,并提交一个空任务,而不是写出书),在不支持 Batch API 的路线上也会被拒绝。TXT、SRT 和 Markdown 加载器也没有实现它(运行会实时翻译),所以目前没有任何格式用到它。 |
--batch-use |
使用之前提交的批量任务。在 EPUB 上会被拒绝,与 --batch 相同。 |
--no-thinking |
让模型回答前不要先推理。在 OpenAI 形态的路线上,请求字段根据端点自己的拒绝来协商,并按端点和模型缓存;如果每种写法都被拒绝,运行会警告一次,然后不带该字段继续。在 anthropic 上是 thinking: {"type": "disabled"}。在 codex 上会被拒绝(子进程没有请求体);在自行构建请求的路线上会被警告为不起作用。--extra_body 中设置的字段优先。 |
--extra_body JSON |
附加到每个请求体上的字段,适用于构建请求体的路线(openai、groq、xai、litellm、--model orcarouter、anthropic);其他路线会忽略它并说明。它也会进入能力探测和 JSON 各级请求,所以端点是按本次运行实际发出的请求来评定的。它合并在具名参数之上,所以这里的字段优先于对应的参数。 |
--extra_headers JSON |
附加到每个请求上的 HTTP 头,适用路线同上。它设在客户端上,所以能力探测、模型检查和模型列表都会带上它们。值必须是字符串。 |
--quiet |
不显示 EPUB 进度条和段落回显,报告和错误照常显示。 |
--proxy URL |
为本次运行设置 HTTP/HTTPS 代理环境变量。 |
端点与凭据
路线指的是端点,而不是模型名。
| 参数 | 用途 |
|---|---|
--model MODEL |
模型 id,按端点自己的写法(gpt-5-mini、claude-sonnet-4-6、openai/gpt-5-mini)。openai 格式下默认 gpt-6-luna;anthropic 格式必须指定。旧的别名值会被改写,并附一条说明。 |
--api_base URL |
端点地址。默认是该格式的官方地址。粘贴进来的 …/v1/chat/completions 或末尾的斜杠会被去掉。 |
--key KEY |
API key;用逗号分隔多个 key 可以轮换使用。更推荐用 BBM_API_KEY 或该格式自己的变量。 |
--api_format FORMAT |
端点所用的 API:openai(默认)、anthropic、gemini、qwen、groq、xai、litellm、codex、google、caiyun、deepl、deeplfree、tencent、customapi。根据 --api_base 的主机推断,否则根据含 claude/anthropic 的模型 id 推断——各厂商格式从不推断,所以要写明。 |
--api_format gemini | qwen |
Google 和阿里巴巴各自的协议,各有自己的翻译器:Gemini 原生的约束解码和对话历史,以及 Qwen-MT 的语言对请求。默认分别为 gemini-flash-latest 和 qwen-mt-turbo。 |
--api_format groq | xai | litellm |
Groq、xAI 和 LiteLLM 代理(http://localhost:4000)上的 OpenAI 形态。每种都自带地址,所以格式加一个 key 就是完整的路线。必须写 --model:这些模型目录更新很快,所以不预设任何模型。 |
--model codex |
使用 ChatGPT 套餐的 Codex CLI 侧车,等同于 --api_format codex。它运行 gpt-6-luna;--api_format codex --model 可以指定其他模型(侧车还提供 gpt-5.6-luna、gpt-5.6-sol、gpt-5.6-terra、gpt-5.5、gpt-5.2)。 |
--model orcarouter |
OrcaRouter 网关及其智能路由模型 orcarouter/auto。不需要 --api_base;如果你传了,以你的为准。key 来自 BBM_ORCAROUTER_API_KEY。它不是旧别名:不会改写任何东西。 |
--model_list IDS |
轮换使用的多个模型 id,逗号分隔。单个模型应该写在 --model 里;在两个参数里都写模型会报错。与 --use_context session 同用会被拒绝:轮换会让每个请求都成为全价的缓存未命中,还会在一段对话里混用多个模型。 |
--source_lang LANG |
源语言。明确指定时,它会作为依据写进每条大模型路线的提示词,在 qwen/customapi 上还会写进请求本身;默认 auto。 |
--interval SECONDS |
请求之间的间隔,默认 0.01。只有 gemini 路线用它控制节奏。 |
--provider NAME |
bbm_providers.json(当前目录)或 ~/.bbm/providers.json 中的一个具名端点;同名时项目文件优先,两处都没有的名字会回退到随附的 bbm_providers.example.json,并给出警告,说明它用的地址和 key 变量(不包括其中的 FILL-ME 模板)。条目中的 base_url、api_style(openai、anthropic、gemini、qwen、groq、xai 或 litellm)、default_models 和 env_key 分别代替 --api_base、--api_format、--model/--model_list 和 key;img_model/img_base_url/img_env_key 和 classify_model/classify_base_url/classify_env_key 分别代替 --img-model/--img-base-url/--img-key 和 --classify-model/--classify-base-url/--classify-key 参数(见提供方文件与额外模型)。你自己传入的参数优先。 |
提供 Claude 模型的网关说的是 OpenAI 形态,给了网关的 --api_base 时也按这种形态处理。
只有在 Anthropic 自己的主机上,或者没有 --api_base 而模型 id 含 claude 时,才会推断为 anthropic 格式。
如果向一个不提供 anthropic 形态的网关请求这种形态,它会返回 404,运行随即中止,并指出修正方法是 --api_format openai。
key 的查找顺序:--key(--api_key 是同一个参数),然后是 BBM_API_KEY,
再然后是该格式自己的变量:OPENAI_API_KEY、ANTHROPIC_API_KEY、BBM_GOOGLE_GEMINI_KEY、BBM_QWEN_API_KEY、BBM_GROQ_API_KEY、BBM_XAI_API_KEY、BBM_CAIYUN_API_KEY、BBM_DEEPL_API_KEY(每个之后还有该厂商惯用的变量)。
localhost 上的端点不需要 key。
旧的 --model 预设名、各厂商的 --*_key 参数、--ollama_model 和 --deployment_id 已经不在解析器里了,
但旧命令行仍然能运行:book_maker/legacy_cli.py 会在运行开始前把它们改写成上面的参数,并打印每一处改写。
对照表见从旧参数迁移。
旧的各厂商 key 变量(BBM_GROQ_API_KEY、BBM_GOOGLE_GEMINI_KEY……)在原来使用它们的路线上仍然会被读取。
不要把密钥直接写在共享的命令行上。对智能体和 CI 来说,环境变量更安全。
CLI 自己不会加载 .env 文件:请先导出变量,或者在运行前 source 一个被 git 忽略的本地文件,例如set -a; source .env; set +a; bbook_maker ...。
测试翻译
--test
如果你还没为服务付费,或者只想试一试,可以用这个参数预览结果。注意会有限制,可能需要一些时间。
bbook_maker --book_name test_books/Lex_Fridman_episode_322.srt --key ${openai_key} --model gpt-5-mini --testbbook_maker --book_name test_books/animal_farm.epub --key ${openai_key} --model gpt-5-mini --test --language zh-hans--test_num <TEST_NUM>
用这个参数设置测试时要翻译多少段。默认是 10。
续跑
--resume
中断后,用这个参数手动继续处理。
重新翻译(仅 epub)
--retranslate <file_name_in_epub> <start_str> <end_str>
如果 EPUB 中某个文件翻译得不好,可以用它单独重新翻译其中一部分。
argparse 要求四个值都给出。end_str 传空值时只重新翻译起始标签;file_name_in_epub 传空值时会自动查找文件名。
从 start_str 重新翻译到 end_str 所在的标签:
textbbook_maker --book_name "test_books/animal_farm.epub" --retranslate 'test_books/animal_farm_bilingual.epub' 'index_split_002.html' 'in spite of the present book shortage which' 'This kind of thing is not a good symptom. Obviously'只重新翻译
start_str所在的标签(第四个值为空):textbbook_maker --book_name "test_books/animal_farm.epub" --retranslate 'test_books/animal_farm_bilingual.epub' 'index_split_002.html' 'in spite of the present book shortage which' ''重新翻译
start_str所在的标签,并自动查找文件名(第二个和第四个值为空):textbbook_maker --book_name "test_books/animal_farm.epub" --retranslate 'test_books/animal_farm_bilingual.epub' '' 'in spite of the present book shortage which' ''
警告:
它会从成书中 start_str 所在的标签开始,删到 end_str 所在的下一个标签为止,然后重新翻译。
因此,请确保 end_str 之后的那个标签是译文内容。end_str 为空字符串时,使用 start_str 之后的那个标签。两个字符串之间可以有漏译,但结束边界若不是译文就会出问题。
自定义输出样式(仅 epub)
--translation_style
支持修改 epub 文件的输出样式。
bbook_maker --book_name test_books/animal_farm.epub --translation_style "color: #4a4a4a; font-style: normal; background-color: #f7f7f7; padding: 5px; margin: 10px 0; border-radius: 5px;"代理
--proxy
用这个参数指定访问互联网的代理服务器。传入类似 http://127.0.0.1:7890 的字符串。
API 地址
--api_base <API_BASE_URL>
如果你想更换 api_base,比如使用 Cloudflare Workers,可以用这个参数。
bbook_maker --book_name 'animal_farm.epub' --key sk-XXXXX --model gpt-5-mini --api_base 'https://xxxxx/v1'注意:api 地址应该是 'https://xxxx/v1' 这样的形式,必须加引号。
Microsoft Azure 端点
Azure 没有专门的参数。把 --api_base 指向部署的 OpenAI 兼容 URL,
并在 --model 里写上部署名:
bbook_maker --book_name 'animal_farm.epub' --key XXXXX --api_base 'https://example-endpoint.openai.azure.com/openai/v1' --model 'deployment-name'批量大小(TXT、Markdown、PDF 纯文本路线)
--batch_size
用这个参数指定批量翻译的行数。默认是 10。由 TXT、Markdown 和 PDF(纯文本路线)加载器读取;EPUB 用的是 --accumulated_num。
python3 make_book.py --book_name test_books/the_little_prince.txt --test --batch_size 20累积 token 数
--accumulated_num <ACCUMULATED_NUM>
累积到多少个 token 之后才开始翻译。gpt3.5 把 total_token 限制在 4090。
在 EPUB 计划模式下你很少需要它:运行会自动推算每个请求的预算(用自带提示词时为 1200,在不支持严格 schema 的端点上为 800),并把它打印出来。见上面 --accumulated_num 那一行和每次请求的单元数与 token 数。
例如,如果你用 --accumulated_num 1600,openai 可能会输出 2200 个 token,
system 消息和 user 消息里的其他内容可能还要 200 个 token。1600+2200+200=4000,已经接近上限了。
你得自己选一个合适的值,在发送请求之前无法判断是否已经达到上限。
Zh Disclaimer (`docs/zh/disclaimer.md`)
免责声明
bilingual_book_maker 帮助用户制作图书及其他文档的多语言版本。
请仅翻译您有权翻译的内容。 这包括您持有必要权利的作品、许可协议或权利人授权允许您按预期方式使用的作品、依据适用法律已进入公有领域的作品,以及适用法律另行允许的使用情形。持有或购买一本书或一个副本,本身并不授予翻译权。
分别核实翻译和分发的权利依据。 翻译通常属于改编(演绎作品)。个人使用并不自动免受著作权限制。如果您分享译本,请确认许可协议、授权或适用法律也允许分发输出文件中包含的全部内容,包括原文和图片。同一许可协议或授权可以同时涵盖翻译与分发。使用知识共享(CC)许可的作品时,请核实具体许可及版本,包括其中适用的署名、非商业性使用、相同方式共享和禁止演绎等条件。例如,禁止演绎许可不授权公开分享译本。作品是否进入公有领域因司法辖区而异;现代译本、编辑新增内容、序言及插图可能独立于古旧原文受到保护。
MIT 许可证不为图书内容授予许可。 本项目的 MIT 许可证覆盖软件及随附文档;它不授予输入图书或其他第三方内容的任何权利,也不会自动适用于生成的文件。输出内容的使用仍须遵守适用法律及相关许可或授权。工具不会判定著作权归属,也不会核实您的预期使用方式是否获准。
保留信息不等于核实权利。 生成 EPUB 时,工具会尽量保留源图书的署名与权利元数据。这并不代表翻译或分发已获许可。请检查输出内容,并按适用许可或授权的要求,保留或补充署名、版权声明、许可信息及修改说明。
不提供担保;责任限制。 软件按本项目 MIT 许可证的规定“按原样”提供,不附带任何担保。在适用法律允许的范围内,作者及著作权人按该许可证的规定,不承担因使用软件而产生的责任。
如对项目有问题或建议,请通过 issue 联系我们。
Disclaimer
bilingual_book_maker helps users create multilingual versions of books and other documents.
Use only material you are entitled to translate. This includes works for which you hold the necessary rights, works whose license or the rights holder's permission allows your intended use, works in the public domain under applicable law, and uses otherwise permitted by applicable law. Owning or buying a copy does not itself grant translation rights.
Check translation and distribution rights separately. Translation is generally an adaptation (a derivative work). Personal use is not automatically exempt from copyright restrictions. If you share a translation, ensure that your license, permission, or applicable law also allows distribution of everything included in the output, including any original text and images. The same license or permission may cover both translation and distribution. For Creative Commons works, check the specific license and version, including attribution, NonCommercial, ShareAlike, and NoDerivatives conditions where applicable. For example, NoDerivatives licenses do not authorize public sharing of translations. Public-domain status varies by jurisdiction; modern translations, editorial contributions, forewords, and illustrations may be protected separately from an older original.
The MIT license does not license book content. The project's MIT license covers its software and associated documentation; it grants no rights to input books or other third-party content and does not automatically apply to generated files. Use of the output remains subject to applicable law and any relevant licenses or permissions. The tool does not determine copyright ownership or verify that your intended use is permitted.
Preservation is not rights verification. For EPUB output, the tool carries over source attribution and rights metadata where it can. This does not establish that translation or distribution is permitted. Check the output and retain or supply attribution, copyright notices, license information, and indications of changes as required by the applicable license or permission.
No warranty; limitation of liability. The software is provided “as is,” without warranty, as set out in the project's MIT license. To the extent permitted by applicable law, the authors and copyright holders disclaim liability arising from use of the software, as set out in that license.
For questions or suggestions about the project, please open an issue.
Zh Docker (`docs/zh/docker.md`)
Docker
如果不想自己配置 Python,就用 Docker。每次合并到 main 以及每次发布版本标签时,镜像都会发布到 GitHub Container Registry。容器运行的是 python make_book.py,所以本站介绍的每个参数都原样可用。
镜像与标签
镜像有三个,由同一个 Dockerfile 构建。
| 标签 | 包含什么 | 平台 | 用途 |
|---|---|---|---|
latest(也叫 basic) |
基于 python:3.12-slim 的翻译程序及其 Python 包;amd64 上约 280 MB |
amd64、arm64 | EPUB、TXT、Markdown、SRT,以及走旧纯文本路线的 PDF |
pdf |
latest 加上 Pandoc 3.11 和 PDF 相关的包,PyTorch 用的是 CPU 版本;amd64 上约 2.4 GB |
amd64、arm64 | 在处理器上运行 --to-epub:macOS、arm64 Linux,以及任何没有 NVIDIA 显卡的机器 |
pdf-cuda |
官方 PyTorch CUDA 运行时镜像,加上 Pandoc 3.11、翻译程序和 PDF 相关的包;光是基础镜像就要下载 3.4 GB | amd64 | 在 NVIDIA GPU 上运行 --to-epub,Linux 或 Windows |
<version>、<version>-pdf、<version>-pdf-cuda |
某个发布版本的这三个镜像 | 固定到某个发布版本 | |
sha-<commit>、sha-<commit>-pdf、sha-<commit>-pdf-cuda |
某个提交的这三个镜像 | 固定到某次构建 |
docker pull ghcr.io/yihong0618/bilingual_book_maker:latest挂载
- **把你的书所在文件夹挂载到
/book。**以/book/<file>的形式传入书名。译好的书会写回同一个文件夹。 - 把一个命名卷挂载到
/root/.cache(pdf和pdf-cuda镜像)。docling 的模型在第一次--to-epub运行时下载到这里,约 500 MB。没有这个卷的话,每次运行都会重新下载。
容器以 root 身份运行,所以往挂载的文件夹里写入总是可以的。在 Linux 上,它写出的文件归 root 所有:事后 chown 一下,或者加上 --user $(id -u)。
key 请用环境变量传入,而不要写在命令行上:-e OPENAI_API_KEY。
按系统翻译 EPUB
=== "Linux / macOS"
```bash
docker run --rm \
-v "$PWD":/book \
-e OPENAI_API_KEY \
ghcr.io/yihong0618/bilingual_book_maker:latest \
--book_name /book/my_book.epub \
--language zh-hans \
--use_context session
```=== "Windows PowerShell"
```powershell
docker run --rm `
-v "${PWD}:/book" `
-e OPENAI_API_KEY `
ghcr.io/yihong0618/bilingual_book_maker:latest `
--book_name /book/my_book.epub `
--language zh-hans `
--use_context session
```一个完全不需要 key 的测试,走免费的 Google 路线:
docker run --rm \
-v "$PWD":/book \
ghcr.io/yihong0618/bilingual_book_maker:latest \
--book_name /book/animal_farm.epub \
--api_format google \
--test \
--test_num 1 \
--language zh-hant按系统翻译 PDF
latest 镜像里没有 Pandoc,也没有 PDF 相关的包,所以不能运行 --to-epub。有 NVIDIA 显卡请使用 pdf-cuda 标签,其他情况都用 pdf。
=== "带 NVIDIA 显卡的 Linux"
在宿主机上安装 NVIDIA 驱动和 NVIDIA Container Toolkit,别的都不用装:镜像自带 CUDA 运行时。
```bash
docker run --rm --gpus all \
-v "$PWD":/book \
-v bbm-models:/root/.cache \
-e OPENAI_API_KEY \
ghcr.io/yihong0618/bilingual_book_maker:pdf-cuda \
--book_name /book/paper.pdf \
--to-epub \
--use_context session
```=== "带 NVIDIA 显卡的 Windows"
使用 **WSL2 后端**的 Docker Desktop。在 Windows 本身(而不是 WSL 里)安装支持 WSL 的 NVIDIA 驱动。Windows 容器模式访问不到 GPU。
```powershell
docker run --rm --gpus all `
-v "${PWD}:/book" `
-v bbm-models:/root/.cache `
-e OPENAI_API_KEY `
ghcr.io/yihong0618/bilingual_book_maker:pdf-cuda `
--book_name /book/paper.pdf `
--to-epub `
--use_context session
```=== "仅 CPU"
`pdf` 镜像带的是 CPU 版本的 PyTorch,amd64 和 arm64 都有。`--device cpu` 跳过加速器检测。输出和在 GPU 上完全一样,只是更慢。
```bash
docker run --rm \
-v "$PWD":/book \
-v bbm-models:/root/.cache \
-e OPENAI_API_KEY \
ghcr.io/yihong0618/bilingual_book_maker:pdf \
--book_name /book/paper.pdf \
--to-epub \
--device cpu \
--use_context session
```=== "macOS"
无论传什么参数,容器都只用 CPU:Docker 运行的是一个看不到 Metal 的 Linux 虚拟机。`pdf` 镜像在 Apple 芯片(arm64)上原生运行。在那里,[本机安装](installation-pdf.md)更快(MPS)。
```bash
docker run --rm \
-v "$PWD":/book \
-v bbm-models:/root/.cache \
-e OPENAI_API_KEY \
ghcr.io/yihong0618/bilingual_book_maker:pdf \
--book_name /book/paper.pdf \
--to-epub \
--device cpu \
--use_context session
```arm64 上的 NVIDIA 显卡
pdf-cuda 只为 amd64 构建:它所基于的官方 PyTorch CUDA 镜像只发布了这一种架构。在带显卡的 arm64 Linux 主机(GH200、Jetson)上,pdf 在处理器上运行。
所有镜像里都没有的
Codex 路线(--api_format codex)。它驱动的是你机器上已登录的 codex 程序;容器里既没有这个程序,也没有登录状态。在 Docker 里请使用 API 路线。
自己构建
直接构建得到的是小镜像:
docker build --tag bilingual_book_maker .PDF 镜像是 pdf 和 pdf-cuda 阶段:
docker build --target pdf --tag bilingual_book_maker:pdf .
docker build --platform linux/amd64 --target pdf-cuda --tag bilingual_book_maker:pdf-cuda .Zh Env Settings (`docs/zh/env_settings.md`)
环境变量设置
把凭据写进环境变量,就可以省掉 --key。
API key
BBM_API_KEY 对每种格式都有效,所以通常一个变量就够了:
export BBM_API_KEY=${your_api_key}没有设置它时,每种格式还会去看人们为该厂商习惯导出的那个变量:
--api_format |
备用变量 |
|---|---|
openai |
OPENAI_API_KEY、BBM_OPENAI_API_KEY |
anthropic |
ANTHROPIC_API_KEY、BBM_CLAUDE_API_KEY |
gemini |
BBM_GOOGLE_GEMINI_KEY、GEMINI_API_KEY |
qwen |
BBM_QWEN_API_KEY、DASHSCOPE_API_KEY |
groq |
BBM_GROQ_API_KEY、GROQ_API_KEY |
xai |
BBM_XAI_API_KEY、XAI_API_KEY |
litellm |
BBM_LITELLM_API_KEY、LITELLM_MASTER_KEY |
caiyun |
BBM_CAIYUN_API_KEY |
deepl |
BBM_DEEPL_API_KEY |
google、deeplfree、tencent 和 customapi 不需要 key,localhost 上的端点也不需要——包括那里的 LiteLLM 代理,除非 --api_base 另有指定,--api_format litellm 指向的就是它。
命令行工具不读 .env 文件。请先导出变量,或者在运行前 source 一个被 git 忽略的文件:set -a; source .env; set +a; bbook_maker ...
提供方自己的变量
--provider NAME 从工作目录下的 bbm_providers.json 读取端点,没有则读 ~/.bbm/providers.json。条目里有 env_key 时,会先查它指定的变量,再查 BBM_API_KEY 和上面的备用变量——它指明的是正在调用的端点,所以它自己的 key 才是对的。只有在运行仍然调用那个端点时才会查它:--api_base 指向别处,或者在没有写 base_url 的条目上用 --api_format 覆盖格式,都会把请求移到另一台主机,条目的 key 不会发到那里。运行会说明这一点;如果你本意是继续用它,请传 --key。文件里只有地址和变量名,从不存放密钥。见端点、模型与语言。--model orcarouter 以同样方式读取 BBM_ORCAROUTER_API_KEY。
分类器变量
Jev 兼容分类器只为它自己的主机读取 key:
| 变量 | 用于 |
|---|---|
JEV_API_KEY、TYPESAFE_API_KEY |
typesafe.ai 地址上 TypeSafe 的 Jev |
CF_AIG_TOKEN |
gateway.ai.cloudflare.com 上开启认证的 Cloudflare AI Gateway |
通过网关访问时,用 --classify-key 或提供方条目的 classify_env_key 指定 key。
BBM_JEV_MIN_CONFIDENCE 是 0 到 1 之间的数,在不写 --classify-min-confidence 时为单次运行设置 Jev 的闸门(两者都有时以参数为准):低于它的 skip 会被翻译。默认是 0.95。不是 0 到 1 之间的数时,运行会停下。
旧的各厂商变量
旧式命令暗示了对应路线时,上面四个厂商变量同样会被读取——--model gemini 会查 BBM_GOOGLE_GEMINI_KEY,尽管改写后的命令写的是格式(见从旧参数迁移)。它们绝不会被用于 --api_base 指向其他端点的命令:那种情况下 key 必须与地址相符,只有 BBM_API_KEY 和该格式自己的变量适用。
覆盖提示词
export BBM_CHATGPTAPI_USER_MSG_TEMPLATE=${your_prompt_template}
export BBM_CHATGPTAPI_SYS_MSG=${your_system_message}Zh Installation Pdf (`docs/zh/installation-pdf.md`)
安装 PDF 扩展
如果你想把 PDF 变成双语 EPUB(--to-epub),请在仓库的克隆里安装 PDF 扩展:
pip install ".[pdf]"--to-epub 用 docling 的版面模型和表格模型读取 PDF,所以它需要的比基础安装多:PyTorch、模型,以及用来生成 EPUB 的 Pandoc。这些默认都不安装,因为大多数人翻译的是 EPUB,从来不碰 PDF。不需要 Java;提到 JRE 或 Adoptium 的说明都已过时。
1. 获取代码
这条路线还没有进入已发布的包,所以要从克隆安装。
git clone https://github.com/yihong0618/bilingual_book_maker.gitcd bilingual_book_maker建议使用虚拟环境。
2. Pandoc
PATH 上需要有 Pandoc 3.1.12 或更新版本。它负责生成 EPUB 及其导航。更早的版本会让目录指向文件而不是标题,工具会在打开 PDF 之前就拒绝它们。Ubuntu 24.04 和 Debian 13 上的 apt 包都比这个版本旧,所以请从 pandoc.org/installing.html 获取发布版。
pandoc --version3. PDF 扩展
请安装扩展,而不是锁定版本的 requirements 文件:锁定文件把 PyTorch 和其他所有东西都钉在确切的版本上,所以即使你已经装了 torch,它们也会再下载一遍;扩展则复用已经装好的。
装到哪个 PyTorch 版本,取决于它来自哪个索引,而不是版本号。选择你的系统:
=== "macOS(Apple 芯片)"
```bash
pip install ".[pdf]"
```
你会得到 MPS 加速。没有什么要选的。=== "带 NVIDIA 显卡的 Linux"
```bash
pip install ".[pdf]"
```
PyPI 上的 Linux wheel *就是* CUDA 版,所以这样就够了。不需要 CUDA Toolkit;wheel 自带运行时。=== "Linux,只有 CPU"
```bash
pip install ".[pdf]" \
--extra-index-url https://download.pytorch.org/whl/cpu
```
不加 CPU 索引,你会装上约 3.2 GB 用不上的 CUDA。加上它,约 380 MB,而且没有 `nvidia-*` 包。=== "带 NVIDIA 显卡的 Windows"
```bat
pip install ".[pdf]" ^
--extra-index-url https://download.pytorch.org/whl/cu126
```
**PyPI 上的 Windows wheel 只支持 CPU。** Windows 的 CUDA 版只发布在 PyTorch 自己的索引上,所以普通的那行命令会让你悄无声息地停留在处理器上。更新的显卡和驱动可以用 `cu128`。CUDA wheel 约 2.7 GB。
你还需要 NVIDIA 驱动。PyTorch 自带 CUDA 运行时,所以你**不**需要 CUDA Toolkit,但驱动要你自己安装:
- 驱动下载:<https://www.nvidia.com/en-us/drivers/>(Game Ready 或 Studio 都可以)。
- PyTorch 的安装矩阵,用来确认适合你显卡的渠道:<https://pytorch.org/get-started/locally/>。
安装 PyTorch 之前先检查驱动:
```bat
nvidia-smi
```
它会打印驱动版本和它支持的最高 CUDA 版本。如果这个数字低于你选的渠道(`cu126` 对应 12.6),就更新驱动。在有显卡的机器上 `torch.cuda.is_available()` 却返回 `False`,通常就是驱动太旧。=== "Windows,只有 CPU"
```bat
pip install ".[pdf]"
```
在 Windows 上这会给你 CPU 版,因为 PyPI 在那里发布的就是它。请用 --extra-index-url,不要用 --index-url。--index-url 会替换掉 PyPI,这个工具需要的其他所有包就都解析不到了。
如果你想要与项目测试时完全相同的版本,requirements-pdf-gpu.txt 和 requirements-pdf-cpu.txt 就是 Docker 镜像安装的那两套锁定版本(pdf 用 CPU 那份,pdf-cuda 用 GPU 那份、去掉基础镜像里已有的 PyTorch;pip install -r requirements-pdf-cpu.txt 的文件里已经写明了 CPU 索引)。它们会替换掉你已有的 PyTorch。
使用 uv
uv pip install ".[pdf]"想要 CPU 版时:
uv pip install ".[pdf]" --torch-backend=cpu4. 运行
先读两页。--test 只翻译少数几段。
python make_book.py \
--book_name paper.pdf \
--to-epub \
--pages 1-2 \
--test然后完整运行:
python make_book.py \
--book_name paper.pdf \
--to-epub \
--use_context session模型(约 500 MB)在第一次运行时下载,所以第一份 PDF 会比第二份明显慢一些。下载期间进度行会一直在走。模型来自 Hugging Face,所以可以用 HF_HOME 换个缓存位置:
export HF_HOME=/path/with/room用 --pdf-ocr 读取扫描件不会再下载别的东西:pdf 扩展会连同模型一起安装 rapidocr,在 Mac 上还会安装 Apple 的引擎 ocrmac,auto 在那里会选它。选择 OCR 引擎对它们做了比较。
模型缓存好以后,提取就不需要网络了。PDF 推荐设置按文档类型给出了对应的命令。
检查装到了什么
python -c "import torch; print(torch.__version__, torch.version.cuda)"- 版本号以
+cpu结尾,后面是None:CPU 版。走 CPU 路线时这是对的。 - 类似
12.6的 CUDA 版本:CUDA 版。这时torch.cuda.is_available()会告诉你这台机器能否用上它。False表示这个版本带有 CUDA,但没有可用的显卡,或者驱动太旧。 - 在 Apple 芯片上,
None是对的:MPS 不是 CUDA。用python -c "import torch; print(torch.backends.mps.is_available())"检查它。 - 在带 NVIDIA 显卡的 Windows 上,
None表示你装的是 CPU 版,PyPI 在那里发布的就是它。回到第 3 步。
--device cuda 能把这两种失败区分开,因为它们的解决办法不同:只支持 CPU 的版本要重新安装;没有显卡的机器则不是重装能解决的。
不克隆直接安装:暂时不行
pip install "bbook_maker[pdf]" 不会安装这条路线,也不会报错。pip 把未知的扩展当作警告处理,安装不带 docling 的最新发布版,然后以 0 退出:
WARNING: bbook-maker 1.2.1 does not provide the extra 'pdf'
Successfully installed bbook-maker-1.2.1下一次处理 PDF 时,运行会以缺少扩展的信息拒绝。在某个发布版包含这条路线之前,请克隆仓库(第 1 步),并从克隆安装 ".[pdf]"。
保留 CPU 版
torch==…+cpu 满足任何 torch>=… 的要求,所以没有什么会强制替换它。但下一次不带索引、又涉及 PyTorch 的 pip install -U 会拉取 CUDA wheel,带进约 3 GB 的内容。让这个索引固定在环境里:
export PIP_EXTRA_INDEX_URL=https://download.pytorch.org/whl/cpu或者写进 pip.conf / pip.ini:
[global]
extra-index-url = https://download.pytorch.org/whl/cpuuv 的对应写法是 UV_TORCH_BACKEND=cpu。
或者全部跳过:Docker
pdf 镜像标签带有 Pandoc 和完整的 docling 运行环境,PyTorch 用的是 CPU 版本:docker pull ghcr.io/yihong0618/bilingual_book_maker:pdf。有 NVIDIA 显卡的话,用 pdf-cuda 标签。见 Docker 安装,其中也解释了为什么 Mac 应该直接在本机安装。
大小
PDF 这一步在 PyTorch 2.7.1 下要下载的内容,按各平台分别解析(102 个包):
| 下载量 | |
|---|---|
| macOS,Apple 芯片 | ~270 MB |
| Linux x86_64,CPU 版 | ~380 MB |
| Linux x86_64,CUDA 版 | ~3.2 GB |
| Windows x86_64,PyPI(CPU 版) | ~420 MB |
Windows x86_64,cu126 |
~2.9 GB |
另外第一次运行时还有 ~500 MB 的模型,所有平台都一样。
差异主要来自 PyTorch。依赖树的其余部分在各平台上都约为 200 MB(opencv 48 MB,scipy 29 MB,rapidocr 27 MB,transformers、numpy、pandas……)。单看 PyTorch wheel:
| torch 2.7.1 wheel | |
|---|---|
| macOS arm64 | 68.6 MB |
Linux x86_64,+cpu |
175.8 MB,其元数据中没有声明任何 nvidia-* 依赖 |
| Linux x86_64,PyPI 默认 | 821.0 MB,另加 ~2.16 GB 的 nvidia-* 和 triton wheel |
| Windows x86_64,PyPI | 216.0 MB(CPU 版) |
Windows x86_64,cu126 |
2.72 GB |
两个平台恰好相反,这正是陷阱所在:在 Linux 上默认是 CUDA,要主动退出;在 Windows 上默认是 CPU,要主动加入。macOS 没有 CUDA 版本;它那个小小的 PyTorch 带的是 Metal 内核,没有 CUDA,也不缺任何东西。
如果不能用
下面是工具会打印的信息,以及该怎么做。
reading a PDF needs the pdf extra, which is not installed.第 3 步没有做。如果你运行的是pip install "bbook_maker[pdf]",而它说安装成功了,那就是上面说的陷阱。Pandoc is required for --to-epub. Install it and make sure pandoc is on PATH.做第 2 步。… is too old for EPUB export; Pandoc 3.1.12 or newer is required你的 Pandoc 来自 apt。从 pandoc.org 安装发布版(第 2 步),并把它放在 PATH 的最前面;这条信息还提到了另一种办法:分步脚本tools/pdf_to_book.py --pandoc PATH。--device cuda was asked for, but the installed PyTorch is a CPU-only build.用 CUDA 版重新安装扩展:Linux 上用普通的那行命令,Windows 上用cu126索引(第 3 步)。--device cuda was asked for, but this machine has no cuda accelerator available.这个版本带有 CUDA,但机器或驱动提供不了。运行nvidia-smi:没有输出说明没有驱动;CUDA 版本低于你的渠道说明驱动太旧。否则就用--device cpu。- 在 Windows 上,机器明明有显卡,
torch.version.cuda却是None。 用的是普通的那行命令。指定cu126索引重新安装(第 3 步)。 … selected pages have no text layer …; rerun with --pdf-ocr …这份 PDF 是扫描件。加上--pdf-ocr;如果扫描件不是中文或英文,再加上--ocr-lang。见 PDF 推荐设置。- 在 Docker 里,ARM 机器上的
--gpus all好像被忽略了。 确实如此:arm64 镜像里的 PyTorch 只支持 CPU。加上--platform linux/amd64。 - 在 Mac 上的 Docker 里,GPU 从来用不上。 这是对的,也没法解决:Linux 虚拟机看不到 Metal。要用 MPS,请直接在本机安装(第 1 到 4 步)。
Zh Installation (`docs/zh/installation.md`)
安装
bilingual book maker 是一个 Python 程序,需要 Python 3.10 或更新版本。用虚拟环境可以把它和你的其他包隔开。
从 PyPI 安装
pip install -U bbook_maker这样你就有了 bbook_maker 命令。它能翻译 EPUB、TXT、Markdown 和 SRT 文件,也能用旧的纯文本路线处理 PDF。
发布的包暂时还不带 PDF 转 EPUB 路线。如果需要它,请从仓库源码安装(见下文),再加上 PDF 扩展。
从仓库源码安装
想用最新代码或 PDF 转 EPUB 路线,就从源码安装。
git clone https://github.com/yihong0618/bilingual_book_maker.gitcd bilingual_book_makerpip install -r requirements.txtrequirements.txt 是锁定并测试过的一组版本。pip install . 也可以,装的是当下能解析到的版本。从源码运行时命令是 python make_book.py,参数与 bbook_maker 相同。
PDF 扩展
如果要把 PDF 转成双语 EPUB(--to-epub),请在同一份源码里加装 PDF 扩展。它会带来 docling 和 PyTorch;模型(约 500 MB)在首次运行时下载,另外还需要 Pandoc 3.1.12 或更新版本。
pip install ".[pdf]"这个扩展会复用你已经装好的 PyTorch,而不是另外下载一个锁定版本。上面这行命令适用于 Apple 芯片、带 NVIDIA GPU 的 Linux,以及不带 NVIDIA GPU 的 Windows。不带 NVIDIA GPU 的 Linux 要加上 PyTorch 的 CPU 索引,否则会下载约 3 GB 用不上的 CUDA;带 NVIDIA GPU 的 Windows 要加上 PyTorch 的 CUDA 索引。安装 PDF 扩展列出了每种情况的命令。
不要运行 pip install "bbook_maker[pdf]"。发布的包还不带这条路线:pip 只会给出警告,然后安装不含它的发布版。
Docker
如果不想安装 Python 包,就用发布的镜像,见 Docker。
检查安装
bbook_maker --help从源码运行时:
python make_book.py --help每个参数都以帮助文本为准。命令行参数把它们列在一张表里。
Zh Languages (`docs/zh/languages.md`)
语言
--language 接受这张表里任何一行的任意一种写法:标签(--language zh-hant)或名称(--language "traditional chinese",不区分大小写)。标签是机械用途的那一半——标在插入的标记和 dc:language 上,记录在翻译元数据记录里,并作为结构化输出回复字段的名字(zh_hant_translation)。名称是文字那一半:真正向模型提出的要求。
这张表里没有的语言,请自己以 TAG:NAME 的形式同时给出两半,按第一个冒号拆分——例如 --language "ain:Ainu"。这时标签驱动标注和字段名,就像它列在这里一样;名称交给模型。随手输入、什么都不匹配的名称仍然可以运行——运行会打印一行 Note:,说明输出上不会标注任何东西。
源语言从不属于 --language;自动检测不够用时,用 --source_lang 指定它。
标签
| 标签 | 名称 |
|---|---|
aa |
afar |
ab |
abkhaz |
ace |
acehnese |
af |
afrikaans |
ak |
akan |
akk |
akkadian |
am |
amharic |
an |
aragonese |
ang |
old english |
apc |
levantine arabic |
ar |
arabic |
arc |
aramaic |
arn |
mapudungun |
ary |
moroccan arabic |
arz |
egyptian arabic |
as |
assamese |
ast |
asturian |
av |
avar |
ay |
aymara |
az |
azerbaijani |
ba |
bashkir |
ban |
balinese |
bcl |
bikol |
be |
belarusian |
bg |
bulgarian |
bho |
bhojpuri |
bi |
bislama |
bm |
bambara |
bn |
bengali |
bo |
tibetan |
br |
breton |
brx |
bodo |
bs |
bosnian |
bug |
buginese |
ca |
catalan |
ce |
chechen |
ceb |
cebuano |
ch |
chamorro |
chr |
cherokee |
ckb |
central kurdish |
co |
corsican |
cop |
coptic |
cr |
cree |
cs |
czech |
csb |
kashubian |
cu |
church slavonic |
cv |
chuvash |
cy |
welsh |
da |
danish |
de |
german |
de-at |
austrian german |
de-ch |
swiss standard german |
doi |
dogri |
dsb |
lower sorbian |
dv |
dhivehi |
dz |
dzongkha |
ee |
ewe |
egy |
ancient egyptian |
el |
greek |
en |
english |
en-au |
australian english |
en-ca |
canadian english |
en-gb |
british english |
en-in |
indian english |
en-us |
american english |
enm |
middle english |
eo |
esperanto |
es |
spanish |
es-419 |
latin american spanish |
es-ar |
argentine spanish |
es-es |
european spanish |
es-mx |
mexican spanish |
et |
estonian |
eu |
basque |
fa |
persian |
ff |
fulah |
fi |
finnish |
fil |
filipino |
fj |
fijian |
fo |
faroese |
fr |
french |
fr-ca |
canadian french |
fro |
old french |
fur |
friulian |
fy |
western frisian |
ga |
irish |
gd |
scottish gaelic |
gl |
galician |
gn |
guarani |
goh |
old high german |
got |
gothic |
grc |
ancient greek |
gsw |
swiss german |
gu |
gujarati |
gv |
manx |
ha |
hausa |
hak |
hakka |
haw |
hawaiian |
he |
hebrew |
hi |
hindi |
hil |
hiligaynon |
hit |
hittite |
hr |
croatian |
hsb |
upper sorbian |
ht |
haitian creole |
hu |
hungarian |
hy |
armenian |
ia |
interlingua |
id |
indonesian |
ie |
interlingue |
ig |
igbo |
ilo |
ilocano |
io |
ido |
is |
icelandic |
it |
italian |
iu |
inuktitut |
ja |
japanese |
jam |
jamaican patois |
jbo |
lojban |
jw |
javanese |
ka |
georgian |
kab |
kabyle |
kam |
kamba |
kbd |
kabardian |
kg |
kongo |
ki |
kikuyu |
kk |
kazakh |
kl |
greenlandic |
km |
khmer |
kn |
kannada |
ko |
korean |
kok |
konkani |
ks |
kashmiri |
ku |
kurdish |
kv |
komi |
kw |
cornish |
ky |
kyrgyz |
la |
latin |
lad |
ladino |
lb |
luxembourgish |
lez |
lezgian |
lg |
ganda |
lij |
ligurian |
lld |
ladin |
lmo |
lombard |
ln |
lingala |
lo |
lao |
lt |
lithuanian |
lu |
luba-katanga |
luo |
luo |
lv |
latvian |
lzh |
literary chinese |
mad |
madurese |
mai |
maithili |
mer |
meru |
mg |
malagasy |
mi |
maori |
min |
minangkabau |
mk |
macedonian |
ml |
malayalam |
mn |
mongolian |
mni |
manipuri |
mr |
marathi |
ms |
malay |
mt |
maltese |
mwl |
mirandese |
my |
myanmar |
myv |
erzya |
nah |
nahuatl |
nan |
hokkien |
nap |
neapolitan |
nb |
norwegian bokmal |
nds |
low german |
ne |
nepali |
new |
newari |
nl |
dutch |
nl-be |
belgian dutch |
nn |
nynorsk |
no |
norwegian |
non |
old norse |
nr |
southern ndebele |
nso |
northern sotho |
nv |
navajo |
ny |
chichewa |
nyn |
nyankole |
oc |
occitan |
oj |
ojibwe |
om |
oromo |
or |
odia |
os |
ossetian |
pa |
punjabi |
pal |
middle persian |
pam |
kapampangan |
pap |
papiamento |
pi |
pali |
pl |
polish |
pms |
piedmontese |
ps |
pashto |
pt |
portuguese |
pt-br |
brazilian portuguese |
pt-pt |
european portuguese |
qu |
quechua |
rm |
romansh |
rn |
kirundi |
ro |
romanian |
ru |
russian |
rue |
rusyn |
rw |
kinyarwanda |
sa |
sanskrit |
sah |
yakut |
sat |
santali |
sc |
sardinian |
scn |
sicilian |
sco |
scots |
sd |
sindhi |
se |
northern sami |
sg |
sango |
sga |
old irish |
shn |
shan |
si |
sinhala |
sk |
slovak |
sl |
slovenian |
sm |
samoan |
sn |
shona |
so |
somali |
sq |
albanian |
sr |
serbian |
sr-cyrl |
serbian cyrillic |
sr-latn |
serbian latin |
srn |
sranan tongo |
ss |
swati |
st |
southern sotho |
su |
sundanese |
sux |
sumerian |
sv |
swedish |
sw |
swahili |
syr |
syriac |
szl |
silesian |
ta |
tamil |
te |
telugu |
tg |
tajik |
th |
thai |
ti |
tigrinya |
tk |
turkmen |
tl |
tagalog |
tn |
tswana |
to |
tongan |
tpi |
tok pisin |
tr |
turkish |
ts |
tsonga |
tt |
tatar |
ty |
tahitian |
tyv |
tuvan |
tzm |
central atlas tamazight |
udm |
udmurt |
ug |
uyghur |
uk |
ukrainian |
umb |
umbundu |
ur |
urdu |
uz |
uzbek |
ve |
venda |
vec |
venetian |
vi |
vietnamese |
vo |
volapuk |
wa |
walloon |
war |
waray |
wo |
wolof |
wuu |
wu chinese |
xcl |
classical armenian |
xh |
xhosa |
yi |
yiddish |
yo |
yoruba |
yua |
yucatec maya |
zh |
chinese |
zh-cn |
mainland chinese |
zh-hans |
simplified chinese |
zh-hant |
traditional chinese |
zh-hk |
hong kong chinese |
zh-sg |
singapore chinese |
zh-tw |
taiwan mandarin |
zh-yue |
cantonese |
zu |
zulu |
其他写法
上表印出的名称之外,下面这些名称也被接受,对应同一个标签。
| 可以输入 | 标签 |
|---|---|
bahasa indonesia |
id |
bahasa melayu |
ms |
bokmal |
nb |
brazilian |
pt-br |
burmese |
my |
castilian |
es |
chewa |
ny |
classical chinese |
lzh |
divehi |
dv |
farsi |
fa |
flemish |
nl |
frisian |
fy |
haitian |
ht |
irish gaelic |
ga |
isixhosa |
xh |
isizulu |
zu |
jamaican creole |
jam |
judeo-spanish |
lad |
kalaallisut |
kl |
kirghiz |
ky |
letzeburgesch |
lb |
maldivian |
dv |
mandarin |
zh |
mandarin chinese |
zh |
min nan |
nan |
modern standard arabic |
ar |
moldavian |
ro |
moldovan |
ro |
norwegian nynorsk |
nn |
nyanja |
ny |
old church slavonic |
cu |
oriya |
or |
panjabi |
pa |
pilipino |
fil |
pushto |
ps |
putonghua |
zh |
scots gaelic |
gd |
sesotho |
st |
setswana |
tn |
shanghainese |
wuu |
sinhalese |
si |
sorani |
ckb |
standard chinese |
zh |
swiss high german |
de-ch |
taiwanese hokkien |
nan |
twi |
ak |
uighur |
ug |
valencian |
ca |
Zh Llm Args (`docs/zh/llm-args.md`)
用大模型翻译
有 OpenAI key 的话,默认路线什么都不用再配:
export OPENAI_API_KEY=sk-...
bbook_maker --book_name my_book.epub --use_context session本页介绍决定请求发往哪里、请求要什么的参数。它们对每种输入格式都一样;各格式额外的参数见格式各页。
路线:模型、端点、格式
路线指的是端点,而不是模型名。三个参数确定一条路线。
| 参数 | 作用 |
|---|---|
--model(-m) |
模型 ID,按端点自己的写法:gpt-5-mini、claude-sonnet-4-6、openai/gpt-5-mini。openai 格式下默认 gpt-6-luna。anthropic 格式必须写。 |
--api_base |
端点 URL。默认是该格式的官方地址。OpenAI 形式的端点以 /v1 结尾。粘贴进来的 …/v1/chat/completions 或末尾的斜杠会被去掉。 |
--api_format |
端点说的 API:openai、anthropic、codex、gemini、qwen、groq、xai、litellm,或某个翻译服务引擎。不写时由 --api_base 推断:Anthropic 的地址是 anthropic,其余都是 openai。没写 --api_base 而模型 ID 是 claude-* 时,也会选 anthropic。 |
每种常见情况一个示例:
=== "OpenAI"
```bash
bbook_maker \
--book_name my_book.epub \
--model gpt-6-luna \
--use_context session
```=== "Anthropic"
```bash
bbook_maker \
--book_name my_book.epub \
--model claude-sonnet-4-6 \
--use_context session
```=== "任意 OpenAI 兼容端点"
```bash
bbook_maker \
--book_name my_book.epub \
--api_base https://gateway.example.com/v1 \
--model provider/model-id \
--use_context session
```=== "Gemini"
```bash
bbook_maker \
--book_name my_book.epub \
--api_format gemini \
--model gemini-flash-latest \
--use_context
```
Gemini 自己维护对话历史,所以用窗口模式(`--use_context`),而不是会话模式。`--interval` 设置请求之间的间隔,免费额度就靠它避开限流。=== "Codex(ChatGPT 套餐)"
```bash
python make_book.py \
--book_name my_book.epub \
--api_format codex
```
通过 [Codex CLI](https://developers.openai.com/codex/cli) 消耗你的 ChatGPT 套餐额度,需要先安装并登录(`codex login`)。默认运行 `gpt-6-luna`,换模型加 `--model `。线程本身就是上下文,所以不需要 `--use_context`。它忽略 `--api_base` 和 `--key`。Codex 回答所选模型已满载时,运行会打印一行 `codex: … retrying in 60 s` 并再次请求;这种回答通常是 Codex 在对它不信任的网络限流,换个网络或账号会有帮助。提供 Claude 模型的网关通常说的是 OpenAI 形式。如果网关对 anthropic 形式回 404,运行会停下,并指出改用 --api_format openai 即可。
--api_format groq、xai 和 litellm 分别是 Groq、xAI 和 LiteLLM 代理(http://localhost:4000)上的 OpenAI 形式。每个格式都知道自己的地址,所以格式、key 和 --model 就是完整的路线。这几个格式必须写 --model。
--model orcarouter 把运行交给 OrcaRouter 网关及其 orcarouter/auto 模型。key 来自 --key 或 BBM_ORCAROUTER_API_KEY。
每家厂商一条命令
你的厂商如果在下面列出,这就是完整的命令(key 也可以来自环境变量,见 API key):
/* Detailed source-code truncated for AI context efficiency. */翻译服务(谷歌、DeepL、彩云、腾讯、自定义 API)见翻译服务。常用的厂商最好写进提供方文件。
API key
key 按以下顺序查找:
--key(--api_key是同一个参数)。用逗号分隔的多个 key 会轮流使用,以绕过单个 key 的限流。BBM_API_KEY。- 该格式自己的变量:
OPENAI_API_KEY、ANTHROPIC_API_KEY、BBM_GOOGLE_GEMINI_KEY、BBM_QWEN_API_KEY、BBM_GROQ_API_KEY、BBM_XAI_API_KEY。
localhost 上的端点不需要 key。写在命令行里的 key 会出现在进程列表中,本机其他用户都能看到;尽量用环境变量。本工具自己不读 .env 文件,请先加载:
set -a; source .env; set +a更多变量见环境变量。
提供方文件与额外模型
--provider NAME 从 JSON 文件而不是参数中读取路线:运行目录下的 bbm_providers.json,或 ~/.bbm/providers.json。两处都没有的名字会退回到随仓库提供的 bbm_providers.example.json,运行时会说明这一点。
bbook_maker \
--book_name my_book.epub \
--provider siliconflow \
--use_context session有两个步骤可以使用翻译模型以外的模型:
| 参数 | 作用 |
|---|---|
--classify-model MODEL |
决定 EPUB 计划的模型,或一个 Jev 兼容的分类器(jev 即 TypeSafe 的分类器)。默认:条目的 classify_model,否则用翻译模型。--plan-classify-model 是它的旧名。 |
--classify-base-url URL、--classify-key KEY |
该模型不在本次运行的端点上时,它的地址和 key:一个 OpenAI 兼容地址,或 Jev 兼容分类器的 URL。 |
--img-model MODEL |
查看页面图像的步骤所用的视觉模型(目前是 PDF 路线上纠正区域角色)。默认:条目的 img_model,否则关闭。绝不会退回使用翻译模型。none 关闭条目里的图像模型。 |
--img-base-url URL、--img-key KEY |
该模型的地址和 key(仅限 OpenAI 兼容)。 |
字段、随附的条目、哪个 key 发往哪里,以及 Jev 和 Jev 兼容分类器,见提供方文件与额外模型。注意随附的 openai 条目写了图像模型,所以 --provider openai 会开启 PDF 路线的图像步骤。
语言
| 参数 | 作用 |
|---|---|
--language |
目标语言。可以是标签(zh-hant)、名称("Traditional Chinese"),表里没有该语言时用 TAG:NAME(--language "zh-hant:Traditional Chinese")。标签写进输出文件,名称是向模型提要求时的说法。默认 zh-hans。完整列表见语言标签。 |
--source_lang |
源语言,直接指定而不是自动检测。在每条大模型路线上都会写进提示词。默认:自动检测。 |
提示词
--prompt 接受模板字符串、JSON 字符串,或 .json、.txt、.md 文件。它有三个部分:
user:模板。必填。必须包含{text};{language}和{crlf}也会被填入。其他任何占位符都会在运行开始时被拒绝。system:系统消息。style:关于语域和语气的说明。它在每个上下文窗口开始处说一次,会话模式下会传给每一个窗口。
裸字符串或 .txt 文件就是 user 模板。运行时会打印用了哪些部分、各自来自哪里。格式见提示词文件。
bbook_maker \
--book_name my_book.epub \
--prompt prompt_template.json \
--use_context session试运行与续跑
| 参数 | 作用 |
|---|---|
--test |
只翻译开头几段,用来检查配置。 |
--test_num N |
翻译多少段(默认 10)。 |
--resume |
从检查点继续一次中断的运行。EPUB 检查点记录了语言、提示词和模型;不一致时续跑会停下。 |
换了新端点,务必先跑一次 --test。它几乎不花钱,能让你看到输出格式和运行选中的路线。
重试:“耐心”是什么意思
很多人用本工具对接的服务商又慢又限流,首个 token 或一次 429 退避可能要等上几分钟。所以只要错误有可能靠等待消除,工具就绝不放弃。
- 超时、连接中断、429 或 5xx 会不限次数地重试。等待从 1 秒开始,逐次翻倍,涨到 5 分钟后不再增加。
- 每次重试打印一行,长时间等待也不会像是卡住了:
retrying after RateLimitError (…) — attempt 4, waiting 8s - 等待也不会消除的错误会立刻停止运行:key 被拒、权限错误、端点拒绝的请求、不存在的模型。
- Ctrl+C 总能停止运行,即使正在长时间等待。进度会保存,用
--resume重新运行即可。
如果同一行重试信息持续很久,说明服务商宕机了或正在对你限流。它恢复后,运行会自己继续。
本地模型(Ollama、llama.cpp、LM Studio)
本地模型就是你自己机器上的一个 OpenAI 兼容端点。把 --api_base 指向它即可,不需要 key。
bbook_maker \
--book_name my_book.epub \
--api_base http://localhost:11434/v1 \
--model qwen3:8b \
--use_context session8B 或 16B 模型的预期表现:
- 没有严格的 JSON schema。 多数本地服务器不强制 schema。运行会用一次探测请求发现这一点并换成分隔符格式,打印类似
doesn't apply JSON schema … using delimiter method的一行。这不是错误。 - 请求更小。 在没有严格 schema 的端点上,每个请求最多携带 8 个单元,是默认 16 个的一半;不在会话模式下时,文本约 800 token。运行会打印它选定的数字。这些默认值就是按小模型能承受的量选的。
- 计划分类只答一个词。 计划模式每轮就五种块问模型
skip还是translate。如果模型连续两次没按回答格式作答,运行改为每轮问三种,再改为一种。仍然不行就停止分类,其余部分全部翻译。任何读不懂的回答都按translate处理,所以弱模型宁可多翻,不会少翻。你也可以用--classify-model把分类交给托管模型,翻译仍在本地进行;见提供方文件与额外模型。 - 上下文短。 把
--context-compact-at设为模型的输入上限(最小 1500),会话窗口就不会溢出。 - 思考型模型。 回答前先推理的模型可以用
--no-thinking让它别想。运行会从服务器自己的拒绝回复中找出它接受的请求字段。如果你的服务器需要它自己的字段,--extra_body优先,例如--extra_body '{"chat_template_kwargs": {"enable_thinking": false}}'。 - 没有图像步骤。 PDF 路线的图像步骤只在你用
--img-model指定的模型上运行,所以本地模型永远不会被要求读页面。
小模型上应避免的参数:
--glossary-auto on。它需要一个用名称而不是大段文字作答的模型。- 调高
--accumulated_num或--max-batch-units。运行打印misaligned batches this run时,应当调低它们。 --model_list。每个模型有自己的缓存,在一段对话里混用模型会损害一致性。
如果你的模型上计划分类总是失败,--plan-classify all 会跳过分类、翻译每一个块。见计划模式。
其他请求选项
| 参数 | 作用 |
|---|---|
--temperature |
采样温度。anthropic 格式总会发送;openai 格式在它等于 API 默认值或模型不接受温度时不发送;codex 忽略它。 |
--no-thinking |
让模型回答前不要推理;翻译一段文字,思考带不来任何好处,只花 token 和时间。在 OpenAI 形式的路线上,请求字段从端点自己的拒绝回复中找出并记住;如果端点拒绝所有写法,运行警告一次,然后不带它继续。在 anthropic 上是 thinking: {"type": "disabled"}。在 codex 上会被拒绝。图像请求也会带上它。 |
--extra_body JSON |
附加到每个请求体的字段(openai 和 anthropic 路线)。这里的字段优先于对应的参数,--no-thinking 也不例外。 |
--extra_headers JSON |
附加到每个请求的 HTTP 头(openai 和 anthropic 路线)。 |
--model_list IDS |
轮流使用的多个模型,用来分摊限流。与 --use_context session 一起使用会被拒绝。 |
--interval SECONDS |
请求之间的间隔。只有 gemini 格式使用。 |
-p、--proxy URL |
本次运行的 HTTP 代理,例如 http://127.0.0.1:7890。 |
--batch、--batch-use |
OpenAI 的 Batch API:先提交任务,之后再用结果生成书。不适用于 EPUB。 |
常见的 --extra_body 和 --extra_headers 取值:
# openai route: turn off a local/vLLM chat template's thinking block
--extra_body '{"chat_template_kwargs": {"enable_thinking": false}}'
# openai route (chat completions): reasoning effort and a token ceiling, neither of which has a flag
--extra_body '{"reasoning_effort": "low", "max_completion_tokens": 2000}'
# anthropic route: keep extended thinking off
--extra_body '{"thinking": {"type": "disabled"}}'
# OpenRouter attribution, shown on its dashboard
--extra_headers '{"HTTP-Referer": "https://example.com", "X-Title": "bilingual_book_maker"}'
# a gateway's own auth or routing header
--extra_headers '{"X-API-Key": "sk-gateway-..."}'Zh Machine Args (`docs/zh/machine-args.md`)
翻译服务
这些路线调用的是翻译服务:送进去文字,拿回译文。其中有些服务在接口背后用的是语言模型,但都不接受本工具的提示词。它们不需要模型,常常也不需要 key。它们也无法回答问题,所以本工具里一切需要问模型的功能,在这些路线上都是关闭的。
路线
用 --api_format 选择一条。
--api_format |
服务 | key | 目标语言 |
|---|---|---|---|
google |
谷歌翻译的公开网页端点 | 无 | 大多数语言;源语言自动检测 |
deepl |
通过 RapidAPI 上的 DeepL Translator 使用 DeepL | --key,或 BBM_DEEPL_API_KEY |
DeepL 支持的语言;其他语言会以 DeepL do not support … 拒绝 |
deeplfree |
DeepL 的免费网页端点 | 无 | DeepL 支持的语言 |
caiyun |
彩云小译 | --key,或 BBM_CAIYUN_API_KEY |
简体中文、英语、日语 |
tencent |
腾讯交互翻译 | 无 | 中文或英语 |
customapi |
你自己的 HTTP 服务 | 无;地址写在 --api_base |
取决于你的服务 |
两条 DeepL 路线都把源语言当作英语发送。请用它们翻译英文书。
彩云提供了一个测试 token 3975l6lr5pcbvidl6jl2,用来试用这条路线;申请自己的 token,请参考这个教程。
命令
=== "谷歌"
```bash
bbook_maker \
--book_name my_book.epub \
--api_format google \
--language zh-hant
```=== "DeepL"
```bash
bbook_maker \
--book_name my_book.epub \
--api_format deepl \
--key ${deepl_key} \
--language ja
```=== "DeepL 免费版"
```bash
bbook_maker \
--book_name my_book.epub \
--api_format deeplfree \
--language de
```=== "彩云小译"
```bash
bbook_maker \
--book_name my_book.epub \
--api_format caiyun \
--key ${caiyun_key} \
--language zh-hans
```=== "腾讯交互翻译"
```bash
bbook_maker \
--book_name my_book.epub \
--api_format tencent \
--language zh-hans
```=== "自定义 API"
```bash
bbook_maker \
--book_name my_book.epub \
--api_format customapi \
--api_base https://your.host/translate \
--language ja
```自定义 API 的约定
工具对每个段落向 --api_base 发送一个 POST 请求,采用表单编码,带三个字段:text、source_lang 和 target_lang。它从 JSON 回复的 data 字段读取译文。每个请求 10 秒超时,请求之间等待 5 秒。--source_lang 就是在这里传给你的服务。
这些路线做不到的事
- 没有提示词。
--prompt无处可去;文本是单独发送的。 - 没有会话。
--use_context session会被拒绝:没有可以保持的对话。 - 引擎本身不做计划分类。 在 EPUB 上,运行会退回到
--translate-tags的选择(默认p),所以 `` 之外的诗歌或表格不会被翻译。指定一个大模型分类器,引擎翻译的同时这本书也能得到完整的计划:--classify-model gpt-6-luna(它读取OPENAI_API_KEY),或提供方条目的classify_model。这在谷歌翻译上端到端跑通过。没有分类器时,--plan-classify model会被拒绝,而--plan-classify all仍会翻译计划找到的每一个块,不问任何人。见计划模式。 - 没有术语表。 只有 OpenAI 形式和 Codex 形式的路线会读
--glossary;在这里它会被忽略并给出警告。 - 在
google、deepl、deeplfree、caiyun和tencent上不能指定源语言。--source_lang会被忽略并给出警告;只有customapi(以及qwen大模型路线)会发送它。
其他都能用:--test、--resume、--single_translate,以及格式各页上的各格式参数。
限流
彩云被限流时会打印 will sleep 60s for the time limit 并等待。DeepL 免费版和腾讯会自行在请求之间暂停。耐心重试策略适用于每一条路线。
Zh Migration (`docs/zh/migration.md`)
从旧参数迁移
为旧命令行写的命令仍然可用。每个已移除的参数都会在运行开始前被改写成新参数,并打印一行说明它变成了什么:
$ bbook_maker --book_name book.epub --model gpt4omini --openai_key sk-...
deprecated: --openai_key is now --key
deprecated: --model gpt4omini is now --model gpt-4o-mini| 旧写法 | 改写为 |
|---|---|
--model gpt4o / gpt4omini / o3mini |
--model 加该模型的 ID |
--model chatgptapi / openai |
去掉:openai 格式就是默认值,想指定模型时用 --model 写模型 |
--model openai --model_list X |
--model_list X |
--model claude |
--model claude-haiku-4-5-20251001 |
确切的 claude-* ID |
不变;anthropic 格式由 ID 推断 |
--model gemini / geminipro |
--api_format gemini --model gemini-flash-latest / gemini-pro-latest |
--model qwen / qwen-mt-turbo / qwen-mt-plus |
--api_format qwen --model qwen-mt-* |
--model groq --model_list X |
--api_format groq --model_list X |
--model xai |
--api_format xai --model grok-4.3 |
--model codex |
--api_format codex(侧车进程的默认模型;要指定模型就加 --model) |
--model google / caiyun / deepl / deeplfree / tencentransmart |
--api_format google / caiyun / deepl / deeplfree / tencent |
--custom_api URL |
--api_format customapi --api_base URL |
--openai_key / --claude_key / --gemini_key / --groq_key / --xai_key / --qwen_key / --caiyun_key / --deepl_key / --orcarouter_key |
--key(--api_key 是同一个参数,从未改名) |
--ollama_model M |
--api_base http://localhost:11434/v1 --model M |
--deployment_id D |
--model D,同时 --api_base 被改写为该部署的 /openai/v1 路径 |
说明:
- 旧的 key 变量对使用它们的那条路线仍然有效:改写后的
--model groq用BBM_GROQ_API_KEY,改写后的--model gemini用BBM_GOOGLE_GEMINI_KEY,以此类推。 - 你自己传的参数优先。
--model gemini --api_base https://my-gateway/v1会保留你的网关。 --interval不是旧参数:它仍在解析器里,仍然控制 gemini 路线的请求节奏,而 gemini 仍是一条路线。qwen-mt-turbo和qwen-mt-plus是真实的模型 ID,所以同时传了--api_format的命令保持原样,也不会为此打印任何东西。- 其他路线别名在任何地方都不是模型 ID。
--model gemini与指向另一条路线的--api_format一起使用会被拒绝,而不是按其中一方解析:遵从格式,会把该别名的 key 发往不属于它的主机;遵从别名,又会无视你输入的内容。错误信息会列出两者以及两种解决办法。 - 改写后的命令运行的是它过去运行的模型,取自旧的预设列表,而不是更新的模型。其中一些模型已经下线,端点的模型检查会指出这一点。
- 不是旧别名的
--model值会作为模型 ID 原样传递,这在现在是常态。 - 已下线 OpenAI 模型的别名(
gpt4、gpt5mini、o1、o1mini、o1preview)已经去掉。它们现在作为模型 ID 原样传递,由端点按名字拒绝——还是同样的失败,只是早一步发生,信息也更清楚。
Zh Model Lang (`docs/zh/model_lang.md`)
端点、模型与语言
路线由它对接的端点决定,而不是由模型名决定。没有需要随时更新的内置模型列表:--model 接受你的端点提供的任何 ID。
bbook_maker --book_name book.epub \
--api_base https://api.openai.com/v1 --key sk-... \
--model gpt-5-mini --language ja旧的 --model 命令仍然可用——它们会被改写成这些参数,并打印出替换内容。完整对照表见从旧参数迁移。
路线参数
| 参数 | 含义 |
|---|---|
--model |
模型 ID,按端点自己的写法。在 openai 格式下可以省略:默认 gpt-6-luna。 |
--api_base |
端点 URL。默认是该格式的官方地址;…/v1、…/v1/ 和 …/v1/chat/completions 都可以。 |
--key |
API key。用逗号分隔多个 key,可以轮流使用以分摊限流。 |
--api_format |
传输格式。会自动推断;只有推断错了才需要传。 |
--provider |
提供方文件里的一个命名端点,代替上面四个参数。 |
--model_list a,b 在多个模型之间轮换,旧命令用的也是它;一个模型只在其中一个参数里写,不要两个都写。
--api_format 取值为 openai(默认)、anthropic、gemini、qwen、groq、xai、litellm、codex,或固定的翻译服务 google、caiyun、deepl、deeplfree、tencent、customapi。
五种厂商格式各自带着自己的端点,所以格式加一个 key 就是一条完整的路线,不必再去查 --api_base:
| 格式 | 端点 | --model |
|---|---|---|
gemini |
Gemini API | 可选,默认 gemini-flash-latest |
qwen |
DashScope 上的 Qwen-MT | 可选,默认 qwen-mt-turbo |
groq |
https://api.groq.com/openai/v1 |
必填 |
xai |
https://api.x.ai/v1 |
必填 |
litellm |
http://localhost:4000 |
必填,填代理配置里的名字 |
gemini 和 qwen 有各自的协议——Gemini 有原生的约束解码、安全设置和对话历史;Qwen-MT 用一对源/目标语言代替提示词——--interval 控制 gemini 路线的请求节奏,免费额度就靠它避开限流。另外三种是换了地址的 OpenAI 路线,保留它的一切功能。这五种都不会从主机名推断出来:要么写明格式,要么给出厂商的 OpenAI 兼容 --api_base,走 openai 路线。
codex 根本不是端点:它驱动本地的 codex app-server 侧车进程,本次运行记在你的 ChatGPT 套餐上,所以它不接受 --key 和 --api_base,--model 可选(默认 gpt-6-luna)。它从不会被推断出来,必须明确写出。见用大模型翻译。
推断按以下顺序进行:明确写出的 --api_format 优先;然后看 --api_base 的主机(anthropic.com 表示 anthropic 形式,其他都是 OpenAI 形式);然后,在没有指定端点时,看模型 ID 是否提到 claude 或 anthropic,anthropic/claude-sonnet-4-6 也算。
网关被要求使用它并不提供的 anthropic 形式时,会在 /v1/messages 上回 404 或 405。运行会停下并指出解决办法:用 --api_format openai 重新运行。在 Anthropic 自己的主机上,404 表示模型不存在,也会照此报告。
凭据依次来自 --key、BBM_API_KEY,然后是该格式惯用的变量——见环境变量设置。localhost 上的端点不需要 key。
命名端点:`--provider`
用到不止一次的端点可以写下来,而不必每次重敲。先读工作目录下的 bbm_providers.json,再读 ~/.bbm/providers.json;同名时项目里的条目优先。
{
"nvidia": {
"api_style": "openai",
"base_url": "https://integrate.api.nvidia.com/v1",
"default_models": ["moonshotai/kimi-k2-thinking"],
"env_key": "NVIDIA_API_KEY"
}
}bbook_maker --book_name book.epub --provider nvidia --language jaapi_style 可以是任何指明端点的 --api_format——openai、anthropic、gemini、qwen、groq、xai、litellm——另外 claude 是 anthropic 的旧写法。其他任何 OpenAI 兼容主机都是 openai,地址写在 base_url。随仓库提供的 bbm_providers.example.json 为每种都准备了一个条目。明确传入的参数仍然优先于条目,而条目的 env_key 只跟着条目的地址走,不会更远。在条目自己的网关上要求另一种传输格式,并没有移动任何东西,所以仍会读取这个 key。--api_base 指向别处就会移动这次运行;在没有 base_url 的条目上用 --api_format 覆盖格式也会,因为这时地址是由格式提供的——key 不会为另一家厂商的主机读取,运行会说明它在比较哪两个地址。如果你本意是继续用它,请传 --key。default_models 只有一个 ID 时充当 --model,有多个时充当 --model_list;env_key 指明从哪个变量读取 key;文件里不放任何密钥。明确传入的一切都优先,所以 --provider nvidia --model 会保留那个模型。未知的名字会报错,并列出两个文件。
OrcaRouter
bbook_maker --book_name book.epub --model orcarouter --language ja--model orcarouter 把运行交给 OrcaRouter 网关,并请求它的智能路由模型 orcarouter/auto。它不需要 --api_base,你自己传的会优先。key 先从 BBM_ORCAROUTER_API_KEY 读取,然后才是通常的备用变量。这是一条受支持的路线,而不是旧别名,所以不会被改写。要在网关上固定一个模型,就像其他端点一样写明:--api_base https://api.orcarouter.ai/v1 --model 。
OpenAI 兼容端点
下面这些都是同一条路线,只是 --api_base 不同。结构化输出、--use_context、并行 worker、异步和 Batch API 在它们上面都可用,程度取决于端点本身的支持——支持情况在运行时探测,而不是根据模型名假定。
--use_context session 还要求端点对缓存的提示 token 收费更低。注意进度条上的 cached= 计数:十几个请求之后仍为零,说明端点没有缓存,窗口模式更便宜。
| 厂商 | --api_base |
|---|---|
| OpenAI | https://api.openai.com/v1(默认) |
| Groq | https://api.groq.com/openai/v1,或 --api_format groq |
| xAI | https://api.x.ai/v1,或 --api_format xai |
| Gemini(兼容模式) | https://generativelanguage.googleapis.com/v1beta/openai/ |
| 阿里 Qwen(DashScope) | https://dashscope.aliyuncs.com/compatible-mode/v1 |
| SiliconFlow | https://api.siliconflow.cn/v1 |
| OpenRouter | https://openrouter.ai/api/v1 |
| Azure OpenAI | 部署的 OpenAI 兼容 URL;--model 写部署名 |
| Ollama | http://localhost:11434/v1 |
| vLLM / LM Studio / llama.cpp | 它们提供服务的任何主机 |
bbook_maker --book_name book.epub \
--api_base https://api.groq.com/openai/v1 --key gsk-... \
--model llama-3.3-70b-versatile在这条路线上,--extra_body 用来传递厂商特有的请求字段。
Anthropic
bbook_maker --book_name book.epub \
--api_base https://api.anthropic.com --key sk-ant-... \
--model claude-sonnet-4-6 --language zh-hans端点提供的任何模型 ID 都接受。Claude 每次运行只用一个模型,所以多出来的 --model_list 条目会被说明并忽略,而不是悄悄丢掉。从 OpenAI 风格的 /v1 base 提供 anthropic 形式的网关也能处理:末尾的 /v1 会被去掉,因为 SDK 会自己加上。
通过这种格式分类时使用提示词档位——不会要求端点编译 schema。
翻译服务
这些引擎说自己的协议,不接受模型,所以指定模型会报错,而不是悄悄不起作用。
--api_format |
凭据 |
|---|---|
google |
无 |
deeplfree |
无 |
tencent |
无 |
customapi |
无;端点 URL 写在 --api_base |
caiyun |
必填 |
deepl |
必填(RapidAPI DeepL Translator) |
它们只翻译文本,别的什么都不做:没有上下文窗口,也没有结构化输出。它们自己无法为计划模式给 EPUB 分类;用 --classify-model 指定一个能分类的模型,EPUB 在这些引擎上也能用计划模式。--source_lang 会传给 customapi(写进请求体);其他引擎自己检测源语言。(在大模型路线上,这个参数进入提示词——见下文的“语言”。)
语言
--language LANGUAGE 设置目标语言,默认 zh-hans。它接受标签(zh-hant)、名称("Traditional Chinese"),或者对于内置表里没有的语言,两者一起写——--language "zh-hant:Traditional Chinese"。标签部分是机械用途:标在插入的标记和 dc:language 上,记录在翻译元数据里,并作为结构化输出字段的名字。名称部分是提示词里向模型提要求时的说法。单独的标签或名称照旧通过表格解析;不匹配任何标签的值仍然可以运行,启动时打印一条 Note:,不标注任何东西。完整的表:语言标签与名称。
bbook_maker --help
bbook_maker --book_name book.epub --api_format google --language ja--source_lang 直接指定源语言,而不是自动检测;一旦指定,它会进入每条大模型路线的提示词,在 qwen/customapi 上还会写进请求体。默认值 auto 什么都不指定。并不是每个端点都支持解析器接受的每一种语言。
Zh Prompt (`docs/zh/prompt.md`)
调整提示词
要调整提示词,请使用 --prompt 参数。user 模板可用的占位符有 {text}(必填)、{language} 和 {crlf}(换行符,供无法携带换行的形式使用)。花括号里的其他任何内容都会在运行开始前被拒绝;要写字面的花括号,请用 {{ 和 }}。配置提示词有以下几种方式:
如果不需要设置
system角色的内容,可以简单地这样写:--prompt "Translate {text} to {language}."或--prompt prompt_template_sample.txttext# prompt_template_sample.txt Translate the given text to {language}. Be faithful or accurate in translation. Make the translation readable or intelligible. Be elegant or natural in translation. If the text cannot be translated, return the original text as is. Do not translate person's name. Do not add any additional text in the translation. The text to be translated is: {text}如果需要设置
system角色的内容,可以使用以下格式:--prompt '{"user":"Translate {text} to {language}", "system": "You are a professional translator."}'或--prompt prompt_template.jsontext# prompt_template.json { "system": "You are a professional book translator. Translate the given paragraphs into {language} and be accurate, faithful, and fluent. Return translated {language} text only.", "user": "Translate the following into {language}. Text:{crlf}{crlf}{text}", "style": "" }第三个键
style是关于怎么写的常驻指令。它在窗口开始时说一次——在 API 路线上随系统消息发出,在 codex 上随线程指令发出——而不是每个请求都重复。随仓库提供的prompt_template.json带有全部三个键,style留空:写进你自己想要的文风,或者就让它空着。--prompt适用于每一条大模型路线,srt 书也不例外:在那里,它的各个部分逐一叠加在字幕加载器自己的提示词之上;替换user模板,就意味着你得自己说明块编号和时间轴必须原样返回。
你也可以通过环境变量设置 user 和 system 角色的提示词:BBM_CHATGPTAPI_USER_MSG_TEMPLATE 和 BBM_CHATGPTAPI_SYS_MSG(gemini 读取 BBM_GEMINIAPI_USER_MSG_TEMPLATE 和 BBM_GEMINIAPI_SYS_MSG)。--prompt 优先于它们全部。OPENAI_API_SYS_MSG 仍会作为后备被读取,但已弃用;--prompt 自己的系统消息优先于它。
.md文件按 PromptDown 的块形式读取。格式是他们定的;读取器是本仓库自己写的,所以不用额外安装包。--prompt prompt_md.prompt.mdtext# Translation Prompt ## System Message You are a professional translator who specializes in accurate translations. ## Conversation **User:** Please translate the following text into {language}: {text}读取三个部分:
## System Message(也接受## Developer Message作为它的另一个名字)、可选的## Style,以及## Conversation,其中第一个**User:**轮次就是user模板。写成| Role | Content |表格的对话会被拒绝——模板里的换行无法在表格单元格中保留。
示例
python3 make_book.py --book_name test_books/animal_farm.epub --prompt prompt_template_sample.txt
# or
python3 make_book.py --book_name test_books/animal_farm.epub --prompt prompt_template.json
# or
python3 make_book.py --book_name test_books/animal_farm.epub --prompt "Please translate \`{text}\` to {language}"Zh Providers (`docs/zh/providers.md`)
提供方文件与额外模型
一次运行通过一个端点翻译。有两类步骤可以使用第二个端点:
- 分类。 计划模式会问模型,EPUB 里哪几种块值得翻译。默认问的就是翻译模型。你可以用
--classify-model另外指定一个。 - 查看页面图像的步骤。 目前只有一个:在 PDF 路线上,视觉模型可以纠正 docling 给页面各区域判定的角色。只有用
--img-model指定了模型时才会运行。
两者都可以在命令行或提供方条目里指定。本页介绍提供方文件、这两个额外模型,以及哪个 key 发往哪里。
提供方文件
--provider NAME 从 JSON 文件而不是参数中读取路线。运行会读三个文件,同名时后面的优先:
bbm_providers.example.json,随make_book.py一起提供。只有某个名字在另外两个文件里都没有时,运行才会用它,而且每次都会说明,并给出所用的地址和 key 变量。其中的FILL-ME条目是模板,永远不会被使用。~/.bbm/providers.json。- 运行目录下的
bbm_providers.json。
{
"providers": {
"siliconflow": {
"api_style": "openai",
"base_url": "https://api.siliconflow.cn/v1",
"default_models": ["Qwen/Qwen2.5-72B-Instruct"],
"env_key": "BBM_SILICONFLOW_API_KEY"
}
}
}bbook_maker \
--book_name my_book.epub \
--provider siliconflow \
--use_context session你自己传入的参数优先于条目。
字段
| 字段 | 必填 | 含义 |
|---|---|---|
api_style |
是 | openai、anthropic、gemini、qwen、groq、xai 或 litellm |
base_url |
否 | 地址;默认是该格式自己的地址 |
default_models |
否 | 要用的模型;不传 --model 时必填 |
env_key |
否 | 存放 key 的环境变量;不传 --key 时必填 |
prices |
否 | 每百万 token 的价格,按模型分列:{"<model>": {"input": …, "output": …, "cached_input": …}}。本次运行的每个模型都有价格时,进度条显示已花的钱(spent=$0.012),而不是 token 数 |
currency |
否 | 默认 USD;EUR、GBP、CNY、JPY 会带各自的符号打印 |
img_model |
否 | 页面图像步骤所用的视觉模型。相当于 --img-model |
img_base_url |
否 | img_model 不在本条目自己的地址上时,它所在的地址。仅限 OpenAI 兼容 |
img_env_key |
否 | 存放 img_model 所在地址的 key 的变量 |
classify_model |
否 | 分类用的模型。相当于 --classify-model |
classify_base_url |
否 | classify_model 不在本条目自己的地址上时,它所在的地址。OpenAI 兼容地址,或 Jev 兼容分类器的 URL |
classify_env_key |
否 | 存放 classify_model 所在地址的 key 的变量 |
花费金额是按每个请求报告的用量估算的;以厂商的账单为准。
随附的 `openai` 条目会开启图像步骤
示例文件的 openai 条目设置了 "img_model": "gpt-6-luna"。所以在 PDF 上用 --to-epub 加 --provider openai,默认会对每一页运行区域角色校正。即使你没有 bbm_providers.json 也一样,因为运行会退回使用示例文件。这一步每页约花 3,000 个提示 token:在它背后的那项研究中,12 页共用了 43,443 个提示 token 和 4,319 个补全 token(用图像模型判定区域角色)。不想要这一步,就传 --img-model none,或者把文件复制一份并删掉那一行。
openai-jev 条目就是 openai 条目加上 "classify_model": "jev" 和 JEV_API_KEY,所以 --provider openai-jev 用 gpt-6-luna 翻译,用 Jev 给 EPUB 的计划分类。jev 条目只有 Jev:它只分类、从不翻译,所以 --provider jev 会被拒绝,并提示改用 --classify-model。其他条目都没有写图像模型或分类模型。
两个额外模型
各模型从哪里来
| 步骤 | 首先 | 其次 | 最后 |
|---|---|---|---|
图像(--img-model) |
参数 | 条目的 img_model |
关闭 |
分类(--classify-model) |
参数 | 条目的 classify_model |
本次运行自己的模型 |
图像步骤绝不会退回使用翻译模型。只有通过参数或条目为它指定了模型才会运行,所以没有哪个默认设置需要能读图的模型。--img-model none 在单次运行中关闭条目里的图像模型。
--plan-classify-model 是 --classify-model 的旧名。它仍然可用,只是不出现在 --help 里。两个都写时,--classify-model 优先。
各模型在哪里被询问
- 没有 base URL 时,模型在本次运行自己的端点上被询问,用本次运行的格式和 key。
- 图像模型要求该端点是 OpenAI 形式。其他任何格式下,运行在开始前就会停下:
--img-model needs an OpenAI-compatible endpoint; … resolves to the … format. - 分类模型在任何能对话的大模型路线上都能用,anthropic 和 codex 路线也包括在内。
- 在翻译服务路线上,没有可以共用端点的模型。这时不带 base 指定的分类模型,会在它的 ID 所暗示的主机上被询问:
--classify-model gpt-6-luna发往 OpenAI。
- 图像模型要求该端点是 OpenAI 形式。其他任何格式下,运行在开始前就会停下:
- 有
--img-base-url或--classify-base-url时,模型在那里被询问。该地址必须说 OpenAI 形式,对分类器来说也可以是 Jev 协议;其他任何情况都会在花钱之前被拒绝。 - 只写 base URL 而不写对应模型,运行会停下:
--img-base-url names where --img-model is served, and no --img-model was given.(--classify-base-url同理)。
指定的分类模型总有自己的客户端,即使在本次运行的地址上也是如此,所以它的 token 与翻译的 token 分开计数。
图像步骤运行之前,会先检查一次模型能否读图。不能的话,运行会说 … did not read the probe image (…); image steps are skipped this run.,然后跳过这一步继续。
哪个 key 发往哪里
key 与地址绑定,永远不会发往它不该去的主机。对图像模型和分类模型,key 按以下顺序查找:
--img-key或--classify-key。- 本次运行的 key,但仅当模型在本次运行自己的地址上被询问时。本次运行的地址是应用
--api_base之后的那个地址。 - 条目的
img_env_key或classify_env_key,但仅限该条目为模型指定的地址:它的img_base_url或classify_base_url,否则是条目的base_url。--api_base把运行移到别处时,不会把条目的 key 一起带过去。 - 该地址所属格式读取的变量,与本次运行相同(见 API key)。
以上都没有 key 时,运行会停下,并说出应该传哪个参数。
--extra_body 和 --extra_headers 只有在图像模型或分类模型在本次运行自己的地址上被询问时才会带给它们。请求头常常带着网关的凭据,所以绝不会发往其他主机。
示例
在本地模型上翻译,让托管模型给计划分类并读取页面图像:
{
"providers": {
"local-with-helpers": {
"api_style": "openai",
"base_url": "http://localhost:11434/v1",
"default_models": ["qwen3:8b"],
"classify_model": "gpt-6-luna",
"classify_base_url": "https://api.openai.com/v1",
"classify_env_key": "OPENAI_API_KEY",
"img_model": "gpt-6-luna",
"img_base_url": "https://api.openai.com/v1",
"img_env_key": "OPENAI_API_KEY"
}
}
}用参数写是这样:
bbook_maker \
--book_name my_book.epub \
--api_base http://localhost:11434/v1 \
--model qwen3:8b \
--classify-model gpt-6-luna \
--classify-base-url https://api.openai.com/v1 \
--classify-key "$OPENAI_API_KEY" \
--use_context session本地服务器不需要 key,所以这里本次运行的 key 是空的;分类器的 key 必须指定。
Jev 与 Jev 兼容分类器
Jev 是 TypeSafe 的分类器:一个专门回答类型化问题、而不是写文字的模型。计划模式对每种块提的问题——翻译还是保留——正是这类问题,Jev 能在一次廉价的往返中回答一整页。它什么都不翻译,所以只能充当分类模型。TypeSafe 前面的网关(Vercel 的,或把 TypeSafe 设为自定义提供方的 Cloudflare AI Gateway)说同一套协议。
命令
| 分类器 | 参数 | key |
|---|---|---|
| TypeSafe 的 Jev | --classify-model jev |
JEV_API_KEY 或 TYPESAFE_API_KEY,只发往 api.typesafe.ai |
| 经网关访问 Jev | --classify-model typesafe-ai/jev --classify-base-url https://ai-gateway.vercel.sh/typesafe --classify-key "$GATEWAY_KEY" |
用 --classify-key 指定 |
| 经 Cloudflare AI Gateway 访问 Jev | --classify-model jev --classify-base-url https://gateway.ai.cloudflare.com/v1/$ACCOUNT_ID/$GATEWAY_ID/custom-typesafe --classify-key "$JEV_API_KEY" |
用 --classify-key 指定;网关开启认证时用 CF_AIG_TOKEN,此时网关也可以代存 key |
例如,用 gpt-6-luna 翻译、用 Jev 分类:
bbook_maker \
--book_name my_book.epub \
--model gpt-6-luna \
--classify-model jev规则
- key 只为它自己的主机从环境变量读取。
JEV_API_KEY和TYPESAFE_API_KEY只发往 typesafe.ai 地址。其他任何地方,包括网关,都要用--classify-key或条目的classify_env_key指定 key。 - Cloudflare 网关的 token 只为 Cloudflare 读取。
CF_AIG_TOKEN只以cf-aig-authorization发往 gateway.ai.cloudflare.com。设置了 token 又没有指定 key 时,不发送 key:由网关提供它保存的 key。在网关上,TypeSafe 是一个 base URL 为https://api.typesafe.ai的自定义提供方。 - 已经以
/systemone结尾的 base URL 原样使用。 其他 Jev base 会在/v1之后加上/systemone。 - 只写
jev时,请求的是 TypeSafe 当前的模型jev-latest。
闸门:没把握的跳过按翻译处理
Jev 的每个回答都带一个概率。低于闸门的 skip 记为 translate,所以不会因为 Jev 没把握的跳过而丢失内容。translate 不论概率多少都会采纳。闸门是所选回答的概率 0.95,这是在 45 本 EPUB 的 662 个计划签名上对照 gpt-5.6-luna 测出来的。取这个值时,Jev 的跳过约有十分之九变成 translate,Jev 仍然跳过的都是附属内容:版权行、行号、注释标记、索引页码。所以在这个语料上,Jev 比全部翻译省不了多少;见用 Jev 做计划分类器。计划文件会在每个回退行上注明:unnamed (jev verdict skip at confidence 0.61, below the gate: translate)。--classify-min-confidence P(0 到 1)为单次运行调整闸门;BBM_JEV_MIN_CONFIDENCE 不用参数也能做到同样的事,两者都有时以参数为准。值越低,保留的 Jev 跳过越多:在测过的语料上,低于 0.9 的跳过与参照结果一致的比例只有 7–55%,所以风险由你承担。
写在提供方条目里
如果你想让 OpenAI 翻译、Jev 分类,随附的 openai-jev 条目已经做到了:--provider openai-jev。其他任何组合,请自己写条目。
classify_model、classify_base_url 和 classify_env_key 指定 Jev 分类器的方式与参数相同:
{
"providers": {
"openai-with-jev": {
"api_style": "openai",
"default_models": ["gpt-6-luna"],
"env_key": "OPENAI_API_KEY",
"classify_model": "typesafe-ai/jev",
"classify_base_url": "https://ai-gateway.vercel.sh/typesafe",
"classify_env_key": "JEV_API_KEY"
}
}
}为自己的网关地址写明 JEV_API_KEY 的条目,就是为那个地址指定了这个 key,会被遵从。没有这样的条目,Jev 的变量永远不会发往网关。
每个 Jev 请求把页面的候选行发送一次,每个签名附一个简短的问题。见用 Jev 做计划分类器。
运行会打印什么
--plan-dry-run 不需要 key,就能显示每个模型会在哪里被询问:
Classifier: gpt-6-luna at the openai endpoint's default host (cli)
Image model: off括号里是这个选择的来源:cli、provider 或 run。在 --plan-classify all 或 agent 下,这一行是 Classifier: none (--plan-classify agent asks nothing)。
运行结束时,拥有自己客户端的模型会在翻译用量那一行下面打印自己的用量:
Classifier (gpt-6-luna at the endpoint's default host): tokens: in 7.6k, out 1.8k, cached 0 (3 requests)图像模型的那一行是 Image model (<model> at <address>): …,在提取之后打印。
以下警告表示某个参数在本次运行中不起作用:
--img-model, --img-base-url and --img-key choose the vision model for the steps that look at a page image, and only the PDF route (--to-epub on a PDF) has one; …你指定了图像模型,但这本书不是走--to-epub路线的 PDF。--classify-model names a classifier, and --plan-classify all translates the whole partition without classifying anything; it is ignored this run.--plan-classify agent也一样:智能体模式把每一行都留给你的智能体,不问任何模型。Nothing on this route classifies yet, so --classify-model is ignored on a … book.目前只有 EPUB 有分类步骤。
Zh Session Grouping Eval (`docs/zh/session_grouping_eval.md`)
这份报告已拆分为每次请求的单元数与 token 数和会话压缩预算两页。
En Evaluation Grouping Batch Size (`docs/en/evaluation/grouping-batch-size.md`)
Units and tokens per request: `--max-batch-units`, `--accumulated_num`
Abstract
Plan mode puts several units (paragraphs, verse lines, cells) into one request. Two limits bound a request: a unit cap (--max-batch-units) and a token budget (--accumulated_num). Two paid studies asked where requests start to go wrong. A 210-request study across five endpoints found every request-level fault in its 50-unit bucket. A 923-request sweep found the first content fault at 64 effective units per request, on literary prose only, and no fault anywhere across budgets of 1600 to 4800 tokens. The shipped defaults sit well below both: 16 units and a 1200 to 1600 token budget, halved on endpoints that do not verify a strict JSON schema. These defaults are an owner-set safety margin, not a measured optimum. The measurements say larger requests are cheaper per translated word; the owner chose margin over that saving.
Setup
The degradation study (September 4, 2026). 210 requests, 5 arms, request sizes of 500, 1000, 2000, 4000 and 8000 characters, up to 50 units per request. The batches were byte-identical across arms at each size, built by the branch's own planner from childrens-literature, moby-dick, jlreq-in-english and mahabharata. Arms: gpt-4o-mini and gpt-5.6-luna on OpenAI's endpoint, luna and claude-sonnet-5 through a reseller, and deepseek-v4-flash through OpenRouter. Faults were judged by fixed rules: lazy tail compression, truncation, digit preservation.
The fault-emergence sweep (September 5, 2026). 76 valid cells and 923 requests on the Codex route: 46 specified cells plus a 30-cell extension to 64, 96 and 128 units once the route's own halving was found to cap the grid at 24 effective units. Four epub-samples books: childrens-literature (literary prose), moby-dick-mo (a long novel), epub30-spec (short technical units) and wasteland (verse). Two models: gpt-5.4-mini and the Codex default. Every cell was read back from the produced EPUB. A content fault is a translation in the wrong slot, a missing one, or marker residue in the text.
A weak-model rerun of both axes on gpt-4o-mini and deepseek-chat. It is known from the September 2026 grouping report and its two figures below; its per-cell table was not kept.
Results
The degradation study, largest clean request size per arm:
| arm | largest clean rung | notes after correction |
|---|---|---|
| off-mini (official 4o-mini) | 1000 | real faults at 2000+ (below) |
| off-luna (official luna) | 8000, all clean | its rule-4 trips are zh-numeral false positives |
| rtr-luna (nianhuaapi luna) | 8000, all clean | only delta vs official: 2.4–6× latency |
| rtr-sonnet (nianhuaapi claude-sonnet-5) | none | 0/30 schema-valid JSON — wire format, not translation |
| or-deepseek (OpenRouter deepseek-v4-flash) | ~4000 | + source-echo fault (below), 6.6× completion-token burn, slowest |
- Lazy tail compression never tripped (worst 88% against a 70% threshold). Zero truncation in 210 requests. 36 of 39 unmatched digit runs were correct Chinese numerals.
- Every request-level fault outside rtr-sonnet was in the 50-unit bucket. Unit count, not character budget, was the binding axis.
- One silent tail shift:
gpt-4o-mini, 8000 characters, 50 alternating quote/citation units. The ids came back in order but the translations of roughly slots 44 to 49 were shifted by one. The id echo cannot catch this shape. - The id echo caught what it was built for: a 50-unit mahabharata batch came back with ids 0 to 21 and 28 paragraphs missing, reported as a success by the endpoint.
The fault-emergence sweep:
- First content fault at 64 effective units per request, childrens-literature prose only, 9.4% of units. moby, spec and wasteland were clean through 64. The Codex default model's 7.8% at 24 units was a single misaligned request, clean at 32, 48 and 64.
- Content per request, not unit count, carries the risk. 4705 source tokens in 48 units were clean; 3563 tokens in 64 units faulted; 64 units of short lines (600 to 1100 tokens) were clean.
- The token budget was fault-free across 1600 to 4800. What rises past about 2000 is retry cost: +150% request overhead at 2400 with large unit counts.
- Misalignment is U-shaped: two-unit batches misaligned on 3 of 4 books (harmless; the ladder lands on single units), the middle was mostly silent, and at 64 the retry misaligned again.
- The sweep found a real corruption path: a one-slot shift that survived reconciliation put three literal
⟦spanN⟧tokens into visible prose. It is fixed: such a batch now goes down the halving ladder instead of being written.
Figure: fault rate and retry cost against units and budget, 76 cells, 923 requests. The lines show the defaults of the time (32 units, a 1600 to 2000 budget), before the September 7 ruling.
The weak-model rerun: a raised unit cap costs retries first and faults later, so the cap is the knob to lower and the budget is not.
DeepSeek compressed aggressively at every cap and budget (median zh/en character ratio 0.25 to 0.31, against gpt-4o-mini's 0.31 to 0.35). Short translations on that model are the model, not a grouping fault.
Figures: weak-model rerun, from the September 2026 grouping report; its per-cell table was not kept.
Decision
On September 5 the sweep's lead set the cap to 32 (half the measured emergence) and kept a 2000-token ceiling on retry-cost grounds. On September 7 the owner lowered every grouping default:
| constant | old | new |
|---|---|---|
GENERAL_GROUP_MAX_UNITS (plan.py) |
32 | 16 (quarter of the 64-unit measured fault onset) |
SUBSTRICT_GROUP_MAX_UNITS |
16 (derived //2) |
8 (still derived) |
substrict_batch_cap (chatgptapi) |
16 | 8 |
SESSION_BUDGET_FLOOR / CEILING |
2400 / 3200 | 1200 / 1600 |
SUBSTRICT_BUDGET_FLOOR |
derived 1200 | typed 800 (deliberately NOT floor//2=600 — no double margin) |
The owner's reasons, as recorded: schema support is an endpoint property, not evidence that the model follows instructions; strong models are not necessarily better at format compliance; and a fault means the model cannot hold the request together. A shifted slot is a wrong book, not an expensive one. So a safety margin outranks the measured per-content savings (15.5 input-equivalents per content token at a budget of 800 against 12.3 at 1600).
Why the halving on weaker endpoints: both content regressions the September 5 JSON-object eval found were large batches, and an endpoint without a strict-schema verdict cannot be told apart from one where miscounted replies live. So on such an endpoint the unit cap is 8 and the per-request budget is 800.
What would change it: a rerun of the sweep that shows faults below 16 on some model (lower it), or the owner deciding the margin is worth less than the savings (raise it). Until then, lower --max-batch-units when a run prints misaligned batches this run; raise it only for cost, at your own risk, and never past 48.
Limits
- The sweep's "weak" model was
gpt-5.4-mini, which is not a weak-model proxy. It is unusually strong at format compliance. The sweep's finding that "the weak model held the reply format better than the strong one" (2 recoveries in 9 childrens cells against 5 in 6) is therefore unproven for genuinely weak models. Thegpt-4o-miniand DeepSeek rerun is the weak-model evidence, and its per-cell table was not kept. - Only four books, and only childrens-literature reached prose at 128 units; moby and spec spent their test slice on front matter.
- Nothing between 16 and 64 units was measured on an on-device model.
- The token budget was measured fault-free only from 1600 up, and only at the old 32-unit cap. The shipped 800 to 1600 sits below the measured range on purpose.
En Evaluation Pdf Ocr Engines (`docs/en/evaluation/pdf-ocr-engines.md`)
OCR engines on scanned pages: `--ocr-engine` rapidocr, ocrmac, easyocr, tesseract
Since this measurement, the pdf extra installs onnxruntime, and ocrmac on macOS, so rapidocr and ocrmac download nothing on a fresh install; the install notes below describe the install as it was measured.
Abstract
--ocr-engine picks the engine docling runs on pages with no text layer. The four engines docling can run locally were measured on the same 20 scanned pages (14 English, 6 Chinese), with the same scorer and the same language asked of each, all on the CPU. On English, tesseract had the lowest error (mean CER 0.104) and ocrmac came next (0.136); rapidocr (0.169) and easyocr (0.267) trailed. On simplified Chinese, rapidocr was best (mean Han CER 0.054 over five pages, against ocrmac 0.097 and tesseract 0.116); on the one traditional page ocrmac and rapidocr were close (0.043 and 0.060). easyocr was last almost everywhere, took three times as long and up to 6.6 GB of memory, and dropped text on Chinese pages. ocrmac, Apple's own engine, was the fastest (median 6 s per two pages) and needs nothing downloaded.
The default stays auto. This page is the evidence behind Which OCR engine.
Setup
- Route: the PDF route's own extraction, run through the staged harness (
tools/pdf_to_book.py extract), docling 2.129.0,--pdf-ocr --device cpu --pages 1-2, default OCR mode (a page's embedded layer is kept where docling keeps it). One conversion at a time, each under a memory cap,HF_HUB_OFFLINE=1. - Engines: rapidocr 3.9.2 on onnxruntime 1.23.2 (PP-OCRv6 small models, shipped inside the rapidocr package); ocrmac 1.0.1 (Apple Vision, macOS 26); easyocr 1.7.2 on PyTorch 2.9.0 (CPU); tesseract 5.5.3 (Homebrew) with the
tessdata_fastlanguage files (eng,chi_sim,chi_tra,osd). - Languages: the same
iso:tag for every engine, which docling maps onto each engine's own code:iso:enfor the English pages,iso:zh-Hansfor simplified Chinese,iso:zh-Hantfor traditional. The run's own line confirmed the engine and the languages in every cell. - Device:
--device cpuon every cell, so the layout and table models ran on the CPU for all four arms. rapidocr (onnxruntime), easyocr (PyTorch) and tesseract ran on the CPU; ocrmac runs inside Apple's Vision framework, which chooses its own hardware. - Machine: Apple silicon, 16 GB, Python 3.12.12.
- Pages: two-page cuts; the scored pages are the ones with ground truth:
| fixture | scored page(s) | what it is | language flag |
|---|---|---|---|
| scan_cia_typewriter | 1, 2 | degraded typewriter scan (CCITT) | iso:en |
| scan_cia_typewriter_mid | 2 | typewriter scan, mid-document | iso:en |
| scan_forward1963 | 1 | 1963 magazine, two columns, footnotes | iso:en |
| scan_en_degraded_1_mid | 1 | colour pamphlet with a table | iso:en |
| scan_en_degraded_2_mid | 1 | bitonal old novel page | iso:en |
| scan_zh_hans_qm_mid | 1 | Internet Archive book scan, simplified Chinese, invisible OCR layer, JBIG2 masks | iso:zh-Hans |
| zh_hant_scan_2_mid | 2 | traditional Chinese, horizontal, with a table | iso:zh-Hant |
| syn_arxiv_1706_03762_{clean,deg} | 1, 2 | born-digital paper rendered as a scan | iso:en |
| syn_wps_eula_{clean,deg} | 1, 2 | WPS licence document rendered as a scan | iso:en |
| syn_web_zh_report_chrome_{clean,deg} | 1, 2 | Chinese web report rendered as a scan | iso:zh-Hans |
Synthetic scans: 200 dpi; clean is a lossless PNG, degraded is grayscale, rotated 1.5°, blurred (r=0.8) and JPEG q40. Their ground truth is exact (the source page's text layer). The real scans' ground truth was typed from the page image. The Chinese book scan carries JBIG2 masks, so its page images came from pypdfium2 on every arm (the run printed the JBIG2 line).
- Scorer: Normalisation: Markdown syntax stripped (image links, comments,
#, table pipes and rules,**, escapes, list bullets), HTML unescaped, NFKC, every quote variant mapped to", every hyphen and dash removed, whitespace collapsed (removed entirely for Chinese). CER is order-sensitive: a paragraph read in another order counts as errors. Han CER is CER over the Han characters only. line_err ignores order: the length-weighted mean of (1 − partial match) over ground-truth lines, so it measures recognition and detection alone. The heading the route adds above an extraction that does not open with one (# <file name>) was removed before scoring.
Commands, one per cell ($E is the evaluation directory, $ENGINE one of the four, $LANG from the table above):
HF_HUB_OFFLINE=1 TESSDATA_PREFIX=$E/tessdata \
python tools/pdf_to_book.py extract $E/fixtures/p12/scan_cia_typewriter.pdf \
--output $E/runs/K/$ENGINE_cia/bundle \
--pdf-ocr --ocr-engine $ENGINE --ocr-lang iso:en --device cpu --pages 1-2and the same with --ocr-engine auto on one fixture per script, to see what auto picks.
Results
CER per page, lower is better; Chinese pages also give the Han-only CER:
| page | rapidocr | ocrmac | easyocr | tesseract | auto |
|---|---|---|---|---|---|
| cia p1 | 0.158 | 0.119 | 0.073 | 0.120 | 0.119 |
| cia p2 | 0.272 | 0.109 | 0.148 | 0.110 | 0.109 |
| ciamid p2 | 0.188 | 0.029 | 0.080 | 0.017 | - |
| fwd p1 | 0.251 | 0.231 | 0.260 | 0.212 | - |
| deg1 p1 | 0.355 | 0.360 | 0.965 | 0.046 | - |
| deg2 p1 | 0.543 | 0.491 | 0.602 | 0.408 | - |
| zh p1 | 0.112 (Han 0.009) | 0.210 (Han 0.056) | 0.390 (Han 0.235) | 0.201 (Han 0.036) | 0.210 (Han 0.056) |
| hant2 p2 | 0.069 (Han 0.060) | 0.041 (Han 0.043) | 0.670 (Han 0.724) | 0.322 (Han 0.350) | 0.041 (Han 0.043) |
| synarxc p1 | 0.051 | 0.050 | 0.124 | 0.051 | - |
| synarxc p2 | 0.040 | 0.003 | 0.056 | 0.004 | - |
| synarxd p1 | 0.142 | 0.055 | 0.431 | 0.169 | - |
| synarxd p2 | 0.002 | 0.269 | 0.534 | 0.272 | - |
| syneulac p1 | 0.015 | 0.014 | 0.071 | 0.015 | - |
| syneulac p2 | 0.171 | 0.008 | 0.028 | 0.008 | - |
| syneulad p1 | 0.110 | 0.163 | 0.215 | 0.014 | - |
| syneulad p2 | 0.066 | 0.008 | 0.153 | 0.008 | - |
| synzhc p1 | 0.042 (Han 0.000) | 0.040 (Han 0.003) | 0.564 (Han 0.514) | 0.077 (Han 0.019) | - |
| synzhc p2 | 0.076 (Han 0.053) | 0.045 (Han 0.000) | 0.656 (Han 0.611) | 0.085 (Han 0.021) | - |
| synzhd p1 | 0.188 (Han 0.191) | 0.167 (Han 0.163) | 0.799 (Han 0.862) | 0.193 (Han 0.171) | - |
| synzhd p2 | 0.136 (Han 0.018) | 0.312 (Han 0.262) | 0.800 (Han 0.848) | 0.390 (Han 0.333) | - |
line_err per page (order ignored):
| page | rapidocr | ocrmac | easyocr | tesseract | auto |
|---|---|---|---|---|---|
| cia p1 | 0.085 | 0.001 | 0.045 | 0.003 | 0.001 |
| cia p2 | 0.181 | 0.076 | 0.110 | 0.078 | 0.076 |
| ciamid p2 | 0.093 | 0.007 | 0.036 | 0.000 | - |
| fwd p1 | 0.009 | 0.020 | 0.054 | 0.007 | - |
| deg1 p1 | 0.010 | 0.043 | 0.559 | 0.033 | - |
| deg2 p1 | 0.054 | 0.118 | 0.291 | 0.019 | - |
| zh p1 | 0.036 | 0.144 | 0.142 | 0.119 | 0.144 |
| hant2 p2 | 0.050 | 0.037 | 0.536 | 0.272 | 0.037 |
| synarxc p1 | 0.034 | 0.033 | 0.057 | 0.033 | - |
| synarxc p2 | 0.025 | 0.002 | 0.034 | 0.004 | - |
| synarxd p1 | 0.035 | 0.034 | 0.265 | 0.082 | - |
| synarxd p2 | 0.001 | 0.003 | 0.316 | 0.007 | - |
| syneulac p1 | 0.001 | 0.000 | 0.047 | 0.001 | - |
| syneulac p2 | 0.109 | 0.002 | 0.018 | 0.002 | - |
| syneulad p1 | 0.001 | 0.041 | 0.117 | 0.000 | - |
| syneulad p2 | 0.047 | 0.002 | 0.085 | 0.002 | - |
| synzhc p1 | 0.031 | 0.030 | 0.482 | 0.062 | - |
| synzhc p2 | 0.059 | 0.035 | 0.563 | 0.067 | - |
| synzhd p1 | 0.089 | 0.043 | 0.693 | 0.074 | - |
| synzhd p2 | 0.047 | 0.044 | 0.719 | 0.117 | - |
Means per class, CER (Han CER for Chinese) / line_err:
| class | n | rapidocr | ocrmac | easyocr | tesseract |
|---|---|---|---|---|---|
| typewriter scan, English (cia p1, p2, ciamid p2) | 3 | 0.206 / 0.120 | 0.086 / 0.028 | 0.100 / 0.064 | 0.082 / 0.027 |
| magazine and old print, English (fwd, deg1, deg2) | 3 | 0.383 / 0.024 | 0.361 / 0.060 | 0.609 / 0.301 | 0.222 / 0.020 |
| synthetic scan, English, clean | 4 | 0.069 / 0.042 | 0.019 / 0.009 | 0.070 / 0.039 | 0.019 / 0.010 |
| synthetic scan, English, degraded | 4 | 0.080 / 0.021 | 0.124 / 0.020 | 0.333 / 0.196 | 0.116 / 0.023 |
| book scan, simplified Chinese (zh, JBIG2) | 1 | 0.009 / 0.036 | 0.056 / 0.144 | 0.235 / 0.142 | 0.036 / 0.119 |
| scan, traditional Chinese, horizontal (hant2) | 1 | 0.060 / 0.050 | 0.043 / 0.037 | 0.724 / 0.536 | 0.350 / 0.272 |
| synthetic scan, simplified Chinese, clean | 2 | 0.027 / 0.045 | 0.001 / 0.033 | 0.562 / 0.523 | 0.020 / 0.065 |
| synthetic scan, simplified Chinese, degraded | 2 | 0.105 / 0.068 | 0.213 / 0.044 | 0.855 / 0.706 | 0.252 / 0.096 |
Per engine:
| engine | pages | mean CER, English (n) | mean Han CER, Chinese (n) | mean line_err, English | mean line_err, Chinese | median extraction s per two-page cell | peak memory MB (max) |
|---|---|---|---|---|---|---|---|
| rapidocr | 20 | 0.169 (14) | 0.055 (6) | 0.049 | 0.052 | 12 | 2248 |
| ocrmac | 20 | 0.136 (14) | 0.088 (6) | 0.027 | 0.056 | 6 | 1661 |
| easyocr | 20 | 0.267 (14) | 0.632 (6) | 0.145 | 0.523 | 37 | 6596 |
| tesseract | 20 | 0.104 (14) | 0.155 (6) | 0.019 | 0.119 | 10 | 1902 |
| auto | 4 | 0.114 (2) | 0.050 (2) | 0.039 | 0.090 | 12 | 1977 |
Extraction time is the run's own PDF extracted: 2 pages, …, N s. line: model loading plus layout, tables and OCR for two pages. Per cell: rapidocr 9–19 s, ocrmac 4–11 s, tesseract 7–21 s, easyocr 32–90 s (the 90 s cell includes its first-use model download).
What auto picked: ocrmac in 3 of 3 cells (OCR engine: ocrmac (docling's choice on this install), …), because ocrmac was installed; its text was identical to the ocrmac cells.
Reading order, not recognition, explains several CER gaps. synarxd p2: ocrmac and tesseract CER 0.269 and 0.272 against rapidocr's 0.002, but line_err 0.003 and 0.007: the text is right, a block was placed elsewhere. deg1: rapidocr and ocrmac CER 0.355 and 0.360 with line_err 0.010 and 0.043. For a book, a paragraph in the wrong place is a real defect, so CER is the primary score; line_err says whether the words themselves were read.
easyocr dropped text. On the Chinese synthetic pages it returned 108 to 370 of 753 Han characters; on deg1 it returned 133 characters of 3,518. It is also the only engine over 4 GB of memory: the simplified Chinese cell passed 6,000 MB and was killed by the cap, and the retry at an 8,000 MB cap peaked at 6,596 MB.
Install and first-use download, measured on this machine:
| engine | installed beyond the pdf extra |
downloaded on first use | runs off-line afterwards |
|---|---|---|---|
| rapidocr | nothing: rapidocr 3.9.2 (31 MB) is in the extra, its onnxruntime models are inside the package; onnxruntime (62 MB) is not in the extra | nothing with onnxruntime (no file appeared during the sweep, for iso:en, iso:zh-Hans or iso:zh-Hant); on PyTorch (what auto falls back to without onnxruntime): 9.8 + 0.6 + 20.3 MB of models from modelscope.cn, observed once |
yes |
| ocrmac | ocrmac 1.0.1 and pyobjc (28 MB), macOS only | nothing: Apple's Vision framework is part of macOS | yes |
| easyocr | easyocr 1.7.2 (15 MB) plus opencv-python-headless (98 MB), scikit-image (28 MB), networkx (7 MB) and smaller ones: about 150 MB | the detector craft_mlt_25k.pth (83 MB) always, then one recognizer per script: english_g2.pth 15 MB, latin_g2.pth 15 MB, zh_sim_g2.pth 22 MB, chinese.pth (traditional) 226 MB, downloaded during this sweep. From GitHub, by easyocr itself: HF_HUB_OFFLINE does not stop it |
yes, from ~/.EasyOCR/model |
| tesseract | the tesseract program (Homebrew: 25 MB plus leptonica 7 MB and about 35 libraries it depends on) | nothing: language files are installed by hand, tessdata_fast sizes eng 4.1 MB, chi_sim 2.5 MB, chi_tra 2.4 MB, osd 10.6 MB |
yes |
Decision
- The default stays
auto: docling takes the first installed of ocrmac, rapidocr and easyocr, and names the one it used.--ocr-enginenames one explicitly; an engine that is not installed, or ocrmac off macOS, is refused before any page is read, with the line that installs it. - The recommendations on Which OCR engine follow the class table: tesseract or ocrmac for English (tesseract ahead on old print), rapidocr for simplified Chinese, ocrmac or rapidocr for traditional Chinese, easyocr only when nothing else reads the script.
- The
pdfextra installs ocrmac on macOS, soautopicks it with nothing to download, and installs onnxruntime, so rapidocr runs on its bundled models instead of downloading PyTorch ones.
Limits
- 20 pages, one to four per class; the real Chinese rows are one page each. Differences under about 0.02 CER between two engines on one class are within what a different page would change, so they are called ties. The engines are deterministic: rapidocr reproduced the earlier study's CIA numbers (0.158, 0.272) exactly, and
autoreproduced the ocrmac cells exactly. - Japanese, Korean and other scripts were not measured. Vertical Chinese was not measured here.
- One run per cell; no GPU. On a GPU, easyocr's time would drop; its memory would not.
- rapidocr was measured on onnxruntime. On PyTorch (the
autofallback of an install without onnxruntime) it was not scored: the one check stopped after the downloads. - tesseract needs its
configsfolder beside the language files: withTESSDATA_PREFIXpointing at a folder of.traineddatafiles alone, every tesseract cell failed (KeyError: 'text', tesseract printingread_params_file: Can't open tsv). Linking Homebrew'sconfigsandtessconfigsfolders into it fixed that; the numbers above are from the rerun. - Default OCR mode: on the simplified Chinese scan, whose invisible OCR layer docling reads alongside, the engine's text is mixed with the layer's.
--ocr-replace-layerwas not measured here. - Ground truth for the real scans was typed by one person.
En Evaluation Pdf Structure Llm Roles (`docs/en/evaluation/pdf-structure-llm-roles.md`)
Region roles with an image model: `--img-model`
Abstract
docling cuts each page into regions (paragraph, heading, footnote, code, table, picture and so on) and puts them in reading order. When a region has the wrong role or the wrong order, the book shows it: a code listing printed as footnotes, section 2 before section 1, a figure missing above its caption. The study catalogued docling's faults on 85 pages, then asked a vision model (gpt-5.6-luna) to relabel the regions it could see. The model fixed most wrong labels on born-digital pages. It could not fix what only shows across pages (running headers), and it could not rebuild a page docling had shattered into fragments. Those need rules based on page geometry, run before any model.
The role pass is built: --img-model on the PDF route. The geometry repairs the study calls for are not built yet, so running headers and shattered pages still come through as docling made them.
Setup
- docling 2.129.0 with the text layer only (no OCR), on 43 slices of one or two pages: 85 pages, of which 51 are arXiv, 16 other papers, 4 scans and 14 web or Word pages.
- Catalogue: every page rendered with its regions drawn and tagged, and each fault recorded by hand with a class and a severity (book-breaking, visible, minor).
- Role pass: 12 pages carrying at least one wrong-label or furniture fault. The model saw the tagged overlay and a list of region ids with their current label and first 80 characters, and answered one label per id, or
abstain. A script scored the answers against the catalogue; the model did not judge itself. - Whole-page probe: 3 pages with a missing region. The model saw only the clean page and returned regions with boxes; they were matched to docling's at IoU 0.5 or more.
Results
The catalogue: 123 faults, 13 book-breaking. Copied from the record:
| fault_class | arXiv (26 sl/51 pp) | paper (8 sl/16 pp) | scan (2 sl/4 pp) | web (7 sl/14 pp) | total | book_breaking | visible | minor |
|---|---|---|---|---|---|---|---|---|
| wrong_label | 8 | 11 | 5 | 7 | 31 | 1 | 26 | 4 |
| paragraph_merge_split | 11 | 5 | 5 | 9 | 30 | 8 | 13 | 9 |
| column_order | 6 | 2 | 1 | 3 | 12 | 2 | 8 | 2 |
| heading_level | 0 | 6 | 0 | 5 | 11 | 0 | 1 | 10 |
| furniture_as_body | 2 | 1 | 3 | 4 | 10 | 0 | 8 | 2 |
| spurious_region | 7 | 2 | 0 | 0 | 9 | 0 | 3 | 6 |
| missing_region | 1 | 2 | 0 | 3 | 6 | 2 | 2 | 2 |
| caption_attachment | 2 | 2 | 0 | 2 | 6 | 0 | 4 | 2 |
| duplicate_asset | 1 | 0 | 0 | 3 | 4 | 0 | 0 | 4 |
| misplaced_asset | 1 | 0 | 0 | 1 | 2 | 0 | 2 | 0 |
| body_as_furniture | 0 | 1 | 0 | 1 | 2 | 0 | 1 | 1 |
| all | 39 | 32 | 14 | 38 | 123 | 13 | 68 | 42 |
Wrong labels are the most frequent class but break a book once. Shattered or wrongly merged paragraphs break it 8 times: an embedded OCR layer turned into 165 regions on one page, inline maths split into 49 one-glyph items, a code listing that lost every newline, and docling's cross-page merge gluing a paragraph to an author block.
Missing text is not a usable trigger. 0.00% of text-layer characters fell outside every region on 78 of 85 pages, and at most 1.23% on any page. A missing figure or equation has no text-layer characters to miss. The signs that do work are geometric: an orphan caption, a formula box holding only its number, a sliver picture where a table should be.
The role pass, 12 pages:
| page | regions | label faults | fixed | kept wrong label | changed to another wrong label | abstained | introduced errors | extra correct changes | prompt / completion (reasoning) tokens | latency s |
|---|---|---|---|---|---|---|---|---|---|---|
| arxiv_1906_00495v1_p34 p4 | 17 | 3 | 1 | 2 | 0 | 0 | 0 | 0 | 3442 / 239 (157) | 5.6 |
| arxiv_2009_12164v1_p12 p1 | 13 | 5 | 5 | 0 | 0 | 0 | 0 | 1 | 3346 / 199 (130) | 5.7 |
| arxiv_2010_03667v1_p12 p1 | 20 | 4 | 4 | 0 | 0 | 0 | 0 | 1 | 3431 / 351 (254) | 7.6 |
| latexbook_p12 p2 | 11 | 6 | 2 | 4 | 0 | 0 | 1 (13:text->key_value_region) | 1 | 3190 / 520 (460) | 9.0 |
| mixed_photo_code_1_p3031 p1 | 17 | 9 | 9 | 0 | 0 | 0 | 0 | 0 | 2958 / 165 (84) | 6.0 |
| mixed_photo_code_1_p3031 p2 | 9 | 1 | 1 | 0 | 0 | 0 | 0 | 0 | 2739 / 150 (100) | 5.3 |
| statement_p12 p1 | 24 | 4 | 1 | 2 | 1 | 0 | 1 (18:text->key_value_region) | 1 | 3555 / 625 (512) | 6.0 |
| statement_p12 p2 | 9 | 5 | 0 | 5 | 0 | 0 | 0 | 0 | 3145 / 143 (94) | 3.5 |
| web_en_chrome_1_p12 p1 | 21 | 15 | 15 | 0 | 0 | 0 | 0 | 1 | 3308 / 619 (512) | 10.6 |
| word_en_table_1_mid p1 | 16 | 1 | 0 | 1 | 0 | 0 | 0 | 0 | 3451 / 265 (186) | 6.9 |
| word_en_table_1_p12 p2 | 5 | 2 | 2 | 0 | 0 | 0 | 0 | 0 | 3018 / 29 (0) | 2.7 |
| zhqm_mid p1 | 164 | 11 | 0 | 9 | 2 | 0 | 0 | 0 | 7860 / 1014 (345) | 9.5 |
| total (12 pages) | 66 | 40 | 23 | 3 | 0 | 2 | 5 | 43443 / 4319 | 78.4 |
Catalogued label faults on the 12 pages from the table above, split by what the model did. 66 faults in all.
- Fixed 40 of 66. Leaving out the bank-statement pages and the OCR scan, 39 of 46.
- Running furniture: 0 of 8. A bank statement's repeated header and regulatory block look like body text on a single page. Only their repetition across pages marks them.
- A shattered OCR page: 0 of 11. Relabeling fragments does not rebuild a page.
- 2 errors introduced, both
text -> key_value_region, low harm. 5 extra changes were correct (the document title). The model never abstained, not even on the 164-region page. - Cost: 43,443 prompt and 4,319 completion tokens for 12 pages, 2.7 to 10.6 s per page.
Whole-page probe, 3 pages: the model found all 3 missing regions with the right label (a figure, an equation with its number, a census table). Its boxes matched docling's on 38 of 45 regions, with mean IoU 0.82 to 0.90, so a proposed box has to be snapped to the page's own glyph and image bounds before anything is cropped from it.
Recognition, deferred. Tables: docling's cell F1 0.888 against the model's 0.854 on 17 tables, where 7 ground truths equal docling's output by construction. docling's formula decoding ran 27 minutes on two pages without output and was killed.
Decision
The plan that came out of this study:
- Geometry repair runs first and is not a model's job: shattered lines and paragraphs, the cross-page merge, one-glyph slivers, code newlines, and running furniture found by repetition across pages.
- Then a role pass over the cleaned region ids. This is the first model step, because it is the cheapest seam and the only measured win.
- A whole-page proposal for missing regions, only on a page one of the three geometric triggers flags, with the box snapped and checked.
- Reading order by a model is untested and stays a candidate. Table and formula recognition stay deferred.
A design consult (Codex, recorded) set the constraints for step 2:
- a thin image adapter on the OpenAI-style route, reusing the existing structured-output ladder;
- a role change is a typed replacement of the item, never a bare label write;
- furniture, tables, pictures and lists are out of the first pass;
- accepted changes apply atomically; a page with an unusual change rate is quarantined, not trimmed (changed after the first real runs, below);
- every decision is written to
decisions.jsonfor audit; - off by default.
What was built. Step 2, the role pass, ahead of the geometry repairs. The model sees the page rendered with docling's regions tagged, and answers one label per eligible region (text, section_header, title, caption, footnote, code, or abstain) under a strict per-region schema. Eligible regions are body text, titles and headings that no caption or footnote refers to, outside table cells and formulas. Lists, formulas, tables, pictures and furniture are not asked about. Accepted answers replace the item with one of the right type, before the formula and heading steps run.
The first real runs, gpt-5.6-luna on pages 1-2 of four fixtures, copied from the record:
| fixture | asked | accepted (applied) | kept | rejected | quarantined pages | calls | prompt / completion tokens | seconds |
|---|---|---|---|---|---|---|---|---|
| arxiv_2010_03667v1 | 27 | 6 | 21 | 0 | 0 | 2 | 7001 / 368 | 9.9 |
| web_en_chrome_1 | 28 | 4 | 24 | 0 | 0 | 2 | 6930 / 365 | 8.8 |
| mixed_photo_code_1_p3031 | 15 | 1 (9 more quarantined) | 5 | 0 | 1 | 2 | 5538 / 177 | 6.2 |
| arxiv_1706_03762 (held-out) | 21 | 2 | 19 | 0 | 0 | 2 | 6645 / 322 | 7.8 |
The quarantine blocked the one book-breaking fix it met: on the code page the model was right on 9 of 10 answers, which crossed the 60% change threshold. So the owner dropped the quarantine. A page where more than 60% of the asked items change now keeps its changes and prints Region roles: … on page N changed; read that page in source.md before translating. Rerun without the quarantine, the eight listing lines export as code (eight separate code blocks, since adjacent code items are not merged yet). The held-out paper came out with 0 changed lines of Markdown.
The image model is chosen explicitly. The pass runs only on a model you name, with --img-model or a provider entry's img_model. The translating model is never used for images by fallback (owner ruling), so no default needs a model that reads images. The shipped openai provider entry names gpt-5.6-luna, so --provider openai turns the pass on; --img-model none turns it off. Before the pass, a one-image probe checks that the model can see a picture; if it cannot, docling's labels stand.
On-device models: about 3,000 prompt tokens per page is modest, but the 164-region page took 7,860, which is beyond an 8B-class model. Point the pass at a hosted vision model with --img-base-url, or leave it off.
Limits
- The role pass is measured on 12 pages and the whole-page probe on 3. The class-to-fix table in the record is a direction, not a rate.
- The catalogue was made by one worker from overlays; heading levels on arXiv were not judged (a separate study covers them).
- Only one model (gpt-5.6-luna) was run. No on-device model was tried.
- The built pass was checked on pages 1-2 of four fixtures; label-only changes such as text to footnote change nothing in the export yet.
- No OCR engine ran in the catalogue; the two scans were read through their embedded layers.
En Evaluation Plan Classifier Jev (`docs/en/evaluation/plan-classifier-jev.md`)
Jev as the plan classifier: `--classify-model`, measured against gpt-5.6-luna
Abstract
Plan mode asks a model, for each kind of block in an EPUB, whether it is worth translating. That is a labeling job, not a writing job, so a dedicated classifier could do it. Jev, TypeSafe's System One classifier, answers typed multiple-choice questions with a confidence. The same 31 signatures of the test book were classified by Jev and by gpt-5.6-luna. They agreed on 27. All 4 disagreements were Jev choosing skip where luna chose translate, each at low confidence. Jev used about twice luna's prompt tokens. A second round sent each page's shared text once: Jev's prompt tokens halved, and its agreement with luna moved no more than between two Jev runs of the same code. The same round found that a gate at 0.5 on Jev's probability could never fire on a two-option question; the gate was then measured on a 45-EPUB corpus and set to 0.95. Verdict: Jev works as a classify model; the default stays the translating model.
Setup
- Book:
test_books/animal_farm.epub, the repository's test book. Its plan has 31 tag signatures. - Run:
--test --test_num 8 --quiet --language zh-hans, translating with gpt-5.6-luna, under a 1500 MB memory cap. - Luna arm:
--model gpt-5.6-luna --classify-model gpt-5.6-luna. The endpoint verifies a strict JSON schema, so the signatures go in pages of several per request. - Jev arm:
--model gpt-5.6-luna --classify-model typesafe-ai/jev --classify-base-url https://ai-gateway.vercel.sh/typesafe. One Choice question per signature. In this first round, below 0.5 confidence in its top answer, Jev answeredunsure. - Fixed-engine cell:
--api_format google --classify-model gpt-5.6-luna, to check that a translation-service route gets a plan from an LLM classifier. - Compared: each signature's verdict between the two arms. Nobody judged which verdict was right.
Results
Copied from the record:
| cell | classification | classifier usage line |
|---|---|---|
| luna | 31 signatures in 3 requests, 4 skips | Classifier (gpt-5.6-luna at the endpoint's default host): tokens: in 7.6k, out 1.8k, cached 0 (3 requests) |
| Jev | one 503 retry, then 31 verdicts, 8 skips | Classifier (typesafe-ai/jev at https://ai-gateway.vercel.sh/typesafe): tokens: in 14.4k, out 1.1k, cached 0 (3 requests) |
27 of 31 agree. All 4 disagreements are Jev choosing skip where luna chose translate, and all 4 were Jev's low-confidence calls (0.58–0.61):
| Signature | Sample text |
|---|---|
inline:span.underline |
www.ericseat.com |
inline:span.calibre_4 |
www.ericseat.com |
inline:span.calibre_16 |
"M" |
inline:span.calibre3 |
George Orwell |
Where the two agree, Jev's confidence is mostly above 0.9. It used about twice luna's prompt tokens (14.4k vs 7.6k), since each signature is sent as its own question.
Fixed engine with an LLM classifier. Google Translate with gpt-5.6-luna as the classifier: llm classification: 31 verdict(s), 4 skip(s), Translation plan: … coverage 99.8%, and the classifier's line Classifier (gpt-5.6-luna …): tokens: in 7.6k, out 1.6k (3 requests). The translations sat beside their originals and matched them (George Orwell → 乔治·奥威尔).
A second Jev run, after the key-routing fixes, retried three 503s and ended with llm classification: 31 verdict(s), 8 skip(s) and Translation plan: 20 documents, 202560 chars, coverage 99.7%.
Second round: lean requests and the gate
Each Jev question used to repeat the whole per-signature prompt. The lean mapping sends the page's shared text once as the state, and each question carries only its own instruction. A doubtful skip now falls back to translate instead of unsure. Same book, same flags. Copied from the record:
| arm | classifier tokens in / out | requests, latency each | non-translate verdicts | agreement with c_final (luna) | agreement with c_before (luna) |
|---|---|---|---|---|---|
| (a) before, F's mapping, gateway | 14.4k / 1.1k | 3, after four 503 retries (wall 36 s) | 7 | 27/31 | 26/31 |
(a) after lean mapping only (a_lean) |
7.8k / 1.1k | 3 (wall 14 s) | 9 | 25/31 | not computed |
| (a) final, lean + gate | 7.8k / 1.1k | 3: 0.87, 0.37, 0.67 s | 9 | 25/31 | 26/31 |
(b) luna schema (c_final) |
7.6k / ~1.4k | 3: 5.07, 6.03, 4.15 s | 5 | — | 30/31 |
The two luna runs agree 30/31 with each other, and the two Jev runs on the same code (a_lean, a_final) agree 29/31.
The lean mapping halves Jev's prompt tokens (14.4k → 7.8k) and cuts wall time (36 s → 14 s incl. no retries); agreement did not move beyond Jev's own run-to-run variation.
The gate finding, copied from the record:
The official Jev's
confidencefield is not the chosen option's probability. On two options it is about2p − 1(0.51 → 0.02, 0.61 → 0.22, 0.76 → 0.53). The code keeps F's choice and readsprobabilities[choice]first. On two options it never falls below 0.5, so a 0.5 gate does nothing.Replaying
a_finaloffline: a gate at 0.75 on the probability, which is the same as 0.5 on Jev's ownconfidence, would turn all 5 low-confidence skips into translate and keep the 4 confident ones. Agreement with luna would go to 30/31. That is one book and 31 signatures.
That was one book and 31 signatures, so 0.75 was a hint, not a setting. The gate was then measured on a corpus: 0.95 on the chosen option's probability, over the 662 plan signatures of the 45-EPUB corpus against gpt-5.6-luna, with a lost skip costing 10 and an extra translate 1. At that value about nine of ten of Jev's skips fall back to translate, and what survives is apparatus: copyright lines, line numbers, note marks, index locators. Never skipping would cost 140 against 115 gated, so Jev's saving over translating everything is modest on this corpus.
Decision
- Jev is available as a classify model:
--classify-model jevat TypeSafe's own address, a gateway's id with--classify-base-urland--classify-key. See Provider file and extra models. - The default classifier stays the translating model.
- First round: the abstain threshold stayed at 0.5; the four low-confidence skips were recorded, not judged (two URLs, a single letter and a bare author name, which a reading edition could keep or translate).
- Second round: the requests are lean (the shared text sent once), and the threshold became an asymmetric gate. A
skipbelow it becomestranslate; atranslateis taken at any probability. Content is never lost to a doubtful skip. - The gate is 0.95 on the chosen option's probability, from the corpus measurement above.
--classify-min-confidence Pmoves it for a run (BBM_JEV_MIN_CONFIDENCEdoes the same without the flag; the flag wins).
What would change it: a comparison on more books, with verdicts judged against what a reader wants, and a price for both arms.
Limits
- One book, 31 signatures, one run per arm in each round. Both rounds' agreement numbers, the lean mapping's token halving and the 0.75 replay rest on that one book.
- Only the gate value comes from the 45-EPUB corpus, and agreement there is still agreement with luna.
- The key was a gateway key, so TypeSafe's own address was not exercised live (it answered 401 to that key).
- Token counts only; no money comparison.
- Agreement with luna is not correctness: luna's verdicts were not judged either.
En Evaluation Session Compact Budget (`docs/en/evaluation/session-compact-budget.md`)
Session compact budget: `--context-compact-at`
Abstract
In session mode the history grows until it reaches --context-compact-at, then the model writes a handoff report and a new window starts. A short budget means many seams; a long one means every request re-reads a long history. An 18-run paid study on three books measured cost against the budget. The curve was flat from 1500 to 4000, rose after that, and rose steeply at 20000. Continuity held across 44 seams at a budget of 2500. After the grouping defaults were halved on September 7, a second measurement found that 8192 costs more than 4096: +39.8% against +27.3% over the old defaults on a 300-unit run. The default stays at 8192 anyway, for continuity, not price. A lower value is the cheaper setting.
Setup
The cost study (September 5, 2026). 18 paid runs, gpt-5.6-luna on OpenAI's endpoint, 3.267M tokens, --language zh-hans, --use_context session, cells sized for about 25 requests each. Three epub-samples books: childrens-literature (literary prose with recurring names), moby-dick-mo (long chapters) and epub30-spec (short, repetitive technical units).
| phase | cells | what it measures |
|---|---|---|
| 0 | 3 books at stock defaults | calibration: per-request overhead F, JSON wrapper j, handoff prompt F_h, report size K |
| 1 | C ∈ {1500, 4000, 8000, 20000} × 3 books, B=800 | the cost-vs-C curve |
| 2 | B ∈ {400, 1600} on moby-dick (800 from phase 1) | the cost-vs-B curve |
| 3 | one full book at the solved optimum (B=1088, C=2500) | out-of-sample validation |
Cost is stated in input-token-equivalents: (tokens_in − tokens_cached) + 0.1·tokens_cached + 4.0·tokens_out, the vendor's price ratio. The meter of the time did not count the compaction turns themselves (up to 38% of true cost at C=1500; fixed since), so every total is metered cost plus the reconstructed compaction turns.
The price check of the new defaults (September 7, 2026). animal_farm, gpt-5.6-luna, session mode, 300 and 150 units, the new grouping (16 units, budget 1200 to 1600) at C=4096 and C=8192, against the pre-September defaults (budget 2400 to 3200, C=8000).
Results
Total price-weighted cost per book, September 5:
| book | C=1500 | C=4000 | C=8000 | C=20000 | C* solved |
|---|---|---|---|---|---|
| childrens-literature | 218.8k | 220.1k | 228.4k | 340.7k | 2512 |
| moby-dick-mo | 207.9k | 201.7k | 237.2k | 263.3k | 2191 |
| epub30-spec | 117.2k | 108.4k | 111.9k | 169.0k | 1580 |
- Flat within 0.6 to 3% across 1500 to 4000 on all three books; all three solved optima land there.
- The results record puts 8000 at 4 to 18% above the flat region. The grouping report, written later from the same cells, puts it at 9 to 25% above each book's own optimum. Both describe the same runs, measured against different baselines.
- C=20000 costs up to 56% more, and it is the only cell where drift appeared: a 你→您 register shift within one long window.
- Continuity survives short windows. The full-book run at C=2500 crossed 44 seams with zero terminology breaks in 1012 nodes (皇帝 62/62, 夜莺 38/38, 公主 34/34, 安徒生 12/12). The handoff report works as an accumulating name table.
- The model behind the curve predicted the full-book run's request count exactly (82/82) and its cost within −6.2%. That run was cheaper per content token (11.7) than every grid cell (13.1 to 20.4).
- Fitted constants: F=104, j=11.3 per unit, F_h=72, K=538 (139 reports), ρ 1.41 to 1.74, g 1245/920/645 per book, r 1.12 to 1.18, α rising with C (0.042 to 0.213 on childrens).
Figure: the September 5 cost model and cells. It marks the defaults of that day (C=8000, a 1600 to 2000 budget clamp), before the September 7 ruling. Open squares at 12000 and 16000 are model predictions.
The price of the new grouping defaults, September 7:
| arm | 300-unit requests | all-in tokens | compactions |
|---|---|---|---|
| old defaults | 33 | 283,361 | 8 |
| new caps, C=4096 | 63 | 360,681 (+27.3%) | 19 |
| new caps, C=8192 | 52 | 396,197 (+39.8%) | 9 |
At 150 units: +5.9% at 4096 and +24.0% at 8192. Raising C from 4096 to 8192 brought the compaction count back to the old level (19 to 9, against 8), as intended, and made the bill worse. The halved budget multiplies the requests (33 to 52 or 63), and every request re-reads the carried history: the prompt load per request was 7007 (old), 4690 (at 4096) and 6537 (at 8192). Caching did not rescue it: 9k to 21k tokens cached against about 340k of prompt.
Figure: the September 5 cost model (left) and the September 7 price check (right). Script: docs/img/src/session_compact_cost.py.
Decision
--context-compact-at defaults to 8192 on every session run, grouped or not, the Codex route included. The run prints it at start.
The reason is continuity, for the many people who run this tool on local-device models, whose cost curve is not the hosted, cached one measured here. It is not the cheap setting. Under the current grouping budget, a lower value is cheaper: at 300 units, 4096 used 360,681 tokens against 396,197 at 8192. Past 16000 the cost rises steeply, and the only drift ever observed lived in the long-window cell.
What would change it: a new measurement of both cells (4096 and 8192) under a changed grouping budget, or a continuity measurement on local models. Do not move either number without re-running those two cells.
For you: leave it unset for the fewest seams. Set it lower (toward 4096) if session cost matters more to you than seams. Set it to your model's input limit if that is smaller. The minimum is 1500.
Limits
- One model (
gpt-5.6-luna) on one endpoint with prompt caching. Uncached and local endpoints were not measured. - Each cell was a single run of about 25 requests; differences under about 30% on one run are close to noise.
- The September 7 price check used one book (animal_farm) at two sizes.
- The continuity result rests on one full book (childrens-literature, zh-hans).
En Features Pdf Ocr Engines (`docs/en/features/pdf-ocr-engines.md`)
Which OCR engine
When a PDF page is a scan, --pdf-ocr has an OCR engine read it. --ocr-engine chooses which one. Four run on your own machine; they differ in what they read well, what they install and whether they download anything.
python make_book.py --book_name scan.pdf --to-epub --pdf-ocr \
--ocr-engine ocrmac --ocr-lang iso:en --language zh-hansTry two pages first (--pages 1-2) and read source.md in the bundle before you pay for a translation. The run prints the engine it used (OCR engine: …).
If you need X, use Y
| your situation | use | why |
|---|---|---|
| A Mac, and you do not want to download anything | ocrmac (already there with the pdf extra) |
Apple's own engine, part of macOS: no model download. The fastest in the measurement (6 s per two pages) and second-best on English, tied with the best on typewriter and clean pages. Weaker than rapidocr on simplified Chinese. |
| You have not chosen | auto (the default) |
Takes the first installed of ocrmac, rapidocr, easyocr, and names it. With the pdf extra that is ocrmac on a Mac and rapidocr elsewhere. |
| Simplified Chinese | rapidocr |
Best on simplified Chinese: mean Han-character error 0.054, against ocrmac 0.097 and tesseract 0.116. |
| Traditional Chinese (horizontal) | ocrmac or rapidocr |
Close on the one page measured (0.043 and 0.060). tesseract (0.350) and easyocr (0.724) were far behind. Vertical text comes out in the wrong column order with any engine: check source.md. |
| Japanese or Korean | ocrmac or rapidocr, with --ocr-lang iso:ja or iso:ko |
Not measured. Both engines have these languages; tesseract needs the jpn or kor language file installed. |
| English typewriter or other degraded scans | tesseract or ocrmac |
Tied on typewriter pages (error 0.082 and 0.086; rapidocr 0.206). On old print and a magazine, tesseract was clearly ahead (0.222; ocrmac 0.361, rapidocr 0.383). |
| A machine without a GPU | anything but easyocr |
Every number here was measured on the CPU. easyocr took 37 s per two pages (others 6 to 12 s) and up to 6.6 GB of memory. |
| Linux or Windows | tesseract for English, rapidocr for Chinese |
ocrmac exists only on macOS. |
| A script none of the above reads | easyocr |
It has models for 80+ languages. In the measurement it was last on English and Chinese and dropped text. |
Installing each engine
| engine | install | downloads on first use | size |
|---|---|---|---|
rapidocr |
Comes with the pdf extra, with onnxruntime. |
Nothing: its models are inside the package. | rapidocr 31 MB, onnxruntime 62 MB |
ocrmac |
Comes with the pdf extra on macOS (macOS only). |
Nothing. | 28 MB |
easyocr |
pip install easyocr |
A detector (83 MB) and one model per script: English 15 MB, Latin 15 MB, simplified Chinese 22 MB, traditional Chinese 226 MB, into ~/.EasyOCR/model. |
about 150 MB of packages |
tesseract |
The tesseract program and its language files, on PATH: brew install tesseract on macOS, apt install tesseract-ocr tesseract-ocr-chi-sim on Debian and Ubuntu, the installer from the tesseract project on Windows. |
Nothing: add a language by installing its file (eng 4 MB, chi_sim 2.5 MB, chi_tra 2.4 MB in the fast set). |
25 MB program plus its libraries |
All four work off-line once installed and, for easyocr, once its models are downloaded.
An engine you name that is not installed, or ocrmac on Linux or Windows, is refused before any page is read, with the line that installs it. auto is never refused.
If you keep tesseract's language files in a folder of your own (TESSDATA_PREFIX), put tesseract's configs folder in it too: without it tesseract cannot write the table docling reads, and the extraction fails.
Language codes
--ocr-lang takes each engine's own codes, or a portable iso: tag that works on every engine. When you switch engines, the iso: tag is the one to use.
| language | any engine | rapidocr | ocrmac | easyocr | tesseract |
|---|---|---|---|---|---|
| English | iso:en |
en |
en-US |
en |
eng |
| Simplified Chinese | iso:zh-Hans (or iso:zh) |
ch |
zh-Hans |
ch_sim |
chi_sim |
| Traditional Chinese | iso:zh-Hant |
chinese_cht |
zh-Hant |
ch_tra |
chi_tra |
| Japanese | iso:ja |
japan |
ja-JP |
ja |
jpn |
| Korean | iso:ko |
korean |
ko-KR |
ko |
kor |
rapidocr reads one language per run (the first). Without --ocr-lang each engine reads its own defaults: rapidocr Chinese and English, ocrmac, easyocr and tesseract English, Spanish, French and German. A scan in another script then comes out wrong.
What was measured
20 scanned pages (14 English, 6 Chinese), the same language asked of each engine, all on the CPU. Error is the character error rate against a typed transcript: 0 is perfect, and it counts a paragraph put in the wrong place as errors.
| pages | rapidocr | ocrmac | easyocr | tesseract |
|---|---|---|---|---|
| English typewriter (3) | 0.206 | 0.086 | 0.100 | 0.082 |
| English magazine and old print (3) | 0.383 | 0.361 | 0.609 | 0.222 |
| English, clean synthetic scan (4) | 0.069 | 0.019 | 0.070 | 0.019 |
| English, degraded synthetic scan (4) | 0.080 | 0.124 | 0.333 | 0.116 |
| Simplified Chinese book scan (1), Han characters | 0.009 | 0.056 | 0.235 | 0.036 |
| Traditional Chinese scan (1), Han characters | 0.060 | 0.043 | 0.724 | 0.350 |
| Simplified Chinese synthetic scans (4), Han characters | 0.066 | 0.107 | 0.709 | 0.136 |
| Time per two pages (median) | 12 s | 6 s | 37 s | 10 s |
Who won where:
- English: tesseract, with ocrmac close behind. On typewriter and clean pages the two are tied (differences under 0.005). tesseract's lead is real on old print. On the degraded synthetic pages rapidocr's lower number comes from reading order: ignoring order, all three read the words equally well.
- Simplified Chinese: rapidocr, on the mean and on 3 of 5 pages; ocrmac was ahead on one clean synthetic page, ocrmac and tesseract on one degraded one.
- Traditional Chinese: ocrmac and rapidocr, within noise of each other on one page.
- easyocr was last on every class but typewriter, the slowest, and the heaviest in memory.
Every number, the pages, the scorer and the commands: OCR engines on scanned pages.
En Features Pdf To Epub (`docs/en/features/pdf-to-epub.md`)
PDF to bilingual EPUB
If you want a PDF as a bilingual book with a table of contents, read two pages first, then run the whole file:
python make_book.py --book_name paper.pdf --to-epub --pages 1-2 --test
python make_book.py --book_name paper.pdf --to-epub --use_context sessionRead paper_pages-1-2_book/source.md between the two. Recommended settings for PDF has the command for each kind of document and each system.
The route has been checked on arXiv papers and a set of books, scans and browser-saved pages, not on every PDF shape. Issues and pull requests are welcome; attach the PDF if it can be shared, or the page of source.md that came out wrong.
What it does
--to-epub reads the PDF with docling's layout and table models into Markdown, translates that Markdown with the Markdown loader, and has Pandoc build a reflowable bilingual EPUB. Every paragraph is followed by its translation, and the table of contents follows the headings. Heading levels are read from the page itself: numbering first (1., 1.1, I., A.), then font size and weight. Figures stay pictures, and each display formula is cropped from the page as a picture and placed where it stood. Pages with no text layer, a scan, are read by the OCR models only when you pass --pdf-ocr; a scan whose text layer is wrong can have it replaced with --ocr-replace-layer. If you name a vision model with --img-model, it looks at each page and corrects the roles docling gave the regions (a heading that is really an author line, a code listing read as footnotes) before the Markdown is written.
Everything lives in a working folder beside the PDF, <name>_book/: source.md (the extraction), images/, book_bilingual.md (the translation) and a manifest. The finished book is copied out as <name>_bilingual.epub. Rerunning the same command reuses the extraction and a finished translation, so you can stop, read source.md, fix a heading, and go on without paying twice. A PDF is a page description, not a document: it stores glyphs at positions and knows nothing of paragraphs or headings, so every extractor guesses the structure back. A heading one level off or a table that arrives as prose is the format showing through, and a minute's edit in source.md.
source.md marks where each page starts with a comment, . N is the page's number in the PDF file, counted from 1: the numbers `--pages` takes, not the number printed on the page. A page with nothing on it keeps its marker, so the numbering never slips. Rarely, docling returns an item with no page. Such items are written once, at the end of `source.md`, under , and the run says how many. Their place in the reading order is lost: move them where they belong, or delete them, before you translate.
Setup
- Install the PDF extra from a clone of the repository,
pip install ".[pdf]"(Linux without an NVIDIA card and Windows with one add PyTorch's own index: Installing the PDF extra). You also need Pandoc 3.1.12 or newer on PATH. No Java. - The models (about 500 MB) download on the first run.
- Read the first two pages before anything longer, and read
source.md, the headings at least, before you pay for the full translation. They become the table of contents.
The route's flags:
| flag | what it does |
|---|---|
--to-epub |
Take this route. |
--pdf-ocr |
Read pages that have no text layer. Off by default: a born-digital PDF is already readable, and OCR costs several times the time without changing what is read. Layout, heading and table detection run either way. |
--ocr-replace-layer |
With --pdf-ocr: OCR every page and use that text instead of the text layer the PDF carries. Off by default: a layer is kept and only pages without one are OCR'd. For a scan whose layer is wrong (another language, or garbage from a poor OCR); on a clean scan the layer reads better than the local engine. Turning it on or off reads the PDF again on the next run. |
--ocr-lang LANGS |
With --pdf-ocr: the languages the OCR engine reads, in its own codes. auto picks ocrmac on a Mac and rapidocr elsewhere; the pdf extra installs both, and neither downloads anything. Which OCR engine has the measurement. The codes: rapidocr ch, en, latin; easyocr ch_sim, ja, ko; ocrmac zh-Hans, ja-JP. A BCP-47 tag behind iso: (iso:zh, iso:ja, iso:zh-Hant) works on every engine, so it is the safe choice when you do not know which one will run. rapidocr has no ch_sim: for Chinese write iso:zh. rapidocr reads one language per run and uses the first. The run prints the engine and languages it used. |
--ocr-engine ENGINE |
With --pdf-ocr: which OCR engine reads the pages: auto (default; the first installed of ocrmac, rapidocr, easyocr), rapidocr, ocrmac (macOS), easyocr or tesseract. An engine that is not installed is refused before any page is read. Which to choose, measured: Which OCR engine. |
--device auto|cpu|cuda|mps|xpu |
Where the models run. auto detects CUDA or MPS and falls back to the CPU. The CPU gives the same text, only slower. |
--pages 12-30 |
Only these pages, numbered from 1 (1,3,5-7 works too). The book gets its own names: <name>_pages-12-30_book/, <name>_pages-12-30_bilingual.epub. |
--no-formula-images |
Leave display formulas as `` placeholders. Almost never what you want: the parser never reads equations, so without the pictures the mathematics is missing. |
--pdf-image-dpi N |
How sharp the figures are, in dots per inch of the PDF's own page size (72 to 600). Default 200: sharp on a tablet or a high-density e-reader; 150 for a smaller book, 300 for figures with tiny labels. A rerun at another value redraws the figures only; the extraction and the translation are kept. Formulas keep their own resolution. A figure that is a single embedded picture keeps that picture's own resolution when it is lower, and no figure goes above 5.6 megapixels. |
--img-model MODEL |
A vision model that corrects region roles from the page image. Off unless named here or as the provider entry's img_model; never the translating model by fallback. none turns an entry's model off. See Correcting region roles. |
--img-base-url URL |
Where that model is served, when it is not the run's endpoint (OpenAI-compatible only). |
--img-key KEY |
The key for --img-base-url. Defaults to the run's key on the run's own endpoint. |
Every Markdown-loader flag works unchanged: --use_context session (recommended), --glossary, --parallel-workers (not with a session), --no-thinking, --test. The full list is on PDF. A combination the translation would refuse, such as --no-thinking on the codex route, is refused before the extraction starts, so nothing is paid for first. --classify-model does nothing on this route yet; the run warns.
Correcting region roles with a vision model
docling cuts each page into regions and labels them: text, heading, title, caption, footnote, code, table, picture. Some labels are wrong, and the book shows it: an author line in the table of contents, a code listing printed as prose, a figure label promoted to a section. With --img-model, each page is shown to the model with docling's regions drawn and numbered on it. The model answers one role per region (text, heading, title, caption, footnote, code) or abstains. Accepted answers replace the labels before the Markdown is written, so the contents and the code blocks come out right. The text itself is never rewritten.
- What it cannot fix. Lists, tables, pictures, formulas and running headers and footers are not asked about. A scanned page that docling has shattered into fragments is not rebuilt by relabeling them.
- What it costs. About 3,000 prompt tokens per page; 2.7 to 10.6 s per page in the study. In the study behind it, the model fixed 40 of 66 wrong labels on 12 pages; see Where an LLM fixes layout.
- What it needs. An OpenAI-compatible endpoint that accepts images. The run checks once that the model can read a picture. If it cannot, the run says so and extracts with docling's own labels.
- The model is named, never assumed. The step runs only on a model you name with
--img-modelor in the provider entry'simg_model. The shippedopenaientry namesgpt-6-luna, so--provider openaiturns the step on;--img-model noneturns it off. An on-device translating model is never asked to read a page. - Changing it extracts again. The image model and its address are part of what the extraction is compared against, so adding, changing or dropping
--img-modelreads the PDF again. A bundle whose role pass did not finish is also read again when a run asks for the pass.
The decisions are kept in <name>_book/.work/extraction/decisions.json. At the end of the extraction the run prints the pass's token usage on its own line: Image model (<model> at <address>): ….
Mend a translation by hand
If a translated paragraph is wrong, correct it in <name>_book/book_bilingual.md, then take two steps:
- Run
python tools/pdf_to_book.py export path/to/<name>_bookfrom the repository folder. It rebuilds<name>_book/book_bilingual.epubfrom your edit; nothing is extracted or translated again, and no key is needed. - Copy
<name>_book/book_bilingual.epubover<name>_bilingual.epubbeside the PDF. The export writes only inside the working folder, so the book beside the PDF keeps the old text until you copy it.
Recommended commands
Every PDF starts with two pages and a few translated blocks (--pages 1-2 --test), then a read of source.md. The command for a novel, a textbook, a paper, a scanned book, a scan with a bad text layer and a Chinese scan, and the install and device for macOS, Linux with NVIDIA, Windows, a CPU-only machine and Docker, are on Recommended settings for PDF.
Known limitations
- Inline math equations may not be displayed correctly. Only display formulas are cropped as pictures; maths inside a sentence arrives as whatever text the extractor read from it.
What can go wrong
Every failure on this route prints one line starting with Error:, before anything is paid for where it can. These are the lines, and what to do.
Before extraction
Pandoc is required for --to-epub. Install it and make sure pandoc is on PATH.Install Pandoc 3.1.12 or newer from pandoc.org.pandoc 3.1.3 is too old for EPUB export; Pandoc 3.1.12 or newer is required …Your Pandoc came from apt (Ubuntu 24.04 ships 3.1.3, Debian 13 ships 3.1.11). Install the release and put it first on PATH; the line names the harnesstools/pdf_to_book.py --pandoc PATHas the other way.reading a PDF needs the pdf extra, which is not installed.From a clone,pip install ".[pdf]"(step 3 of Installing the PDF extra). Notpip install "bbook_maker[pdf]".--device cuda was asked for, but the installed PyTorch is a CPU-only build.Reinstall through the CUDA route.… but this machine has no cuda accelerator available.Use--device cpuor--device auto.--parallel-workers is not supported with --use_context session …Choose one.--no-thinking has no request to travel in on the codex route: …Drop--no-thinkingon codex.--img-model needs an OpenAI-compatible endpoint; … resolves to the … format.The image model is asked at the run's endpoint, or at--img-base-url, and that endpoint is not OpenAI-shaped. Give an OpenAI-compatible--img-base-url(and--img-key), or drop--img-model.--img-base-url names where --img-model is served, and no --img-model was given. …Name the model too.--ocr-replace-layer re-reads pages that already carry a text layer with the OCR engine, so it needs --pdf-ocr.Add--pdf-ocr.
During extraction
N of M selected pages have no text layer (page(s) …); rerun with --pdf-ocr to read them with the OCR models.The PDF (or part of it) is a scan. Add--pdf-ocr.No --ocr-lang given: the OCR engine reads its own default languages, which may not be the pages'; …ThenOCR engine: rapidocr (docling's choice on this install), languages: the engine's defaults.Checksource.md. If the scan is not in Chinese or English, rerun with--ocr-lang; the bundle is read again.The parser produced no text for a document whose pages have no text layer; the OCR pass returned pictures only.orWarning: no text was recognised on page(s) …The engine could not read the script. Rerun with--ocr-langfor the page's language.The parser returned no text for this PDF; there is nothing to translate. If its pages are scans, rerun with --pdf-ocr.As it says.Warning: page N extracted C characters, several times what a printed page holds; inspect source.md before translating.Something on that page, usually a figure, carries far more text than it shows. A page whose Markdown is thousands of lines is junk, not a long page. Remove it fromsource.mdor leave the page out with--pages.The PDF carries JBIG2 image masks, which docling-parse renders wrongly (docling issue #4329); page images are rendered by pypdfium2 instead, the text layer still by docling-parse.Information. Scans from the Internet Archive and ABBYY FineReader often use such masks, and docling-parse would draw the page as a smear. The run takes the page image from pypdfium2 for the whole document.N of M selected pages carry only an invisible OCR text layer (a scanned book with recognised text underneath); that layer is kept as the page's text …Information. With--pdf-ocrthe layer may be mixed with what the OCR engine reads, so name the language with--ocr-lang; in the wrong language a readable page turns to garbage. If the layer itself is garbage, rerun with--pdf-ocr --ocr-replace-layerand the language.OCR: replacing the embedded text layer on every page (--ocr-replace-layer)Information. Without--ocr-langthe language hint follows.page N: the OCR engine read nothing where the PDF carried a text layer; …With--ocr-replace-layer, that page came back empty and stays an empty page: its layer is not used in its place. The line also goes into the manifest's limitations; past ten pages the rest are counted (... and N more). Rerun with--ocr-langfor the page's language, or without--ocr-replace-layerto keep the layer.The OCR engine read nothing on any selected page, and with --ocr-replace-layer the PDF's text layer is not used, so there is nothing to translate; …The run stops before any translation is paid for. The manifest records the failed attempt: the reason, the empty pages and the settings used. The usual cause is the wrong--ocr-lang, or none on a scan outside the engine's default languages. Rerun with the right one, or without--ocr-replace-layerto keep the layer.N item(s) carry no page number; they are placed after the last page in source.md.Rare. docling returned items without a page. They are at the end ofsource.md, under ``, and their place in the reading order is lost. Move them where they belong, or delete them, before you translate.… has no model for the OCR language 'ch_sim'. …The engine does not know that code; the message lists the ones it does. On rapidocr, writeiso:zhfor Chinese. Nothing was read.… did not read the probe image (…); image steps are skipped this run.The image model cannot see pictures at that endpoint. The extraction goes on with docling's own labels.Region roles: A of B asked items changed by <model> (… kept, … rejected, … pages with many changes); overlay at .Information: what the image model changed.Region roles: C of D asked items on page N changed; read that page in source.md before translating.Most of a page changed; the changes are kept, so look at it.Region roles: …; the detector's own labels stand there.Part of the pass did not finish (a budget ran out, an answer was rejected, a region went unanswered); those regions keep docling's labels.Extracting again: the bundle's region-role pass is <status>; this run asks for --img-model …, which only a complete pass satisfies.Information. The earlier pass did not finish, so the PDF is read again.--img-model <model> failed: …The image endpoint answered with an error that waiting does not fix, such as a rejected key. The run stops. Fix the key (--img-key) or the address.
After extraction, before translation
The document does not open with a top-level heading, so a heading "<name>" was added above its text; …The EPUB's contents need a level-1 heading at the start. Rename it insource.mdbefore translating if you like.Page 12: the selection starts inside a section, so a heading "Page 12" was added above its prose; …The same, for a--pagesrange that starts mid-section.The page selection is not one run of pages, so pages 1-7 were read and the ones outside the selection dropped afterwards; …A range with a gap reads everything it spans. A single range reads fewer pages.Display formulas kept as images: N. …Information: the equations are pictures and are not translated.Warning: a formula region on page N covers S% of the page, which is a layout mistake rather than an equation; …That region stays a placeholder instead of a picture of the whole page. The otherWarning: … formula …lines mean one formula could not be placed; its placeholder stays.Extraction: removed N control character(s) that docling read from glyphs on page(s) …; they are not text and would break the EPUB.Information. docling read some symbols (often a minus or an approximately-equal sign) as invisible control characters, and they were taken out ofsource.md. The symbol they stood for is missing on those pages; put it back insource.mdif it matters.Unsupported Markdown structure: control character U+0000 at line N. …source.mdstill holds a control character (a bundle extracted before the line above existed, or an edit). Delete it and rerun; nothing has been translated yet.Unsupported Markdown structure: raw tex '\s' at block 29 … Normalize the source before translation.The extractor does not escape Markdown in prose, so a sentence containing\s,[u](y)or<k>reads as raw TeX, a link or HTML. Escape it insource.mdand rerun; the extraction is not repeated.Reusing the extraction made with docling … on …; this run would use docling … on …. Delete the bundle directory to extract again.Information. The extraction came from another device or docling version.
Translation and export
Translation reused: … (same source and settings; delete it to translate again).Information. Deletebook_bilingual.mdto translate again.Error: translate failed: Source or translation settings changed; start a new translation bundle.You changed the model, the language orsource.mdafter a partial translation. Rerun with the original settings, or move the bundle aside and start over.Error: … Bilingual Markdown was edited; rebuild the EPUB from it with: python tools/pdf_to_book.py export <name>_book, then copy <name>_book/book_bilingual.epub over <name>_bilingual.epub.You editedbook_bilingual.mdby hand. That is allowed, but the run will not overwrite it. Take the two steps the line names (it carries the full paths): the export rebuilds the book in the working folder from your edit, and the copy puts it beside the PDF; see Mend a translation by hand. To translate again instead, deletebook_bilingual.md.EPUB navigation is invalid: …The headings do not form a usable table of contents. Fix the heading levels insource.md(one#title, then##,###) and rerun.Interrupted. Rerun the same command to resume.Ctrl+C. Rerun; the stages that finished are not repeated.
After the run
Image model (<model> at <address>): tokens: …The image model's own usage, printed after the extraction, apart from the translation's.Nothing on this route classifies yet, so --classify-model is ignored on a … book.A classify flag or a provider'sclassify_modelreached the translation ofsource.md, where nothing classifies. Harmless.
Limits to know
- Figures stay pictures and their labels are not translated.
- The EPUB carries no
bbm_translation_metadata.json, and--no_disclosureis not honored on this route yet: the credit line is always added. --glossary-autolearns only when a compaction happens, so a short paper at the default budget learns nothing.--ocr-replace-layeron a scan with a good visible layer reads worse than the layer (fewer characters, visible misreadings), so keep it for a layer that is wrong. On a scan with an invisible layer the two give nearly the same text.- With an invisible layer,
--pdf-ocralone already has the OCR engine read the page, which is why the Chinese numbers above are close. Keeping such a layer untouched while OCR is on is not possible yet. - The manifest records a failed attempt only for the all-empty stop.
En Features Plan Mode (`docs/en/features/plan-mode.md`)
Plan mode
What it does
An EPUB keeps its text in many kinds of markup: paragraphs, headings, list items, table cells, blockquotes, verse lines, captions. Plan mode finds all of them. The loader partitions the whole book into units, groups the units by their tag signature (the tag and its classes), and asks the model which signatures are worth translating. The answer is written to _plan.json, which every later run of the same book reuses. Without a plan, only the --translate-tags selection is translated, by default, and verse or a table outside is left in the source language without a word.
Plan mode also groups the work. Consecutive units share one request up to a token budget (--accumulated_num) and a unit cap (--max-batch-units, default 16). Each request asks for the units back by id, so a reply that drops, merges or reorders units is caught and retried in smaller pieces instead of shifting every translation by one slot. Plan mode is on by default for an EPUB on any LLM route; you do not need a flag. The numbers behind the defaults are on Units and tokens per request.
Setup
Nothing to install. Plan mode needs an EPUB and a model to classify with. That is the translating model on any LLM route. On a translation-service route there is no model to ask, so the run falls back to the --translate-tags selection, unless you name a classifier with --classify-model (see Choosing the classifier).
The flags:
| flag | what it does |
|---|---|
--plan-classify auto |
Default. The translating model decides, over a JSON schema where the endpoint verifiably applies one, otherwise over a plain conversation with one-word skip/translate/unsure answers. Unsure and unreadable answers translate. |
--plan-classify model |
Like auto, but an unresolved row stops the run instead of falling back. |
--plan-classify all |
Translate every unit; no classification, no plan file. |
--plan-classify none |
No plan: translate the --translate-tags selection, as before plan mode existed. |
--plan-classify agent |
Write the plan with samples, print instructions for a coding agent (or you), and stop. Rerun the same command to translate. |
--classify-model MODEL |
Classify with another model, or with a Jev-compatible classifier (jev). Named on the command line, it puts the run in model mode, so a classification failure stops the run. --plan-classify-model is the old name and still works. |
--classify-base-url URL |
Where that model is served, when it is not the run's endpoint: an OpenAI-compatible address, or a Jev-compatible classifier's URL. |
--classify-key KEY |
The key for --classify-base-url. |
--plan-dry-run |
Print the per-signature coverage table, write the plan with every decision empty, and exit. No key needed. |
--plan-min-coverage FRACTION |
Stop when the plan covers less than this share of the book's text (default 0.5; 0 turns the check off). |
--accumulated_num N |
Token budget per request. Unset, the run derives it: 1200 with the stock prompts, up to 1600 under a long custom --prompt, 800 on an endpoint without a strict schema (session runs keep the un-halved value). 1 turns grouping off. |
--max-batch-units N |
Most units per request. Default 16; 8 on an endpoint without a strict schema. |
--exclude-translate-tags TAGS |
Tags whose content is never sent (default sup,code). |
--only_filelist, --exclude_filelist |
Plan only these internal files, or skip these. The dry run honors them too. |
Do not pass these in plan mode: --translate-tags (ignored, the plan covers everything), --allow_navigable_strings (ignored), --block_size and --batch_size (they re-cut text the plan already partitioned). --retranslate, --batch and --sentence_mode contradict a plan and are refused.
Choosing the classifier
The classifier is found in this order: --classify-model, then the provider entry's classify_model, then the translating model.
- How it is asked. Where the classifier's endpoint verifies a strict JSON schema, one request carries a page of signatures and the reply is held to the schema. Elsewhere it is a plain conversation: five signatures per turn, each answered
skip,translateorunsure. Two missed replies in a row step down to three per turn, then to one. Below one, classification stops and the remaining signatures are translated. Each step prints one line. - A classifier of its own (named by flag or provider entry) plans the book whatever the translating route can do. The run prints
plan mode: on (classified by …). This is how a translation-service route gets a plan. Google Translate with gpt-5.6-luna as the classifier was run end to end on the test book: all 31 signatures decided, coverage 99.8%. - Agent mode asks no model.
--plan-classify agenthands every undecided row to you or your coding agent. A named classifier does not pre-fill the plan, because an agent judges worse from pre-filled answers; the run warns that it is ignored.--plan-classify allignores it too. - Where it is asked and with which key is on Provider file and extra models. A classifier on its own address prints its own usage line at the end of the run.
- Jev, TypeSafe's classifier, is built for exactly this question: translate or skip, a page of signatures per request, in one cheap round trip.
--classify-model jevuses it; a gateway in front of TypeSafe (Vercel's, or Cloudflare AI Gateway) works too. Which key goes where, and the URL rules, are on Provider file and extra models. - Jev's gate. A doubtful
skipbecomestranslate, so no content is lost to it. The gate is 0.95 on the probability of Jev's chosen answer, measured over 662 plan signatures from 45 EPUBs against gpt-5.6-luna. At that value about nine of ten of Jev's skips fall back totranslate; what it still skips is apparatus (copyright lines, line numbers, note marks, index locators). On that corpus Jev saves little over translating everything. The plan file marks each fallback on its row (… below the gate: translate).--classify-min-confidence Pmoves the gate for a run (BBM_JEV_MIN_CONFIDENCEdoes the same without the flag); lower keeps more of Jev's skips, at your risk. See Jev as the plan classifier.
=== "A translation-service route with an LLM classifier"
```bash
bbook_maker \
--book_name novel.epub \
--api_format google \
--classify-model gpt-6-luna \
--language zh-hans
```
The classifier goes to OpenAI and reads `OPENAI_API_KEY`. The translation stays with Google.=== "Jev from the shipped provider file"
```bash
bbook_maker \
--book_name novel.epub \
--provider openai-jev \
--use_context session
```
The one-line way: translates with gpt-6-luna at OpenAI (`OPENAI_API_KEY`) and classifies the plan with Jev (`JEV_API_KEY`), both from the `openai-jev` entry of the shipped `bbm_providers.example.json`.=== "Jev at TypeSafe"
```bash
bbook_maker \
--book_name novel.epub \
--model gpt-6-luna \
--classify-model jev \
--use_context session
```
Reads `JEV_API_KEY` or `TYPESAFE_API_KEY`.=== "Jev through a gateway"
```bash
bbook_maker \
--book_name novel.epub \
--model gpt-6-luna \
--classify-model typesafe-ai/jev \
--classify-base-url https://ai-gateway.vercel.sh/typesafe \
--classify-key "$GATEWAY_KEY" \
--use_context session
```
The key must be named: the Jev variables are sent on their own only to a typesafe.ai address.=== "Jev through Cloudflare AI Gateway"
```bash
bbook_maker \
--book_name novel.epub \
--model gpt-6-luna \
--classify-model jev \
--classify-base-url https://gateway.ai.cloudflare.com/v1/$ACCOUNT_ID/$GATEWAY_ID/custom-typesafe \
--classify-key "$JEV_API_KEY" \
--use_context session
```
TypeSafe is a [custom provider](https://developers.cloudflare.com/ai-gateway/configuration/custom-providers/) on your gateway, slug `typesafe`, base URL `https://api.typesafe.ai`. For an authenticated gateway, set `CF_AIG_TOKEN`; if the gateway stores the TypeSafe key, leave out `--classify-key`.Recommended commands
The command per kind of book (novel, textbook, paper, dictionary), per endpoint (hosted, on-device, translation service) and per system is on Recommended settings for EPUB.
What can go wrong
These are the lines the run prints, what they mean, and what to do.
plan mode: on (endpoint verified strict JSON schema),plan mode: on (no structured output here; classifying over a plain session)orplan mode: on (classified by …). Not a problem. It says how the plan will be decided.plan: this endpoint missed the reply format twice at 5 per turn; continuing at 3 per turn. The plain-conversation classifier stepped down. Nothing to do; it costs more turns.plan: this endpoint missed the reply format twice even one at a time; classification stops here and the remaining N signature(s) are translated. The model cannot hold the one-word format. The book is still translated, generously. For a better plan, name a stronger classifier with--classify-model, or use--plan-classify agent.… doesn't apply JSON schema (…), using delimiter methodor… honors JSON schema shape but not value constraints; using the delimiter method for translation, schema kept for classification. Not a problem. The endpoint does not do strict schema decoding, so translation uses a delimiter format. Expected on the anthropic route, most proxies and local servers.plan mode: off (…). The run translates the--translate-tagsselection. The reason in brackets says why: a route with no model,--translate-tagsgiven, or a failed probe. If you did not expect it, read the reason. On a route that does not plan by itself, the run suggests--plan-classify model.N misaligned batches this run — if this keeps happening, a lower --max-batch-units or --accumulated_num may fit this model better. Printed from the third recovered batch. The model keeps miscounting large requests. Nothing is lost (each bad batch was retried in halves), but it costs requests. On the next run halve the unit cap:--max-batch-units 8, then4.Plan coverage X% is below the required 50.0% — refusing to translate a fraction of the book silently.The plan would skip most of the book. Open_plan.jsonand look at what was markedskip. A dictionary or a book with a large apparatus may really translate less; then lower--plan-min-coverageon purpose.--classify-model names a classifier, and --plan-classify agent leaves every row to your agent and asks no model; it is ignored this run.(or… all translates the whole partition …). As it says; drop the flag or the mode.--plan-classify model asks an LLM to rule on every plan signature, and the google format translates through one fixed engine with no model to ask. …A fixed-engine run inmodelmode with no classifier of its own. Add--classify-model, or useagentorall.--classify-model needs an OpenAI-compatible endpoint; … resolves to the … format.A--classify-base-urlthat is not OpenAI-shaped.--classify-base-url names where --classify-model is served, and no --classify-model was given. …Name the model too.No API key for the jev classifier at … Pass --classify-key, …The Jev variables are read only for a typesafe.ai address. Through a gateway, name the key with--classify-key.BBM_JEV_MIN_CONFIDENCE must be a number from 0 to 1; got …Fix or unset the variable._plan.json has N undecided signature(s) …. After--plan-classify agent, some rows still have no decision. Fill everyactionwithtranslateorskip, then rerun.…: invalid action '…' — use …. A typo in a hand-edited plan. Fix the JSON and rerun.- A resume refused with a fingerprint message. The book or the plan changed since the checkpoint was written. Rerun with the original flags, or delete the checkpoint and start over, knowing what gets paid again.
- The plan skipped something you wanted. Delete
_plan.jsonand run again, or edit itsactionfields by hand. A--testrun classifies the whole book, not only the slice, so its plan is the real one.
En Features Recommended Epub (`docs/en/features/recommended-epub.md`)
Recommended settings for EPUB
For most books this is the whole command:
bbook_maker \
--book_name my_book.epub \
--language zh-hans \
--use_context session \
--quietThe book is translated through a plan (every block of text is found, and the model decides which kinds are worth translating), and a session keeps names and terms consistent. The rest of this page is what to change when your book, your model or your machine is not the common case.
If you need this, pass that
| if you need… | pass | read more |
|---|---|---|
| a first look for almost nothing | --test --test_num 8 |
Quick start |
| to see what will be translated and skipped before paying (no key needed) | --plan-dry-run |
Plan mode |
| every block translated, no classification | --plan-classify all |
Plan mode |
| to decide the plan yourself, or with a coding agent | --plan-classify agent, then rerun the same command |
Plan mode |
| names and terms that must hold across the book | --glossary names.txt |
Session mode |
| your own voice or register | --prompt my_prompt.json |
Prompt files |
| only some chapters | --only_filelist ch03.xhtml,ch04.xhtml (names inside the EPUB) |
EPUB |
| a long book done faster, consistency second | --parallel-workers 4 with bare --use_context (a session cannot be shared between workers) |
EPUB |
| the translation visually set apart | --translation_style "color:#808080;font-style:italic" |
Command line options |
| the translation only, without the original | --single_translate |
EPUB |
| no AI-translation credit line | --no_disclosure (a reader can then no longer tell the translation from a human one) |
EPUB |
| to continue after Ctrl+C or a crash | the same command plus --resume |
Quick start |
By kind of book
=== "Novel"
The command at the top of this page. If a recurring name drifts after a window seam, pin it with a glossary file:
```bash
bbook_maker \
--book_name novel.epub \
--language zh-hans \
--use_context session \
--glossary names.txt \
--quiet
```=== "Textbook with tables and formulas"
Preview the plan first. A textbook puts text in table cells, captions and sidebars, and you want to see what will be skipped before you pay.
```bash
bbook_maker \
--book_name textbook.epub \
--plan-dry-run
```
If the table shows kinds of text you want that were not selected, translate every unit instead of classifying, and pin the terms that must hold across hundreds of pages:
```bash
bbook_maker \
--book_name textbook.epub \
--language zh-hans \
--plan-classify all \
--use_context session \
--glossary terms.txt \
--quiet
```
Formulas in an EPUB are MathML or images. MathML (`<math>`) and SVG are never sent to the model, so equations stay as they are.=== "Paper"
A paper as an EPUB takes the same command as a novel. A paper as a PDF: see [Recommended settings for PDF](recommended-pdf.md).=== "Dictionary or critical edition"
A book that is mostly apparatus (headwords, sigla, line numbers) legitimately translates less than half its text, and the run stops at the coverage check. Preview with `--plan-dry-run`, then lower the check on purpose:
```bash
bbook_maker \
--book_name edition.epub \
--language zh-hans \
--plan-min-coverage 0.2 \
--use_context session
```=== "A scan, or a Chinese scan"
A scan is a PDF, and plan mode is EPUB only. See [Recommended settings for PDF](recommended-pdf.md). A Chinese EPUB translated to English works as a novel:
```bash
bbook_maker \
--book_name chinese_novel.epub \
--language en \
--use_context session
```By model and endpoint
| if your endpoint is… | pass | why |
|---|---|---|
| OpenAI, Anthropic, or a gateway with prompt caching | --use_context session |
the history is re-read at the cache rate |
an endpoint without prompt caching (cached= on the progress bar stays 0) |
bare --use_context |
a session would pay for the whole history on every request |
| Gemini or Qwen | --api_format gemini --use_context (or qwen) |
they keep their own history and refuse a session |
| your ChatGPT plan | --api_format codex, no context flag |
the thread is the context; see Translating with an LLM |
| a translation service (Google, DeepL, …) | --classify-model gpt-6-luna |
the engine cannot classify, so without an LLM classifier only `` is translated; see Translation services |
| a small on-device model (8B to 16B) | the defaults; add --no-thinking for a reasoning model, and --context-compact-at at the model's input limit |
the run halves its request sizes by itself on an endpoint without a strict schema; see On-device models |
a model that keeps misaligning (N misaligned batches this run) |
--max-batch-units 8, then 4 |
smaller requests; see Units and tokens per request |
| a small model whose plan classification keeps failing | --plan-classify all |
skips classification and translates every block |
By system
The work happens on the endpoint, so a hosted endpoint needs the same command on every system. The system matters when the model runs locally.
=== "macOS (Apple silicon)"
A local model through Ollama, which uses Metal. Bound the session window to a small model's context:
```bash
bbook_maker \
--book_name novel.epub \
--api_base http://localhost:11434/v1 \
--model qwen3:8b \
--use_context session \
--context-compact-at 4000
```
A small local model has no strict JSON schema, so the run halves the unit cap to 8 by itself.=== "Linux with NVIDIA"
A local server with an OpenAI-compatible API (vLLM, llama.cpp, Ollama) on the GPU:
```bash
bbook_maker \
--book_name novel.epub \
--api_base http://localhost:8000/v1 \
--model your-model-id \
--use_context session \
--context-compact-at 4000
```=== "CPU only"
A local model on the CPU is slow for a whole book, and re-reading a history on every request is slower still. Use a hosted endpoint with prompt caching:
```bash
bbook_maker \
--book_name novel.epub \
--model gpt-6-luna \
--use_context session
```=== "Docker"
```bash
docker run --rm \
-v "$PWD":/book \
-e OPENAI_API_KEY \
ghcr.io/yihong0618/bilingual_book_maker:latest \
--book_name /book/novel.epub \
--use_context session \
--quiet
```
See [Docker](../docker.md).En Features Recommended Pdf (`docs/en/features/recommended-pdf.md`)
Recommended settings for PDF
Start every PDF the same way: two pages, a few translated blocks, then read what came out.
python make_book.py \
--book_name book.pdf \
--to-epub \
--pages 1-2 \
--testIt extracts only those two pages and costs almost nothing. Open book_pages-1-2_book/source.md and read the headings at least: they become the table of contents. A page whose Markdown runs to thousands of lines is junk from a figure, not a long page. When it looks right, run the command for your document below. If something is off, fix it in source.md and rerun; the extraction is reused.
The route needs the PDF extra and Pandoc 3.1.12 or newer. What each flag does is on PDF to bilingual EPUB.
If you need this, pass that
| if your PDF is… | pass | why | read more |
|---|---|---|---|
| any PDF you want as a book | --to-epub --use_context session |
a reflowable EPUB with contents; a session gives each short block the text before it | PDF to bilingual EPUB |
| a scan (no text layer) | --pdf-ocr |
pages without text are refused until you pass it, so you never need to guess | OCR flags |
| a scan in Chinese, Japanese, Korean or another script outside the Latin alphabet | --pdf-ocr --ocr-lang iso:ja (or iso:ko, …) |
the default OCR engine reads only its own languages: on a Mac (ocrmac) English, Spanish, French and German, elsewhere (rapidocr) Chinese and English | OCR language |
| a Chinese scan | --pdf-ocr --ocr-lang iso:zh (iso:zh-Hant for traditional) |
not ch_sim: rapidocr refuses it |
Chinese scan |
| a simplified-Chinese scan | --pdf-ocr --ocr-engine rapidocr --ocr-lang iso:zh |
lowest Han error in the measurement | Which OCR engine |
| on a Mac, and you want no download | --pdf-ocr (auto picks ocrmac, which the pdf extra installs on a Mac; --ocr-engine ocrmac names it) |
Apple's Vision framework, nothing to download; within a hair of the best on English and traditional Chinese, rapidocr better on simplified-Chinese scans | Which OCR engine |
| a scan that already has an OCR layer (Internet Archive, ABBYY) | nothing extra | the run uses the layer and says so | Scanned book |
| a scan whose text layer is garbage | --pdf-ocr --ocr-replace-layer --ocr-lang <lang> |
every page is read again by the OCR engine | Scan with a bad text layer |
| a paper, or anything with code listings | --img-model gpt-6-luna |
a vision model fixes author lines taken for headings and listings read as footnotes, about 3,000 prompt tokens a page | Correcting region roles |
| a long book you want a chapter at a time | --pages 12-30 |
each range gets its own book and never overwrites another | PDF flags |
| full of maths | nothing extra | display formulas are kept as pictures by default | PDF flags |
| translated with names or terms that must hold | --glossary terms.txt |
pinned renderings, sent only with the blocks they occur in | Session mode |
| on a machine whose GPU misbehaves | --device cpu |
the same text, only slower | By system |
By document type
=== "Novel"
A typed novel has few headings and no tables. Leave out front matter you do not want with `--pages`.
```bash
python make_book.py \
--book_name novel.pdf \
--to-epub \
--language zh-hans \
--use_context session \
--quiet
```
Check the chapter headings in `source.md` first: a novel often sets chapters without numbering, and their level then comes from font size alone. The region-role pass was measured on papers, web pages and code listings, not on novels; with `--provider openai` it is on, and `--img-model none` saves its tokens.=== "Textbook with tables and formulas"
Tables are detected without OCR. Display formulas become pictures; the prose around them is translated, the equations are not.
```bash
python make_book.py \
--book_name textbook.pdf \
--to-epub \
--pages 12-30 \
--language zh-hans \
--use_context session \
--glossary terms.txt
```
Translate a chapter at a time with `--pages`; each range gets its own book. A textbook with code listings gains from `--img-model gpt-6-luna`: the listing's lines come out as code instead of footnotes. Inline mathematics inside a sentence is not a formula region and is not covered: it arrives as whatever the text layer or OCR made of it.=== "Paper"
A paper extracts into many short blocks; a session gives each block the text before it.
```bash
python make_book.py \
--book_name paper.pdf \
--to-epub \
--language zh-hans \
--use_context session \
--img-model gpt-6-luna
```
Heading levels come out exact on most papers (187 of 195 headings across 20 arXiv papers). `--img-model` demotes an author line or a figure label taken for a heading, for about 3,000 prompt tokens a page; leave it out to spend nothing on it. Leave out the bibliography with `--pages` if you do not want to pay for it.=== "Scanned book"
The run refuses a page with no text layer until you pass `--pdf-ocr`.
```bash
python make_book.py \
--book_name scan.pdf \
--to-epub \
--pdf-ocr \
--language zh-hans \
--use_context session
```
Without `--ocr-lang` the OCR engine reads its own default languages: on a Mac (ocrmac) English, Spanish, French and German, elsewhere (rapidocr) Chinese and English. A scan in another language needs `--ocr-lang`.
**If the scan already carries an OCR layer** (Internet Archive and ABBYY FineReader files often do), try it without `--pdf-ocr` first: the run prints `… selected pages carry only an invisible OCR text layer …` and uses that layer, which usually reads better than a fresh local OCR. If the layer turns out to be garbage, see the next tab.
`--img-model` does little on a scan: it cannot rebuild a page the layout detector shattered into fragments.=== "Scan with a bad text layer"
If `source.md` reads as nonsense although the page image is clear, the scan's text layer is wrong: recognised in the wrong language, or garbage from a poor OCR. `--ocr-replace-layer` drops it and reads every page with the OCR engine instead.
```bash
python make_book.py \
--book_name scan.pdf \
--to-epub \
--pdf-ocr \
--ocr-replace-layer \
--ocr-lang iso:zh \
--language en \
--use_context session
```
Put the scan's own language in `--ocr-lang` (`iso:zh` here, for a Chinese scan): in the wrong language the engine reads nothing. Try two pages first. A page the engine read nothing on is named and left empty; if no page was read, the run stops before any translation is paid for. Do not use it on a scan whose layer reads well: the fresh OCR reads worse than a good layer.=== "Chinese scan"
Name the language the OCR engine should read. An `iso:` tag works whichever engine runs: `iso:zh` for simplified characters, `iso:zh-Hant` for traditional. Not `ch_sim`: rapidocr refuses it before reading a page.
```bash
python make_book.py \
--book_name scan.pdf \
--to-epub \
--pdf-ocr \
--ocr-lang iso:zh \
--language en \
--use_context session
```
Horizontal text reads well. **Vertical text** (traditional books set top to bottom, right to left) comes out with its columns in the wrong order. Check `source.md` before you translate a vertical scan.By system
The route needs no GPU. The device changes the speed, never the text. Install from a clone of the repository; Installing the PDF extra explains every line.
=== "macOS (Apple silicon)"
```bash
pip install ".[pdf]"
```
Install natively; `--device auto` finds MPS and the run prints `PDF extraction device: mps.` Docker cannot reach MPS on a Mac.
```bash
python make_book.py \
--book_name paper.pdf \
--to-epub \
--use_context session
```=== "Linux with NVIDIA"
```bash
pip install ".[pdf]"
```
`--device auto` finds CUDA and the run prints `PDF extraction device: cuda.` To be sure, name it; the run then refuses, with the reason, if it cannot use it:
```bash
python make_book.py \
--book_name paper.pdf \
--to-epub \
--device cuda \
--use_context session
```=== "Windows with NVIDIA"
```bat
pip install ".[pdf]" ^
--extra-index-url https://download.pytorch.org/whl/cu126
```
PyPI's Windows wheel is CPU-only, so name PyTorch's CUDA index, and install the NVIDIA driver. Then run as on Linux.=== "CPU only"
On Linux, add PyTorch's CPU index (about 380 MB instead of about 3.2 GB of CUDA):
```bash
pip install ".[pdf]" \
--extra-index-url https://download.pytorch.org/whl/cpu
```
On Windows the plain `pip install ".[pdf]"` already gives the CPU build. The text is the same as on a GPU; extraction is slower (a two-page OCR scan: 26.3 s on the CPU against 10.6 s on Apple silicon's GPU).
```bash
python make_book.py \
--book_name paper.pdf \
--to-epub \
--device cpu \
--use_context session
```=== "Docker"
The `pdf` image tag carries Pandoc and the PDF packages. Keep the models in a volume so they download once.
```bash
docker run --rm \
-v "$PWD":/book \
-v bbm-models:/root/.cache \
-e OPENAI_API_KEY \
ghcr.io/yihong0618/bilingual_book_maker:pdf \
--book_name /book/paper.pdf \
--to-epub \
--use_context session
```
With an NVIDIA card on Linux or Windows (WSL2), use the `pdf-cuda` tag (amd64 only) and add `--gpus all`. See [Docker](../docker.md).What to expect from any PDF
- Figures stay pictures and their labels are not translated.
- Display equations are pictures cropped from the page: seen where they stood, not translated, not searchable. Inline mathematics is not covered.
- A sentence containing
\s,[u](y)or<k>can be refused before translation; escape it insource.mdand rerun. - The EPUB always carries the one-line AI-translation credit on this route;
--no_disclosureis not honored here. - A PDF is a page description, not a document: a heading one level off or a table that arrives as prose is the format showing through, and a minute's edit in
source.md.
En Features Session Mode (`docs/en/features/session-mode.md`)
Session mode
What it does
--use_context session keeps one conversation for the whole book. Every request adds to it, and every request carries it, so the model reads the last chapter or so before it translates the next paragraph. That is how names, register and terminology stay the same from page to page. On an endpoint with prompt caching, the history that was already sent is billed at the cache rate, so a long history costs much less than its size. The other context mode, bare --use_context (window mode), re-sends only the last few source/translation pairs with each request.
A history cannot grow for ever. When it reaches --context-compact-at (default 8192 estimated tokens, the opening summary included), the model writes a short handoff report, about 300 tokens: the names, the register and the terminology decisions so far. That report opens the next window. The latest one is saved in _handoff.md, and --resume reads it back. The default was chosen for continuity, not for price; Session compact budget has the measurements, including the fact that a lower value is cheaper.
Setup
Nothing to install. Session mode needs:
- an EPUB, Markdown or PDF book (a PDF on either route:
--to-epubtranslates Markdown, and the text route takes a session too). TXT and SRT never hand context to the model. - a route that keeps a history: the openai-shaped routes (OpenAI, gateways, groq, xai, litellm, local servers) and anthropic. Gemini and Qwen keep their own history and take bare
--use_contextinstead; they refusesession. The codex route is a session whether you ask or not: its thread is the history.
The flags:
| flag | what it does |
|---|---|
--use_context session |
Turn it on. |
--context-compact-at N |
The window budget in estimated tokens, handoff included. Default 8192, minimum 1500. Set it to your model's input limit when that is smaller. |
--no-context-compact |
Never ask for a handoff report. The window still rolls over at the budget, but the next one starts empty. |
--glossary FILE |
Pin renderings you stand behind (term -> translation lines). Only the terms that occur in a request are sent with it. |
--glossary-auto on |
Also keep the renderings the handoff reports establish. Off by default. It needs a model that answers with names rather than prose. |
Session mode is refused with --parallel-workers (one history cannot be shared between workers) and with --model_list (each model has its own cache, and one conversation would be written by several). --context_paragraph_limit belongs to window mode and is ignored here.
Recommended commands
If you translate a novel, add --use_context session and, when a recurring name drifts after a window seam, --glossary names.txt. If your model runs locally with a small context, set --context-compact-at to its input limit. If the endpoint has no prompt cache, use bare --use_context instead. The full list, by kind of book, endpoint and system, is on Recommended settings for EPUB; for a PDF, on Recommended settings for PDF.
What can go wrong
session: compacting at 8192 estimated tokens (the default; --context-compact-at overrides). Not a problem. The run says the budget it uses.cached=on the progress bar stays at 0 after a dozen requests. The endpoint has no prompt cache, and every request pays for the whole history at full price. Press Ctrl+C and rerun with bare--use_context.--use_context session outside plan mode leaves grouping off, so every paragraph is its own request and each one re-reads the whole history.An EPUB outside plan mode (for example--plan-classify none), or an SRT book on the codex route. Raise--accumulated_num. Markdown and PDF runs do not print this; there, a larger--batch_sizesends fewer requests.Error: --use_context session is not implemented for the gemini format; it would be accepted and ignored.Use bare--use_contexton Gemini and Qwen.--use_context session is not supported for txt books; it will be ignored.TXT and SRT carry no context.--parallel-workers is not supported with --use_context session: one history is the context, and a worker cannot share it.Choose one. Bare--use_contextkeeps the workers.a compact budget of N is too small for a session; use at least 1500 estimated tokens.Below 1500 a window is mostly the handoff that opens it. Use bare--use_contextif you want less context than that.ℹ handoff report failed (…); starting the next context window without a summary, or… keeping the current context and retrying on the next paragraph. One compaction produced no usable report. Translation continues. If the text after that seam drifts, pin the terms with--glossary.--glossary-auto on learns renderings from the handoff report a session writes when it compacts, and this run keeps no session to compact.Add--use_context session, or use--glossary, which needs no session.- The text drifts after a compaction on a small model. The handoff is written by the model, and a small model can write a poor one. Pin the names with
--glossary, or pass--no-context-compactand accept an empty window at each seam. --context-compact-ator--no-context-compactprinted asonly applies to --use_context session; ignoring it.You gave a compaction flag without a session.
En Formats Epub (`docs/en/formats/epub.md`)
EPUB
If you want a bilingual edition of an EPUB, this is the command most people need:
bbook_maker --book_name my_book.epub --language zh-hans --use_context session --quietIt writes my_book_bilingual.epub beside the input. Recommended settings for EPUB has what to change for a textbook, a small model or a local server.
EPUB is the format this tool knows best. Every feature works on it.
How the file is read
An EPUB is a zip of XHTML pages. The tool reads every page the book's spine lists.
- With an LLM route (the default): the book goes through plan mode. The loader finds every block that carries text (paragraphs, headings, list items, table cells, blockquotes, verse lines, captions), groups them by kind, and asks the model which kinds are worth translating. The answer is saved in
_plan.jsonand reused by later runs. Consecutive blocks then share one request, up to a token budget and a unit cap. - With a translation-service route and no
--classify-model, or with--plan-classify none: only the--translate-tagsselection is translated,by default. Verse or a table outsidethen stays in the source language. - Links, emphasis and other inline markup inside a paragraph are replaced by numbered markers before translation and put back afterwards, so the model never sees raw HTML.
- Content inside
--exclude-translate-tags(supandcodeby default) is never sent.
What you get
<name>_bilingual.epubbeside the input. Each translation sits right after its original, with the same tag and class. Ids and internal links are kept.- A one-line credit below the book's intro: "Translated by <model>, <year>."
--no_disclosureleaves it out. bbm_translation_metadata.jsoninside the book on plan and session runs: the model, the date, and the checksum of a--glossaryfile._plan.jsonbeside the input (the plan), and_handoff.mdon a session run (the latest handoff report).- On Ctrl+C or an error:
<name>_bilingual_temp.epuband a checkpoint. Rerun with--resume.
Recommended command
bbook_maker \
--book_name my_book.epub \
--language zh-hans \
--use_context session \
--quietPreview the plan first on an unusual book (a textbook, a bilingual edition, a book whose body is not in ``). It needs no key and translates nothing:
bbook_maker \
--book_name my_book.epub \
--plan-dry-runFlags that apply
Route and run
These work the same on every format.
| flag | what it does |
|---|---|
--book_name PATH |
The file to translate. The extension picks the format. |
-m, --model MODEL |
The model id, exactly as the endpoint names it. Default gpt-6-luna on the openai format. |
--key KEY |
API key; several comma-separated keys rotate. Falls back to BBM_API_KEY, then the format's own variable. |
--api_base URL |
The endpoint. Defaults to the format's official host. |
--api_format FORMAT |
The API the endpoint speaks, or a translation service. Inferred from --api_base when left out. |
--provider NAME |
A named endpoint from bbm_providers.json. |
--model_list IDS |
Several models to rotate across. Refused with --use_context session. |
--language LANGUAGE |
Target language: a tag, a name, or TAG:NAME. Default zh-hans. |
--source_lang LANGUAGE |
Source language, stated. Reaches every LLM prompt; sent as a field on qwen and customapi. |
--prompt VALUE_OR_FILE |
Custom prompt: user template (must contain {text}), system, style. |
--temperature FLOAT |
Sampling temperature, on the formats that take one. |
--no-thinking |
Ask the model not to reason before answering. The field is negotiated on the OpenAI-shaped routes; thinking: disabled on anthropic; refused on codex. |
--extra_body JSON |
Extra request-body fields on the openai and anthropic routes. |
--extra_headers JSON |
Extra HTTP headers on the openai and anthropic routes. |
--interval SECONDS |
Pause between requests. Only the gemini format uses it. |
-p, --proxy URL |
HTTP proxy for the run. |
--test |
Translate only the first paragraphs. |
--test_num N |
How many, with --test (default 10). |
--resume |
Continue an interrupted run from its checkpoint. |
--single_translate |
Write the translation only, without the original. |
EPUB flags
| flag | what it does |
|---|---|
--translate-tags TAGS |
Tags to translate when there is no plan (default p). Ignored in plan mode. |
--exclude-translate-tags TAGS |
Tags whose content is never translated (default sup,code; "" clears it). |
--allow_navigable_strings |
Also translate text that sits outside any tag. Redundant in plan mode. |
--only_filelist FILES |
Translate only these internal files (OPF-relative names, comma-separated). |
--exclude_filelist FILES |
Skip these internal files. Ignored when --only_filelist is given. |
--plan-classify MODE |
How the plan is decided: auto (default), none, all, model, agent. See Plan mode. |
--classify-model MODEL |
Classify with another model (default: the provider entry's classify_model, else the translating model). Typed, it puts the run in model mode, so a failure stops the run; it also gives a translation-service route a plan. --plan-classify-model is the old name. Ignored under --plan-classify all or agent. |
--classify-base-url URL |
Where that model is served, when it is not the run's endpoint (OpenAI-compatible only). |
--classify-key KEY |
The key for --classify-base-url. See which key goes where. |
--plan-dry-run |
Print the plan and write _plan.json without translating. No key needed. |
--plan-min-coverage FRACTION |
Stop when the plan covers less than this share of the text (default 0.5). |
--poetry-group-size N |
Deprecated; still works and warns. Use --max-batch-units. |
--accumulated_num N |
In plan mode, the token budget per request (derived when unset; 1 turns grouping off). Without a plan, characters accumulated per request. |
--max-batch-units N |
Plan mode: the most units in one request (default 16; 8 on an endpoint without a strict schema). |
--block_size N |
Without a plan: merge paragraphs into delimiter-translated blocks. Ignored while --accumulated_num is above 1. |
--sentence_mode |
Translate sentence by sentence. Not with a plan; ignored while --accumulated_num is above 1. |
--use_context [window|session] |
Carry earlier paragraphs: bare or window re-sends a few pairs; session keeps one history. See Session mode. |
--context_paragraph_limit N |
Window mode: how many pairs to re-send. |
--context-compact-at N |
Session mode: compact the history at this many estimated tokens (default 8192, minimum 1500). |
--no-context-compact |
Session mode: roll over without a handoff report. |
--glossary FILE |
term -> translation pins, sent with the requests they occur in. openai-shaped and codex routes. --terminology is the same flag. |
--glossary-auto on|off |
Keep the renderings a session's handoff reports establish. Off unless asked for. |
--parallel-workers N |
Translate several chapters at once (2 to 4 is useful). Refused with a session and on codex. |
--translation_style CSS |
CSS for the translated paragraphs. |
--translation_color COLOR |
Color only; --translation_style wins. |
--no_disclosure |
Leave out the one-line AI-translation credit, and the metadata file. |
--translation-metadata |
Write bbm_translation_metadata.json into a plain tag-mode run (plan and session runs write it anyway). |
--retranslate OUT FILE START END |
Retranslate a range of an existing bilingual EPUB. |
--quiet |
No progress bars or paragraph echoes; reports and errors still print. |
Not for this format
--batch_size: the EPUB loader groups with--accumulated_num; the run warns.--batch,--batch-use: refused on EPUB: the Batch API path is never reached there.--to-epub,--pdf-ocr,--ocr-replace-layer,--device,--ocr-lang,--pages,--no-formula-images,--pdf_layout,--img-model,--img-base-url,--img-key: PDF only.--to-epubon an EPUB stops the run; the others warn or do nothing (the three image flags: the run warns that only the PDF route has an image step).
En Formats Md (`docs/en/formats/md.md`)
Markdown
A Markdown file is translated prose block by prose block. Code, tables and front matter are kept as they are.
How the file is read
- YAML front matter, fenced code blocks, tables and lines that carry no prose are passed through untranslated.
- Headings and paragraphs are translation blocks.
- Blocks are sent in batches of
--batch_size(default 10), and a batch also stops at about 2000 characters. The heading path above a batch goes with it as a breadcrumb, so the model knows which section it is in. --use_contextworks here, in both window and session mode.
This is also the loader behind PDF to bilingual EPUB: a PDF is first turned into Markdown, then translated here.
What you get
<name>_bilingual.mdbeside the input: each block followed by its translation. With--single_translate, the translation only.- On a session run,
<name>_handoff.mdwith the latest handoff report. - On Ctrl+C or an error:
<name>_bilingual_temp.txtand a checkpoint. Rerun with--resume.
Recommended command
bbook_maker \
--book_name my_doc.md \
--language zh-hans \
--prompt prompt_md.json \
--use_context session \
--batch_size 20prompt_md.json in the repository is a prompt written for Markdown: it asks the model to keep the markup. There is no plan mode for Markdown, so in session mode every request re-reads the history. Raise --batch_size to send fewer, larger requests.
Flags that apply
Route and run
These work the same on every format.
| flag | what it does |
|---|---|
--book_name PATH |
The file to translate. The extension picks the format. |
-m, --model MODEL |
The model id, exactly as the endpoint names it. Default gpt-6-luna on the openai format. |
--key KEY |
API key; several comma-separated keys rotate. Falls back to BBM_API_KEY, then the format's own variable. |
--api_base URL |
The endpoint. Defaults to the format's official host. |
--api_format FORMAT |
The API the endpoint speaks, or a translation service. Inferred from --api_base when left out. |
--provider NAME |
A named endpoint from bbm_providers.json. |
--model_list IDS |
Several models to rotate across. Refused with --use_context session. |
--language LANGUAGE |
Target language: a tag, a name, or TAG:NAME. Default zh-hans. |
--source_lang LANGUAGE |
Source language, stated. Reaches every LLM prompt; sent as a field on qwen and customapi. |
--prompt VALUE_OR_FILE |
Custom prompt: user template (must contain {text}), system, style. |
--temperature FLOAT |
Sampling temperature, on the formats that take one. |
--no-thinking |
Ask the model not to reason before answering. The field is negotiated on the OpenAI-shaped routes; thinking: disabled on anthropic; refused on codex. |
--extra_body JSON |
Extra request-body fields on the openai and anthropic routes. |
--extra_headers JSON |
Extra HTTP headers on the openai and anthropic routes. |
--interval SECONDS |
Pause between requests. Only the gemini format uses it. |
-p, --proxy URL |
HTTP proxy for the run. |
--test |
Translate only the first paragraphs. |
--test_num N |
How many, with --test (default 10). |
--resume |
Continue an interrupted run from its checkpoint. |
--single_translate |
Write the translation only, without the original. |
Markdown flags
| flag | what it does |
|---|---|
--batch_size N |
Prose blocks sent in one request (default 10; a batch also stops at about 2000 characters). |
--use_context [window|session] |
Carry earlier paragraphs: bare or window re-sends a few pairs; session keeps one history. |
--context_paragraph_limit N |
Window mode: how many pairs to re-send. |
--context-compact-at N |
Session mode: compact at this many estimated tokens (default 8192, minimum 1500). |
--no-context-compact |
Session mode: roll over without a handoff report. |
--glossary FILE |
term -> translation pins (openai-shaped and codex routes). --terminology is the same flag. |
--glossary-auto on|off |
Keep the renderings a session's handoff reports establish. Off unless asked for. |
--parallel-workers N |
Translate several batches or sections at once. Refused with a session and on codex. |
Not for this format
--accumulated_num: not read by the Markdown loader; the run warns and points at--batch_size. Outside plan mode a session re-reads its history on every request, so a larger--batch_sizekeeps the request count down.--max-batch-units,--plan-classify,--plan-dry-run,--plan-min-coverage,--poetry-group-size: plan mode is EPUB only.--classify-model,--classify-base-url,--classify-key(and--plan-classify-model, the old name): nothing classifies on this format yet. The run warns that the flag is ignored.--exclude-translate-tags: accepted without a warning, but the Markdown loader does not read it.--translate-tags,--allow_navigable_strings: EPUB markup selectors; the run warns.--only_filelist,--exclude_filelist: EPUB internal files; ignored.--block_size,--sentence_mode: EPUB only; ignored.--translation_style,--translation_color: Markdown carries no style; the run warns.--no_disclosure,--translation-metadata: the output carries no credit line and no metadata file; the run warns.--retranslate: EPUB only; refused.--quiet: EPUB only; the run warns.--batch,--batch-use: the Markdown loader does not implement the Batch API.--to-epub,--pdf-ocr,--ocr-replace-layer,--device,--ocr-lang,--pages,--no-formula-images,--pdf_layout,--img-model,--img-base-url,--img-key: PDF only.--to-epubon this format stops the run; the others warn or do nothing (the three image flags: the run warns that only the PDF route has an image step).
En Formats Pdf (`docs/en/formats/pdf.md`)
A PDF can take two routes. Pick the first unless you want plain text.
| route | flag | output | needs |
|---|---|---|---|
| PDF to bilingual EPUB | --to-epub |
a reflowable EPUB with a table of contents, figures and display formulas as pictures | the PDF extra and Pandoc 3.1.12+ |
| Text route (older) | none | a bilingual .txt, plus an attempted EPUB |
the base install |
The `--to-epub` route
How the file is read
- docling reads the PDF's text layer with its layout and table models and writes Markdown:
<name>_book/source.mdand the images. - Pages with no text layer (a scan) are refused unless you pass
--pdf-ocr; then the OCR models read them. A page that has a text layer keeps it, unless you also pass--ocr-replace-layer: then every page is OCR'd and the PDF's own layer is not used. - With
--img-model, a vision model corrects the roles docling gave the page's regions (heading, caption, footnote, code) from the page image. - Heading levels are read from the page (numbering first, then font size and weight). Display formulas are cropped from the page as pictures.
- The Markdown loader translates
source.mdintobook_bilingual.md. - Pandoc builds the EPUB. Its navigation follows the headings.
A rerun of the same command reuses the extraction and a finished translation. Edit source.md before the translation runs if something came out wrong. Delete book_bilingual.md to translate again. PDF to bilingual EPUB has the recommended commands and the terminal lines to watch for.
What you get
<name>_book/beside the PDF:source.md,images/,book_bilingual.md, a manifest, and.work/.<name>_bilingual.epubbeside the PDF, a copy of the finished book.- In
source.md, acomment where each page starts, numbered as in the PDF file (from 1, as `--pages` counts), empty pages included. Items docling gave no page, which is rare, come last, under; the run says how many. - With
--pages 12-30:<name>_pages-12-30_book/and<name>_pages-12-30_bilingual.epub, so a chapter never overwrites the whole book.
Flags that apply
The route's own flags:
| flag | what it does |
|---|---|
--to-epub |
Take this route. |
--pdf-ocr |
Read pages with no text layer with the OCR models. Off by default; they are refused without it. |
--ocr-replace-layer |
With --pdf-ocr: OCR every page and use that text instead of the PDF's text layer. Off by default: a layer is kept and only pages without one are OCR'd. For a layer that is wrong (another language, garbage); on a clean scan the layer reads better. Toggling it reads the PDF again. |
--ocr-lang LANGS |
With --pdf-ocr: the languages the OCR engine reads, comma-separated, in the engine's own codes (rapidocr: ch, en, latin; easyocr: ch_sim, ja, ko; ocrmac: zh-Hans, ja-JP), or a BCP-47 tag behind iso: (iso:zh, iso:ja), which every engine accepts. rapidocr uses only the first language and refuses ch_sim: write iso:zh for Chinese. |
--device auto|cpu|cuda|mps|xpu |
Where the extraction models run. auto (default) detects an accelerator and falls back to the CPU. The CPU gives the same output, slower. |
--pages PAGES |
Only these pages, numbered from 1 (12-30, 1,3,5-7). |
--no-formula-images |
Leave display formulas as `` placeholders instead of pictures. |
--img-model MODEL |
A vision model that corrects region roles from the page image. Off unless named here or as the provider entry's img_model (the shipped openai entry names one); none turns that off. Never the translating model by fallback. About 3,000 prompt tokens per page. |
--img-base-url URL |
Where that model is served, when it is not the run's endpoint (OpenAI-compatible only). |
--img-key KEY |
The key for --img-base-url; defaults to the run's key on the run's own endpoint. |
--quiet |
No live progress line during extraction. |
Every other flag goes to the Markdown translation unchanged. These are the ones that do something there:
| flag | what it does |
|---|---|
--book_name PATH |
The file to translate. The extension picks the format. |
-m, --model MODEL |
The model id, exactly as the endpoint names it. Default gpt-6-luna on the openai format. |
--key KEY |
API key; several comma-separated keys rotate. Falls back to BBM_API_KEY, then the format's own variable. |
--api_base URL |
The endpoint. Defaults to the format's official host. |
--api_format FORMAT |
The API the endpoint speaks, or a translation service. Inferred from --api_base when left out. |
--provider NAME |
A named endpoint from bbm_providers.json. |
--model_list IDS |
Several models to rotate across. Refused with --use_context session. |
--language LANGUAGE |
Target language: a tag, a name, or TAG:NAME. Default zh-hans. |
--source_lang LANGUAGE |
Source language, stated. Reaches every LLM prompt; sent as a field on qwen and customapi. |
--prompt VALUE_OR_FILE |
Custom prompt: user template (must contain {text}), system, style. |
--temperature FLOAT |
Sampling temperature, on the formats that take one. |
--no-thinking |
Ask the model not to reason before answering. The field is negotiated on the OpenAI-shaped routes; thinking: disabled on anthropic; refused on codex. |
--extra_body JSON |
Extra request-body fields on the openai and anthropic routes. |
--extra_headers JSON |
Extra HTTP headers on the openai and anthropic routes. |
--interval SECONDS |
Pause between requests. Only the gemini format uses it. |
-p, --proxy URL |
HTTP proxy for the run. |
--test |
Translate only the first paragraphs. |
--test_num N |
How many, with --test (default 10). |
--resume |
Continue an interrupted run from its checkpoint. |
--single_translate |
Write the translation only, without the original. |
--batch_size N |
Prose blocks per request (default 10). |
--use_context [window|session] |
Carry earlier text. session is recommended: a PDF extracts into many short blocks. |
--context_paragraph_limit N |
Window mode: pairs to re-send. |
--context-compact-at N |
Session mode: compact at this many estimated tokens (default 8192, minimum 1500). |
--no-context-compact |
Session mode: roll over without a handoff report. |
--glossary FILE |
term -> translation pins (openai-shaped and codex routes). |
--glossary-auto on|off |
Keep renderings from handoff reports. Learns only when a compaction happens, so a short paper at the default budget learns nothing. |
--parallel-workers N |
Several batches at once. Refused with a session. |
--with-ocr and --no-gpu, the route's old spellings, still work as --pdf-ocr and --device cpu for one release and print a notice. They are not in --help.
Not for the `--to-epub` route
--no_disclosure: not honored yet; the credit line is always added.--translation-metadata: Pandoc builds this EPUB, which carries no metadata file and no embedded glossary.--pdf_layout: belongs to the text route.--accumulated_num,--max-batch-unitsand the plan flags (--plan-classify,--plan-dry-run,--plan-min-coverage): plan mode is EPUB only; the Markdown loader groups with--batch_size.--classify-model,--classify-base-url,--classify-key: nothing on this route classifies yet. The run warns that the flag is ignored.--translate-tags,--exclude-translate-tags,--allow_navigable_strings,--only_filelist,--exclude_filelist,--block_size,--sentence_mode,--translation_style,--translation_color,--retranslate: EPUB input only.--batch,--batch-use: not implemented on this route.
The text route (no `--to-epub`)
How the file is read
The text of every page is taken with PyMuPDF and split into lines. Lines are sent in groups of --batch_size (default 10), with earlier text when --use_context is on (window or session) and the pins of a --glossary. There is no structure: no headings, no figures, no tables.
What you get
<name>_bilingual.txtbeside the PDF.- An attempt at
<name>_bilingual.epubfrom the same text. If it fails, the TXT remains, so nothing is translated twice. - With
--pdf_layout:<name>_bilingual_<layout>.pdffortop-bottom,side-by-side, or both withall. This needs thereportlabpackage, which is not in the requirements; without it the run printspdf creation skipped: install reportlab firstand goes on. - On Ctrl+C or an error:
<name>_bilingual_temp.txt. Rerun with--resume.
bbook_maker \
--book_name paper.pdf \
--language zh-hans \
--batch_size 20Flags that apply
The route and run flags in the table above, plus:
| flag | what it does |
|---|---|
--batch_size N |
Lines per request (default 10). |
--pdf_layout none|top-bottom|side-by-side|all |
Also write a bilingual PDF in this layout (needs reportlab). |
Not for the text route
--parallel-workers: the run stays serial. The run warns.--pdf-ocr,--ocr-replace-layer,--device,--ocr-lang,--pages,--no-formula-images:--to-epubonly. The run warns and reads the whole file.--img-model,--img-base-url,--img-key: the image step belongs to--to-epub. The run warns.--classify-modeland its two companions: nothing classifies here. The run warns.- Every EPUB-only flag listed for the
--to-epubroute above, and--accumulated_num,--quiet,--no_disclosure,--translation-metadata.
En Formats Srt (`docs/en/formats/srt.md`)
SRT
Subtitles are translated block by block. Each block keeps its number and its timeline.
How the file is read
- The file is split into blocks at blank lines. A block is a number, a timeline and one or more lines of text.
- By default each block is its own request.
--accumulated_num Nputs consecutive blocks into one request, up toNcharacters (capped at 512). - The loader's own prompt tells the model to leave each block's number and timeline alone. If you replace the
usertemplate with--prompt, say that in your template too, or the answer will not parse as SRT. The run warns about this.
What you get
<name>_bilingual.srtbeside the input. Each block keeps its number and timeline, with the original text followed by the translation. With--single_translate, the translation only.- On Ctrl+C or an error:
<name>_bilingual_temp.srtand a checkpoint. Rerun with--resume.
Recommended command
bbook_maker \
--book_name test_books/Lex_Fridman_episode_322.srt \
--language zh-hans \
--accumulated_num 400Flags that apply
Route and run
These work the same on every format.
| flag | what it does |
|---|---|
--book_name PATH |
The file to translate. The extension picks the format. |
-m, --model MODEL |
The model id, exactly as the endpoint names it. Default gpt-6-luna on the openai format. |
--key KEY |
API key; several comma-separated keys rotate. Falls back to BBM_API_KEY, then the format's own variable. |
--api_base URL |
The endpoint. Defaults to the format's official host. |
--api_format FORMAT |
The API the endpoint speaks, or a translation service. Inferred from --api_base when left out. |
--provider NAME |
A named endpoint from bbm_providers.json. |
--model_list IDS |
Several models to rotate across. Refused with --use_context session. |
--language LANGUAGE |
Target language: a tag, a name, or TAG:NAME. Default zh-hans. |
--source_lang LANGUAGE |
Source language, stated. Reaches every LLM prompt; sent as a field on qwen and customapi. |
--prompt VALUE_OR_FILE |
Custom prompt: user template (must contain {text}), system, style. |
--temperature FLOAT |
Sampling temperature, on the formats that take one. |
--no-thinking |
Ask the model not to reason before answering. The field is negotiated on the OpenAI-shaped routes; thinking: disabled on anthropic; refused on codex. |
--extra_body JSON |
Extra request-body fields on the openai and anthropic routes. |
--extra_headers JSON |
Extra HTTP headers on the openai and anthropic routes. |
--interval SECONDS |
Pause between requests. Only the gemini format uses it. |
-p, --proxy URL |
HTTP proxy for the run. |
--test |
Translate only the first paragraphs. |
--test_num N |
How many, with --test (default 10). |
--resume |
Continue an interrupted run from its checkpoint. |
--single_translate |
Write the translation only, without the original. |
SRT flags
| flag | what it does |
|---|---|
--accumulated_num N |
Characters of subtitle blocks sent in one request (capped at 512 for SRT). |
Not for this format
--batch_size: not read by the SRT loader; group with--accumulated_num.--use_context: the SRT loader never hands context to the model, in either mode; the run warns.--context_paragraph_limit,--context-compact-at,--no-context-compact: context flags; nothing reads them here.--glossary,--glossary-auto: forwarded by the EPUB, Markdown and PDF loaders only; the run warns.--max-batch-units,--plan-classify,--plan-dry-run,--plan-min-coverage,--poetry-group-size: plan mode is EPUB only.--classify-model,--classify-base-url,--classify-key(and--plan-classify-model, the old name): nothing classifies on this format yet. The run warns that the flag is ignored.--translate-tags,--exclude-translate-tags,--allow_navigable_strings: EPUB markup selectors; the run warns.--only_filelist,--exclude_filelist: EPUB internal files; ignored.--block_size,--sentence_mode: EPUB only; ignored.--parallel-workers: the SRT run stays serial; the run warns.--translation_style,--translation_color: subtitles carry no style; the run warns.--no_disclosure,--translation-metadata: the output carries no credit line and no metadata file; the run warns.--retranslate: EPUB only; refused.--quiet: EPUB only; the run warns.--batch,--batch-use: the SRT loader does not implement the Batch API.--to-epub,--pdf-ocr,--ocr-replace-layer,--device,--ocr-lang,--pages,--no-formula-images,--pdf_layout,--img-model,--img-base-url,--img-key: PDF only.--to-epubon this format stops the run; the others warn or do nothing (the three image flags: the run warns that only the PDF route has an image step).
En Formats Txt (`docs/en/formats/txt.md`)
TXT
bbook_maker --book_name my_book.txt --language zh-hans --batch_size 20That writes my_book_bilingual.txt beside the input. SRT subtitles and Markdown have pages of their own below; a PDF has its own section.
A plain-text file is translated line group by line group. There is no structure to keep, so there are few options.
How the file is read
- The file is read as UTF-8 and split into lines.
- Lines are sent in groups of
--batch_size(default 10), joined by newlines. - A group that is empty, whitespace or only a number is copied without a request.
- No earlier text is sent with a request: the TXT loader has no context mode.
What you get
<name>_bilingual.txtbeside the input: each group of original lines followed by its translation. With--single_translate, the translation only.- On Ctrl+C or an error:
<name>_bilingual_temp.txtand a checkpoint. Rerun with--resume.
Recommended command
bbook_maker \
--book_name test_books/the_little_prince.txt \
--language zh-hans \
--batch_size 20A larger --batch_size means fewer requests and more context per request. Lower it if the model starts merging or dropping lines.
Flags that apply
Route and run
These work the same on every format.
| flag | what it does |
|---|---|
--book_name PATH |
The file to translate. The extension picks the format. |
-m, --model MODEL |
The model id, exactly as the endpoint names it. Default gpt-6-luna on the openai format. |
--key KEY |
API key; several comma-separated keys rotate. Falls back to BBM_API_KEY, then the format's own variable. |
--api_base URL |
The endpoint. Defaults to the format's official host. |
--api_format FORMAT |
The API the endpoint speaks, or a translation service. Inferred from --api_base when left out. |
--provider NAME |
A named endpoint from bbm_providers.json. |
--model_list IDS |
Several models to rotate across. Refused with --use_context session. |
--language LANGUAGE |
Target language: a tag, a name, or TAG:NAME. Default zh-hans. |
--source_lang LANGUAGE |
Source language, stated. Reaches every LLM prompt; sent as a field on qwen and customapi. |
--prompt VALUE_OR_FILE |
Custom prompt: user template (must contain {text}), system, style. |
--temperature FLOAT |
Sampling temperature, on the formats that take one. |
--no-thinking |
Ask the model not to reason before answering. The field is negotiated on the OpenAI-shaped routes; thinking: disabled on anthropic; refused on codex. |
--extra_body JSON |
Extra request-body fields on the openai and anthropic routes. |
--extra_headers JSON |
Extra HTTP headers on the openai and anthropic routes. |
--interval SECONDS |
Pause between requests. Only the gemini format uses it. |
-p, --proxy URL |
HTTP proxy for the run. |
--test |
Translate only the first paragraphs. |
--test_num N |
How many, with --test (default 10). |
--resume |
Continue an interrupted run from its checkpoint. |
--single_translate |
Write the translation only, without the original. |
TXT flags
| flag | what it does |
|---|---|
--batch_size N |
Lines sent in one request (default 10). |
Not for this format
--use_context: the TXT loader never hands context to the model, in either mode. The run warns; for a session it prints--use_context session is not supported for txt books; it will be ignored.--context_paragraph_limit,--context-compact-at,--no-context-compact: context flags; nothing reads them here.--glossary,--glossary-auto: forwarded by the EPUB, Markdown and PDF loaders only; the run warns.--accumulated_num: not read by the TXT loader; the run warns and points at--batch_size.--max-batch-units,--plan-classify,--plan-dry-run,--plan-min-coverage,--poetry-group-size: plan mode is EPUB only.--classify-model,--classify-base-url,--classify-key(and--plan-classify-model, the old name): nothing classifies on this format yet. The run warns that the flag is ignored.--translate-tags,--exclude-translate-tags,--allow_navigable_strings: EPUB markup selectors; the run warns.--only_filelist,--exclude_filelist: EPUB internal files; ignored.--block_size,--sentence_mode: EPUB only; ignored.--parallel-workers: the TXT run stays serial; the run warns.--translation_style,--translation_color: plain text carries no style; the run warns.--no_disclosure,--translation-metadata: the output carries no credit line and no metadata file; the run warns.--retranslate: EPUB only; refused.--quiet: EPUB only; the run warns.--batch,--batch-use: the TXT loader does not implement the Batch API.--to-epub,--pdf-ocr,--ocr-replace-layer,--device,--ocr-lang,--pages,--no-formula-images,--pdf_layout,--img-model,--img-base-url,--img-key: PDF only.--to-epubon this format stops the run; the others warn or do nothing (the three image flags: the run warns that only the PDF route has an image step).
Zh Evaluation Grouping Batch Size (`docs/zh/evaluation/grouping-batch-size.md`)
每次请求的单元数与 token 数:`--max-batch-units`、`--accumulated_num`
摘要
计划模式把若干单元(段落、诗行、单元格)放进一个请求。一个请求受两个上限约束:单元上限(--max-batch-units)和 token 预算(--accumulated_num)。两项付费研究考察了请求从哪里开始出错。一项在五个端点上做了 210 个请求的研究,发现所有请求级错误都出在 50 单元那一档。一项 923 个请求的扫描实验发现,第一个内容错误出现在每请求 64 个有效单元处,且只出现在文学散文上;在 1600 到 4800 token 的预算范围内,任何地方都没有错误。随附的默认值远低于这两者:16 个单元、1200 到 1600 token 的预算,在不校验严格 JSON schema 的端点上减半。这些默认值是所有者设定的安全余量,不是测出来的最优值。 测量表明,请求越大,每个译出的词越便宜;所有者选择了余量,放弃了这部分节省。
实验设置
退化研究(2026 年 9 月 4 日)。 210 个请求,5 个实验组,请求大小为 500、1000、2000、4000 和 8000 字符,每个请求最多 50 个单元。每种大小下各组的批次逐字节相同,由该分支自己的计划器从 childrens-literature、moby-dick、jlreq-in-english 和 mahabharata 构建。实验组:OpenAI 端点上的 gpt-4o-mini 和 gpt-5.6-luna,经转售商访问的 luna 和 claude-sonnet-5,以及经 OpenRouter 访问的 deepseek-v4-flash。错误按固定规则判定:尾部偷懒压缩、截断、数字保留。
错误出现扫描(2026 年 9 月 5 日)。 Codex 路线上 76 个有效单元格、923 个请求:46 个预定单元格,加上一个 30 单元格的扩展(扩到 64、96 和 128 个单元)——因为发现这条路线自己的减半机制把网格限制在了 24 个有效单元。四本 epub-samples 书:childrens-literature(文学散文)、moby-dick-mo(长篇小说)、epub30-spec(短小的技术单元)和 wasteland(诗歌)。两个模型:gpt-5.4-mini 和 Codex 默认模型。每个单元格都从生成的 EPUB 中回读检查。内容错误指译文放错位置、译文缺失,或文本中残留标记。
弱模型重跑,在 gpt-4o-mini 和 deepseek-chat 上重跑了两个维度。它来自 2026 年 9 月的分组报告及下面的两张图;其逐单元格表格没有保留。
结果
退化研究,各组最大的无错请求大小:
| 实验组 | 最大无错档位 | 修正后的说明 |
|---|---|---|
| off-mini(官方 4o-mini) | 1000 | 2000 及以上有真实错误(见下) |
| off-luna(官方 luna) | 8000,全部无错 | 它触发的规则 4 是中文数字导致的误报 |
| rtr-luna(nianhuaapi luna) | 8000,全部无错 | 与官方唯一的差别:延迟为 2.4–6× |
| rtr-sonnet(nianhuaapi claude-sonnet-5) | 无 | 0/30 返回合法 schema 的 JSON——是传输格式问题,不是翻译问题 |
| or-deepseek(OpenRouter deepseek-v4-flash) | ~4000 | 另有原文回显错误(见下),补全 token 消耗 6.6×,最慢 |
- 尾部偷懒压缩从未触发(最差 88%,阈值 70%)。210 个请求中零截断。39 处未匹配的数字串里有 36 处是正确的中文数字。
- rtr-sonnet 之外的所有请求级错误都出在 50 单元那一档。 起约束作用的是单元数,而不是字符预算。
- 一次无声的尾部错位:
gpt-4o-mini,8000 字符,50 个引文/出处交替的单元。ID 按顺序返回,但大约第 44 到 49 个位置的译文错位了一格。ID 回显发现不了这种情形。 - ID 回显抓到了它本来要抓的问题:一个 50 单元的 mahabharata 批次返回的 ID 只有 0 到 21,缺了 28 个段落,而端点报告的是成功。
错误出现扫描:
- 第一个内容错误出现在每请求 64 个有效单元处,只在 childrens-literature 的散文上,占单元的 9.4%。moby、spec 和 wasteland 直到 64 都无错。Codex 默认模型在 24 个单元时的 7.8% 来自单独一个错位请求,在 32、48 和 64 时都无错。
- 带来风险的是每个请求的内容量,而不是单元数。 48 个单元共 4705 个原文 token 无错;64 个单元共 3563 个 token 出错;64 个短行单元(600 到 1100 token)无错。
- token 预算在 1600 到 4800 范围内都没有错误。 超过约 2000 后上升的是重试成本:在 2400、单元数较大时,请求开销 +150%。
- 错位呈 U 形:两单元的批次在 4 本书中的 3 本上错位(无害;阶梯会降到单个单元),中间段大多无事,到 64 时重试又会错位。
- 扫描发现了一条真实的损坏路径:一次在对账后仍未被纠正的单格错位,把三个字面的
⟦spanN⟧token 写进了可见的正文。这已修复:这样的批次现在会沿减半阶梯下降,而不是被写入。
图:错误率和重试成本随单元数与预算的变化,76 个单元格,923 个请求。线条表示当时的默认值(32 个单元,1600 到 2000 的预算),即 9 月 7 日裁定之前。
弱模型重跑:调高单元上限,先付出的是重试,之后才是错误,所以该调低的是单元上限,而不是预算。
DeepSeek 在每种上限和预算下都压缩得很厉害(中/英字符比中位数 0.25 到 0.31,gpt-4o-mini 为 0.31 到 0.35)。这个模型上的译文偏短是模型本身的问题,不是分组错误。
图:弱模型重跑,来自 2026 年 9 月的分组报告;其逐单元格表格没有保留。
决定
9 月 5 日,扫描的负责人把上限设为 32(测得的错误出现点的一半),并出于重试成本的考虑保留了 2000 token 的预算上限。9 月 7 日,所有者调低了所有分组默认值:
| 常量 | 旧值 | 新值 |
|---|---|---|
GENERAL_GROUP_MAX_UNITS(plan.py) |
32 | 16(测得错误起点 64 单元的四分之一) |
SUBSTRICT_GROUP_MAX_UNITS |
16(派生值 //2) |
8(仍是派生值) |
substrict_batch_cap(chatgptapi) |
16 | 8 |
SESSION_BUDGET_FLOOR / CEILING |
2400 / 3200 | 1200 / 1600 |
SUBSTRICT_BUDGET_FLOOR |
派生值 1200 | 直接写定 800(有意不用 floor//2=600——不叠加两重余量) |
所有者记录在案的理由:schema 支持是端点的属性,不能证明模型会遵循指令;强模型在格式遵从上未必更好;而出错意味着模型无法把请求撑住。错位一格就是一本错书,而不只是一本贵书。所以安全余量优先于测得的按内容计算的节省(预算 800 时每个内容 token 折合 15.5 个输入当量,1600 时为 12.3)。
为什么在较弱的端点上减半:9 月 5 日的 JSON 对象评测发现的两次内容退步都是大批次,而没有严格 schema 判定的端点,无法与那种会出现回复计数错误的端点区分开来。所以在这种端点上,单元上限是 8,每请求预算是 800。
什么会改变它:重跑扫描,发现某个模型在 16 以下就出错(调低),或者所有者认为余量不如节省值钱(调高)。在那之前,运行打印 misaligned batches this run 时就调低 --max-batch-units;只为成本调高它,风险自负,而且绝不要超过 48。
局限
- 扫描中的“弱”模型是
gpt-5.4-mini,它不能代表弱模型。 它在格式遵从上异常强。因此,扫描得出的“弱模型比强模型更能守住回复格式”(childrens 的 9 个单元格中 2 次恢复,对 6 个中 5 次)这一结论,对真正的弱模型来说未经证实。gpt-4o-mini和 DeepSeek 的重跑才是弱模型的证据,而其逐单元格表格没有保留。 - 只有四本书,而且只有 childrens-literature 在 128 个单元时读到了正文;moby 和 spec 的测试片段都花在了前置内容上。
- 在本地模型上,16 到 64 个单元之间没有任何测量。
- token 预算只在 1600 及以上测得无错,而且只是在旧的 32 单元上限下。随附的 800 到 1600 有意落在测量范围之下。
Zh Evaluation Pdf Ocr Engines (`docs/zh/evaluation/pdf-ocr-engines.md`)
扫描页上的 OCR 引擎:`--ocr-engine` rapidocr、ocrmac、easyocr、tesseract
在这次测量之后,pdf 扩展开始安装 onnxruntime,并在 macOS 上安装 ocrmac,所以在全新安装上 rapidocr 和 ocrmac 都不再下载任何东西;下文关于安装的说明描述的是测量当时的安装情况。
摘要
--ocr-engine 选择 docling 在没有文字层的页面上运行哪个引擎。docling 能在本地运行的四个引擎,在同样的 20 页扫描页(14 页英文,6 页中文)上接受了测量:评分方法相同,要求识别的语言相同,全部在 CPU 上运行。在英文上,tesseract 的错误率最低(平均 CER 0.104),ocrmac 其次(0.136);rapidocr(0.169)和 easyocr(0.267)落在后面。在简体中文上,rapidocr 最好(五页的平均汉字 CER 为 0.054,ocrmac 为 0.097,tesseract 为 0.116);在仅有的一页繁体页面上,ocrmac 和 rapidocr 接近(0.043 和 0.060)。easyocr 几乎在所有地方都排最后,耗时是其他引擎的三倍,内存最多用到 6.6 GB,还在中文页面上丢了文字。Apple 自己的引擎 ocrmac 最快(每两页中位数 6 s),而且不需要下载任何东西。
默认值仍是 auto。本页是选择 OCR 引擎背后的依据。
实验设置
- 路线:PDF 路线自己的提取,通过分步脚本运行(
tools/pdf_to_book.py extract),docling 2.129.0,--pdf-ocr --device cpu --pages 1-2,默认 OCR 模式(在 docling 保留页面内嵌文字层的地方,文字层会被保留)。一次只做一个转换,每个都在内存上限之下运行,HF_HUB_OFFLINE=1。 - 引擎:rapidocr 3.9.2,运行在 onnxruntime 1.23.2 上(PP-OCRv6 small 模型,随 rapidocr 包一起提供);ocrmac 1.0.1(Apple Vision,macOS 26);easyocr 1.7.2,运行在 PyTorch 2.9.0(CPU)上;tesseract 5.5.3(Homebrew),使用
tessdata_fast语言文件(eng、chi_sim、chi_tra、osd)。 - 语言:所有引擎都用同一个
iso:标签,由 docling 映射成各引擎自己的代码:英文页面用iso:en,简体中文用iso:zh-Hans,繁体中文用iso:zh-Hant。每个单元格中,运行自己打印的那一行都确认了引擎和语言。 - 设备:每个单元格都用
--device cpu,所以四组的版面模型和表格模型都在 CPU 上运行。rapidocr(onnxruntime)、easyocr(PyTorch)和 tesseract 在 CPU 上运行;ocrmac 在 Apple 的 Vision 框架里运行,由框架自己选择硬件。 - 机器:Apple 芯片,16 GB,Python 3.12.12。
- 页面:每份都截成两页;计分的是有标准答案的那些页:
| 样本 | 计分页 | 内容 | 语言参数 |
|---|---|---|---|
| scan_cia_typewriter | 1, 2 | 质量较差的打字机稿扫描(CCITT) | iso:en |
| scan_cia_typewriter_mid | 2 | 打字机稿扫描,文档中部 | iso:en |
| scan_forward1963 | 1 | 1963 年的杂志,双栏,带脚注 | iso:en |
| scan_en_degraded_1_mid | 1 | 带表格的彩色小册子 | iso:en |
| scan_en_degraded_2_mid | 1 | 黑白二值的旧小说页面 | iso:en |
| scan_zh_hans_qm_mid | 1 | Internet Archive 的图书扫描,简体中文,带不可见 OCR 文字层和 JBIG2 蒙版 | iso:zh-Hans |
| zh_hant_scan_2_mid | 2 | 繁体中文,横排,带表格 | iso:zh-Hant |
| syn_arxiv_1706_03762_{clean,deg} | 1, 2 | 渲染成扫描件的原生数字论文 | iso:en |
| syn_wps_eula_{clean,deg} | 1, 2 | 渲染成扫描件的 WPS 许可协议 | iso:en |
| syn_web_zh_report_chrome_{clean,deg} | 1, 2 | 渲染成扫描件的中文网页报告 | iso:zh-Hans |
合成扫描件:200 dpi;干净版是无损 PNG,劣化版是灰度图,旋转 1.5°,模糊(r=0.8),JPEG 质量 q40。它们的标准答案是精确的(来自原页面的文字层)。真实扫描件的标准答案是对照页面图像人工录入的。中文图书扫描件带有 JBIG2 蒙版,所以每一组的页面图像都来自 pypdfium2(运行时打印了 JBIG2 那一行)。
- 评分:归一化:去掉 Markdown 语法(图片链接、注释、
#、表格竖线和分隔线、**、转义符、列表符号),反转义 HTML,NFKC,所有引号变体都映射为",所有连字符和破折号都删去,空白合并(中文则完全删去)。CER 对顺序敏感:一个段落读的顺序不同,也会计为错误。Han CER 是只计汉字的 CER。line_err 不计顺序:对标准答案的每一行取(1 − 部分匹配度),再按长度加权平均,所以它只衡量识别和检测本身。对于开头没有标题的提取结果,路线会在上方加一个标题(# <file name>),计分前把它去掉了。
命令,每个单元格一条($E 是评测目录,$ENGINE 是四个引擎之一,$LANG 取自上表):
HF_HUB_OFFLINE=1 TESSDATA_PREFIX=$E/tessdata \
python tools/pdf_to_book.py extract $E/fixtures/p12/scan_cia_typewriter.pdf \
--output $E/runs/K/$ENGINE_cia/bundle \
--pdf-ocr --ocr-engine $ENGINE --ocr-lang iso:en --device cpu --pages 1-2另外在每种文字各取一份样本,用 --ocr-engine auto 跑同样的命令,看看 auto 会选哪个。
结果
每页的 CER,越低越好;中文页面同时给出只计汉字的 CER:
| 页面 | rapidocr | ocrmac | easyocr | tesseract | auto |
|---|---|---|---|---|---|
| cia p1 | 0.158 | 0.119 | 0.073 | 0.120 | 0.119 |
| cia p2 | 0.272 | 0.109 | 0.148 | 0.110 | 0.109 |
| ciamid p2 | 0.188 | 0.029 | 0.080 | 0.017 | - |
| fwd p1 | 0.251 | 0.231 | 0.260 | 0.212 | - |
| deg1 p1 | 0.355 | 0.360 | 0.965 | 0.046 | - |
| deg2 p1 | 0.543 | 0.491 | 0.602 | 0.408 | - |
| zh p1 | 0.112 (Han 0.009) | 0.210 (Han 0.056) | 0.390 (Han 0.235) | 0.201 (Han 0.036) | 0.210 (Han 0.056) |
| hant2 p2 | 0.069 (Han 0.060) | 0.041 (Han 0.043) | 0.670 (Han 0.724) | 0.322 (Han 0.350) | 0.041 (Han 0.043) |
| synarxc p1 | 0.051 | 0.050 | 0.124 | 0.051 | - |
| synarxc p2 | 0.040 | 0.003 | 0.056 | 0.004 | - |
| synarxd p1 | 0.142 | 0.055 | 0.431 | 0.169 | - |
| synarxd p2 | 0.002 | 0.269 | 0.534 | 0.272 | - |
| syneulac p1 | 0.015 | 0.014 | 0.071 | 0.015 | - |
| syneulac p2 | 0.171 | 0.008 | 0.028 | 0.008 | - |
| syneulad p1 | 0.110 | 0.163 | 0.215 | 0.014 | - |
| syneulad p2 | 0.066 | 0.008 | 0.153 | 0.008 | - |
| synzhc p1 | 0.042 (Han 0.000) | 0.040 (Han 0.003) | 0.564 (Han 0.514) | 0.077 (Han 0.019) | - |
| synzhc p2 | 0.076 (Han 0.053) | 0.045 (Han 0.000) | 0.656 (Han 0.611) | 0.085 (Han 0.021) | - |
| synzhd p1 | 0.188 (Han 0.191) | 0.167 (Han 0.163) | 0.799 (Han 0.862) | 0.193 (Han 0.171) | - |
| synzhd p2 | 0.136 (Han 0.018) | 0.312 (Han 0.262) | 0.800 (Han 0.848) | 0.390 (Han 0.333) | - |
每页的 line_err(不计顺序):
| 页面 | rapidocr | ocrmac | easyocr | tesseract | auto |
|---|---|---|---|---|---|
| cia p1 | 0.085 | 0.001 | 0.045 | 0.003 | 0.001 |
| cia p2 | 0.181 | 0.076 | 0.110 | 0.078 | 0.076 |
| ciamid p2 | 0.093 | 0.007 | 0.036 | 0.000 | - |
| fwd p1 | 0.009 | 0.020 | 0.054 | 0.007 | - |
| deg1 p1 | 0.010 | 0.043 | 0.559 | 0.033 | - |
| deg2 p1 | 0.054 | 0.118 | 0.291 | 0.019 | - |
| zh p1 | 0.036 | 0.144 | 0.142 | 0.119 | 0.144 |
| hant2 p2 | 0.050 | 0.037 | 0.536 | 0.272 | 0.037 |
| synarxc p1 | 0.034 | 0.033 | 0.057 | 0.033 | - |
| synarxc p2 | 0.025 | 0.002 | 0.034 | 0.004 | - |
| synarxd p1 | 0.035 | 0.034 | 0.265 | 0.082 | - |
| synarxd p2 | 0.001 | 0.003 | 0.316 | 0.007 | - |
| syneulac p1 | 0.001 | 0.000 | 0.047 | 0.001 | - |
| syneulac p2 | 0.109 | 0.002 | 0.018 | 0.002 | - |
| syneulad p1 | 0.001 | 0.041 | 0.117 | 0.000 | - |
| syneulad p2 | 0.047 | 0.002 | 0.085 | 0.002 | - |
| synzhc p1 | 0.031 | 0.030 | 0.482 | 0.062 | - |
| synzhc p2 | 0.059 | 0.035 | 0.563 | 0.067 | - |
| synzhd p1 | 0.089 | 0.043 | 0.693 | 0.074 | - |
| synzhd p2 | 0.047 | 0.044 | 0.719 | 0.117 | - |
按类别求平均,CER(中文为 Han CER)/ line_err:
| 类别 | n | rapidocr | ocrmac | easyocr | tesseract |
|---|---|---|---|---|---|
| 打字机稿扫描,英文(cia p1、p2,ciamid p2) | 3 | 0.206 / 0.120 | 0.086 / 0.028 | 0.100 / 0.064 | 0.082 / 0.027 |
| 杂志和老印刷品,英文(fwd、deg1、deg2) | 3 | 0.383 / 0.024 | 0.361 / 0.060 | 0.609 / 0.301 | 0.222 / 0.020 |
| 合成扫描,英文,干净 | 4 | 0.069 / 0.042 | 0.019 / 0.009 | 0.070 / 0.039 | 0.019 / 0.010 |
| 合成扫描,英文,劣化 | 4 | 0.080 / 0.021 | 0.124 / 0.020 | 0.333 / 0.196 | 0.116 / 0.023 |
| 图书扫描,简体中文(zh,JBIG2) | 1 | 0.009 / 0.036 | 0.056 / 0.144 | 0.235 / 0.142 | 0.036 / 0.119 |
| 扫描,繁体中文,横排(hant2) | 1 | 0.060 / 0.050 | 0.043 / 0.037 | 0.724 / 0.536 | 0.350 / 0.272 |
| 合成扫描,简体中文,干净 | 2 | 0.027 / 0.045 | 0.001 / 0.033 | 0.562 / 0.523 | 0.020 / 0.065 |
| 合成扫描,简体中文,劣化 | 2 | 0.105 / 0.068 | 0.213 / 0.044 | 0.855 / 0.706 | 0.252 / 0.096 |
按引擎:
| 引擎 | 页数 | 平均 CER,英文(n) | 平均 Han CER,中文(n) | 平均 line_err,英文 | 平均 line_err,中文 | 每个两页单元格的提取耗时中位数(s) | 内存峰值 MB(最大) |
|---|---|---|---|---|---|---|---|
| rapidocr | 20 | 0.169 (14) | 0.055 (6) | 0.049 | 0.052 | 12 | 2248 |
| ocrmac | 20 | 0.136 (14) | 0.088 (6) | 0.027 | 0.056 | 6 | 1661 |
| easyocr | 20 | 0.267 (14) | 0.632 (6) | 0.145 | 0.523 | 37 | 6596 |
| tesseract | 20 | 0.104 (14) | 0.155 (6) | 0.019 | 0.119 | 10 | 1902 |
| auto | 4 | 0.114 (2) | 0.050 (2) | 0.039 | 0.090 | 12 | 1977 |
提取耗时取自运行自己打印的 PDF extracted: 2 pages, …, N s. 这一行:包括加载模型,以及两页的版面、表格和 OCR。各单元格:rapidocr 9–19 s,ocrmac 4–11 s,tesseract 7–21 s,easyocr 32–90 s(90 s 的那个单元格包含首次使用时的模型下载)。
auto 选了什么:3 个单元格中 3 次都选了 ocrmac(OCR engine: ocrmac (docling's choice on this install), …),因为装了 ocrmac;它的文字与 ocrmac 单元格完全相同。
有几处 CER 差距要用阅读顺序而不是识别来解释。synarxd p2:ocrmac 和 tesseract 的 CER 为 0.269 和 0.272,rapidocr 为 0.002,但 line_err 为 0.003 和 0.007:文字是对的,只是有一块放到了别处。deg1:rapidocr 和 ocrmac 的 CER 为 0.355 和 0.360,line_err 为 0.010 和 0.043。对一本书来说,段落放错位置是实实在在的缺陷,所以 CER 是主要分数;line_err 说明文字本身是否读对了。
easyocr 丢了文字。在中文合成页面上,753 个汉字它只返回了 108 到 370 个;在 deg1 上,3,518 个字符它只返回了 133 个。它也是唯一内存超过 4 GB 的引擎:简体中文那个单元格超过了 6,000 MB,被上限杀掉,改用 8,000 MB 上限重试时峰值为 6,596 MB。
安装与首次使用时的下载,在这台机器上测得:
| 引擎 | pdf 扩展之外还需安装的 |
首次使用时下载的 | 之后能否离线运行 |
|---|---|---|---|
| rapidocr | 无:rapidocr 3.9.2(31 MB)在扩展里,它的 onnxruntime 模型就在包里;onnxruntime(62 MB)不在扩展里 | 用 onnxruntime 时不下载(扫描过程中没有出现任何文件,无论 iso:en、iso:zh-Hans 还是 iso:zh-Hant);用 PyTorch 时(没有 onnxruntime 时 auto 退回的方式):从 modelscope.cn 下载 9.8 + 0.6 + 20.3 MB 的模型,观察到一次 |
能 |
| ocrmac | ocrmac 1.0.1 和 pyobjc(28 MB),仅限 macOS | 无:Apple 的 Vision 框架是 macOS 的一部分 | 能 |
| easyocr | easyocr 1.7.2(15 MB),外加 opencv-python-headless(98 MB)、scikit-image(28 MB)、networkx(7 MB)和一些更小的包:约 150 MB | 总会下载检测器 craft_mlt_25k.pth(83 MB),然后每种文字一个识别器:english_g2.pth 15 MB,latin_g2.pth 15 MB,zh_sim_g2.pth 22 MB,chinese.pth(繁体)226 MB,都是在这次测试中下载的。由 easyocr 自己从 GitHub 下载:HF_HUB_OFFLINE 拦不住它 |
能,从 ~/.EasyOCR/model 读取 |
| tesseract | tesseract 程序(Homebrew:25 MB,外加 leptonica 7 MB 和它依赖的约 35 个库) | 无:语言文件要手动安装,tessdata_fast 中的大小为 eng 4.1 MB,chi_sim 2.5 MB,chi_tra 2.4 MB,osd 10.6 MB |
能 |
决定
- 默认值仍是
auto:docling 取 ocrmac、rapidocr 和 easyocr 中第一个已安装的,并报出它用的是哪一个。--ocr-engine用来明确指定一个;没有安装的引擎,或在 macOS 以外指定的 ocrmac,会在读取任何页面之前被拒绝,并给出安装它的那条命令。 - 选择 OCR 引擎上的建议依据的是按类别的那张表:英文用 tesseract 或 ocrmac(老印刷品上 tesseract 领先),简体中文用 rapidocr,繁体中文用 ocrmac 或 rapidocr,只有在别的引擎都认不出那种文字时才用 easyocr。
pdf扩展在 macOS 上安装 ocrmac,所以auto会选它,无需下载任何东西;扩展还安装 onnxruntime,所以 rapidocr 用它自带的模型运行,而不用去下载 PyTorch 的模型。
局限
- 共 20 页,每个类别一到四页;真实中文扫描的几行各只有一页。同一类别上两个引擎的 CER 相差不到约 0.02 时,换一页就可能改变,所以都算作打平。这些引擎的结果是确定的:rapidocr 精确复现了之前那项研究中 CIA 页面的数字(0.158、0.272),
auto也精确复现了 ocrmac 单元格的结果。 - 没有测量日文、韩文和其他文字。这里也没有测量竖排中文。
- 每个单元格只运行一次;没有用 GPU。在 GPU 上,easyocr 的耗时会下降,内存则不会。
- rapidocr 是在 onnxruntime 上测量的。在 PyTorch 上(没有 onnxruntime 的安装中
auto退回的方式)没有计分:唯一的一次检查在下载之后就停止了。 - tesseract 需要把它的
configs文件夹放在语言文件旁边:当TESSDATA_PREFIX指向一个只有.traineddata文件的文件夹时,所有 tesseract 单元格都失败了(KeyError: 'text',tesseract 打印read_params_file: Can't open tsv)。把 Homebrew 的configs和tessconfigs文件夹链接进去就解决了;上面的数字来自重跑。 - 默认 OCR 模式:在简体中文扫描件上,docling 会同时读取它的不可见 OCR 文字层,所以引擎读出的文字与这层文字混在一起。这里没有测量
--ocr-replace-layer。 - 真实扫描件的标准答案由一个人录入。
Zh Evaluation Pdf Structure Llm Roles (`docs/zh/evaluation/pdf-structure-llm-roles.md`)
用图像模型判定区域角色:`--img-model`
摘要
docling 把每一页切分成区域(段落、标题、脚注、代码、表格、图片等),并排好阅读顺序。区域的角色或顺序一旦出错,书里就会显现出来:代码清单印成了脚注,第 2 节排在第 1 节前面,图题上方的插图不见了。这项研究在 85 页上整理了 docling 的错误目录,然后让一个视觉模型(gpt-5.6-luna)给它能看到的区域重新打标签。在原生数字页面上,模型修正了大部分错误标签。它修不了只有跨页才看得出的问题(页眉),也无法重建被 docling 打碎成碎片的页面。这些需要基于页面几何的规则,在任何模型之前运行。
角色校正已经实现:PDF 路线上的 --img-model。研究所要求的几何修复还没有实现,所以页眉和被打碎的页面仍按 docling 生成的样子输出。
实验设置
- docling 2.129.0,只用文字层(不做 OCR),在 43 个一到两页的切片上:共 85 页,其中 51 页 arXiv、16 页其他论文、4 页扫描件、14 页网页或 Word 页面。
- 错误目录: 每一页都渲染出来,画出并标注区域,每个错误都由人工记录类别和严重程度(book-breaking 毁书级、visible 可见、minor 轻微)。
- 角色校正: 12 页,每页至少有一个错误标签或页面装饰类错误。模型看到的是带标注的叠加图,以及区域 ID 列表,列出当前标签和前 80 个字符;它对每个 ID 回答一个标签,或
abstain。由脚本对照错误目录给答案打分;模型不自我评判。 - 整页探测: 3 页,各缺一个区域。模型只看到干净的页面,返回带框的区域;以 IoU 0.5 及以上与 docling 的区域匹配。
结果
错误目录:123 个错误,13 个毁书级。 照抄自记录:
| fault_class | arXiv (26 sl/51 pp) | paper (8 sl/16 pp) | scan (2 sl/4 pp) | web (7 sl/14 pp) | total | book_breaking | visible | minor |
|---|---|---|---|---|---|---|---|---|
| wrong_label | 8 | 11 | 5 | 7 | 31 | 1 | 26 | 4 |
| paragraph_merge_split | 11 | 5 | 5 | 9 | 30 | 8 | 13 | 9 |
| column_order | 6 | 2 | 1 | 3 | 12 | 2 | 8 | 2 |
| heading_level | 0 | 6 | 0 | 5 | 11 | 0 | 1 | 10 |
| furniture_as_body | 2 | 1 | 3 | 4 | 10 | 0 | 8 | 2 |
| spurious_region | 7 | 2 | 0 | 0 | 9 | 0 | 3 | 6 |
| missing_region | 1 | 2 | 0 | 3 | 6 | 2 | 2 | 2 |
| caption_attachment | 2 | 2 | 0 | 2 | 6 | 0 | 4 | 2 |
| duplicate_asset | 1 | 0 | 0 | 3 | 4 | 0 | 0 | 4 |
| misplaced_asset | 1 | 0 | 0 | 1 | 2 | 0 | 2 | 0 |
| body_as_furniture | 0 | 1 | 0 | 1 | 2 | 0 | 1 | 1 |
| all | 39 | 32 | 14 | 38 | 123 | 13 | 68 | 42 |
错误标签是最常见的类别,但只毁掉过一次书。被打碎或错误合并的段落毁书 8 次:一个内嵌 OCR 层在一页上变成了 165 个区域,行内数学式被拆成 49 个单字形条目,一段代码清单丢掉了所有换行,还有 docling 的跨页合并把一个段落粘到了作者信息块上。
缺失的文字不能用作触发条件。 85 页中有 78 页,落在所有区域之外的文字层字符为 0.00%,任何一页最多也只有 1.23%。缺失的插图或公式没有文字层字符可缺。真正有效的信号是几何上的:孤立的图题、只装着编号的公式框、本该是表格的地方只有一条细长的图片。
角色校正,12 页:
| 页面 | 区域数 | 标签错误 | 已修正 | 保留了错误标签 | 改成了另一个错误标签 | 弃权 | 引入的错误 | 额外的正确修改 | 提示 / 补全(推理)token | 延迟 s |
|---|---|---|---|---|---|---|---|---|---|---|
| arxiv_1906_00495v1_p34 p4 | 17 | 3 | 1 | 2 | 0 | 0 | 0 | 0 | 3442 / 239 (157) | 5.6 |
| arxiv_2009_12164v1_p12 p1 | 13 | 5 | 5 | 0 | 0 | 0 | 0 | 1 | 3346 / 199 (130) | 5.7 |
| arxiv_2010_03667v1_p12 p1 | 20 | 4 | 4 | 0 | 0 | 0 | 0 | 1 | 3431 / 351 (254) | 7.6 |
| latexbook_p12 p2 | 11 | 6 | 2 | 4 | 0 | 0 | 1 (13:text->key_value_region) | 1 | 3190 / 520 (460) | 9.0 |
| mixed_photo_code_1_p3031 p1 | 17 | 9 | 9 | 0 | 0 | 0 | 0 | 0 | 2958 / 165 (84) | 6.0 |
| mixed_photo_code_1_p3031 p2 | 9 | 1 | 1 | 0 | 0 | 0 | 0 | 0 | 2739 / 150 (100) | 5.3 |
| statement_p12 p1 | 24 | 4 | 1 | 2 | 1 | 0 | 1 (18:text->key_value_region) | 1 | 3555 / 625 (512) | 6.0 |
| statement_p12 p2 | 9 | 5 | 0 | 5 | 0 | 0 | 0 | 0 | 3145 / 143 (94) | 3.5 |
| web_en_chrome_1_p12 p1 | 21 | 15 | 15 | 0 | 0 | 0 | 0 | 1 | 3308 / 619 (512) | 10.6 |
| word_en_table_1_mid p1 | 16 | 1 | 0 | 1 | 0 | 0 | 0 | 0 | 3451 / 265 (186) | 6.9 |
| word_en_table_1_p12 p2 | 5 | 2 | 2 | 0 | 0 | 0 | 0 | 0 | 3018 / 29 (0) | 2.7 |
| zhqm_mid p1 | 164 | 11 | 0 | 9 | 2 | 0 | 0 | 0 | 7860 / 1014 (345) | 9.5 |
| 合计(12 页) | 66 | 40 | 23 | 3 | 0 | 2 | 5 | 43443 / 4319 | 78.4 |
上表 12 页中目录收录的标签错误,按模型的处理结果分类。共 66 个错误。
- 66 个中修正了 40 个。 除去银行对账单页面和 OCR 扫描件,是 46 个中的 39 个。
- 重复出现的页面装饰:8 个中修正 0 个。 银行对账单反复出现的页头和监管声明块,在单独一页上看起来就像正文。只有它们跨页重复这一点才能把它们标识出来。
- 被打碎的 OCR 页面:11 个中修正 0 个。 给碎片重新打标签并不能重建一页。
- 引入了 2 个错误,都是
text -> key_value_region,危害很小。另有 5 处修改是正确的(文档标题)。模型从未弃权,连 164 个区域的那一页也没有。 - 成本: 12 页共 43,443 个提示 token 和 4,319 个补全 token,每页 2.7 到 10.6 s。
整页探测,3 页: 模型找到了全部 3 个缺失的区域,标签也都正确(一张插图、一个带编号的公式、一张人口普查表)。它的框在 45 个区域中有 38 个与 docling 的匹配,平均 IoU 为 0.82 到 0.90,所以提出的框必须先吸附到页面自己的字形和图像边界上,才能从中裁剪任何东西。
识别,推迟。 表格:在 17 张表上,docling 的单元格 F1 为 0.888,模型为 0.854,其中 7 个标准答案按构造就等于 docling 的输出。docling 的公式解码在两页上跑了 27 分钟没有输出,被强行终止。
决定
这项研究得出的计划:
- 几何修复最先运行,这不是模型的活:被打碎的行和段落、跨页合并、单字形碎条、代码换行,以及通过跨页重复找出的页面装饰。
- 然后在清理过的区域 ID 上做角色校正。这是第一个模型步骤,因为它是代价最低的切入点,也是唯一测得有收益的一步。
- 对缺失区域做整页提议,只在三个几何触发条件之一标记的页面上进行,框要吸附并检查。
- 由模型判定阅读顺序尚未测试,仍是候选。表格和公式识别仍然推迟。
一次设计咨询(Codex,有记录)为第 2 步定下了约束:
- 在 OpenAI 风格的路线上加一个轻薄的图像适配层,复用现有的结构化输出阶梯;
- 角色修改是对条目的类型化替换,绝不是直接写标签;
- 页面装饰、表格、图片和列表不在第一轮范围内;
- 采纳的修改原子地应用;修改率异常的页面整页隔离,而不是削减(在首批真实运行后已改变,见下);
- 每个判定都写入
decisions.json以供审计; - 默认关闭。
实现了什么。 第 2 步,即角色校正,先于几何修复实现。模型看到的是标注了 docling 区域的渲染页面,在严格的逐区域 schema 下,为每个符合条件的区域回答一个标签(text、section_header、title、caption、footnote、code 或 abstain)。符合条件的区域是正文、标题和小标题,且没有被任何图题或脚注引用、不在表格单元格和公式之内。列表、公式、表格、图片和页面装饰都不会被问到。采纳的回答会把条目替换成正确类型的条目,然后才运行公式和标题两个步骤。
首批真实运行,gpt-5.6-luna,四个测试样本的第 1-2 页,照抄自记录:
| 测试样本 | 询问 | 采纳(已应用) | 保留 | 拒绝 | 隔离页数 | 调用次数 | 提示 / 补全 token | 秒 |
|---|---|---|---|---|---|---|---|---|
| arxiv_2010_03667v1 | 27 | 6 | 21 | 0 | 0 | 2 | 7001 / 368 | 9.9 |
| web_en_chrome_1 | 28 | 4 | 24 | 0 | 0 | 2 | 6930 / 365 | 8.8 |
| mixed_photo_code_1_p3031 | 15 | 1(另有 9 个被隔离) | 5 | 0 | 1 | 2 | 5538 / 177 | 6.2 |
| arxiv_1706_03762(留出样本) | 21 | 2 | 19 | 0 | 0 | 2 | 6645 / 322 | 7.8 |
隔离机制挡掉了它遇到的唯一一个毁书级修正:在那一页代码上,模型 10 个回答中对了 9 个,这超过了 60% 的修改阈值。于是所有者去掉了隔离。现在,所询问条目中超过 60% 被修改的页面会保留修改,并打印 Region roles: … on page N changed; read that page in source.md before translating. 去掉隔离后重跑,那八行清单导出为代码(八个独立的代码块,因为相邻的代码条目还不会合并)。留出的那篇论文,Markdown 中改动的行数为 0。
图像模型需要明确选择。 这一步只在你指定的模型上运行,通过 --img-model 或提供方条目的 img_model。翻译模型永远不会被退而用于图像(所有者裁定),所以没有哪个默认设置需要能读图的模型。随附的 openai 提供方条目写的是 gpt-5.6-luna,所以 --provider openai 会开启这一步;--img-model none 关闭它。这一步之前,会用一张图片探测模型能否看到图片;不能的话,保留 docling 的标签。
本地模型: 每页约 3,000 个提示 token 不算多,但 164 个区域的那一页用了 7,860 个,这超出了 8B 级模型的能力。用 --img-base-url 把这一步指向托管的视觉模型,或者干脆关掉。
局限
- 角色校正只在 12 页上测量,整页探测只有 3 页。记录中“错误类别对应修正”的表格只是一个方向,不是比率。
- 错误目录由一名工作者根据叠加图整理;arXiv 上的标题层级没有评判(另有一项研究专门覆盖它们)。
- 只运行了一个模型(gpt-5.6-luna)。没有尝试任何本地模型。
- 已实现的这一步只在四个测试样本的第 1-2 页上检查过;像 text 改成 footnote 这类只改标签的修改,目前在导出中还不产生任何变化。
- 错误目录中没有运行任何 OCR 引擎;两份扫描件是通过它们内嵌的文字层读取的。
Zh Evaluation Plan Classifier Jev (`docs/zh/evaluation/plan-classifier-jev.md`)
用 Jev 做计划分类器:`--classify-model`,对照 gpt-5.6-luna 测量
摘要
计划模式会就 EPUB 里的每一种块问模型:值不值得翻译。这是贴标签的活,不是写作的活,所以专门的分类器可以胜任。Jev 是 TypeSafe 的 System One 分类器,它回答类型化的选择题,并附带置信度。测试书的同样 31 个签名分别由 Jev 和 gpt-5.6-luna 分类。两者在 27 个上一致。4 处分歧全都是 luna 选了 translate 而 Jev 选了 skip,且每一处 Jev 的置信度都很低。Jev 用的提示 token 约为 luna 的两倍。第二轮让每页的共享文本只发送一次:Jev 的提示 token 减半,它与 luna 的一致程度的变化,没有超出同一份代码两次 Jev 运行之间的差异。同一轮还发现,Jev 概率上 0.5 的闸门在二选一的问题上永远不会触发;随后在一个 45 本 EPUB 的语料上测量了闸门,定为 0.95。结论:Jev 可以用作分类模型;默认仍是翻译模型。
实验设置
- 书:
test_books/animal_farm.epub,仓库里的测试书。它的计划有 31 个标签签名。 - 运行:
--test --test_num 8 --quiet --language zh-hans,用 gpt-5.6-luna 翻译,内存上限 1500 MB。 - luna 组:
--model gpt-5.6-luna --classify-model gpt-5.6-luna。该端点校验严格的 JSON schema,所以签名按页发送,每个请求包含若干个。 - Jev 组:
--model gpt-5.6-luna --classify-model typesafe-ai/jev --classify-base-url https://ai-gateway.vercel.sh/typesafe。每个签名一个 Choice 问题。在第一轮中,首选答案的置信度低于 0.5 时,Jev 回答unsure。 - 固定引擎单元格:
--api_format google --classify-model gpt-5.6-luna,检查翻译服务路线能否从大模型分类器得到计划。 - 比较内容: 两组在每个签名上的判定。没有人评判哪个判定是对的。
结果
照抄自记录:
| 单元格 | 分类 | 分类器用量行 |
|---|---|---|
| luna | 31 个签名,3 个请求,4 个跳过 | Classifier (gpt-5.6-luna at the endpoint's default host): tokens: in 7.6k, out 1.8k, cached 0 (3 requests) |
| Jev | 一次 503 重试,然后 31 个判定,8 个跳过 | Classifier (typesafe-ai/jev at https://ai-gateway.vercel.sh/typesafe): tokens: in 14.4k, out 1.1k, cached 0 (3 requests) |
31 个中有 27 个一致。 4 处分歧全都是 luna 选了 translate 而 Jev 选了 skip,而且 4 处都是 Jev 的低置信度判定(0.58–0.61):
| 签名 | 样本文本 |
|---|---|
inline:span.underline |
www.ericseat.com |
inline:span.calibre_4 |
www.ericseat.com |
inline:span.calibre_16 |
"M" |
inline:span.calibre3 |
George Orwell |
在两者一致的地方,Jev 的置信度大多在 0.9 以上。它用的提示 token 约为 luna 的两倍(14.4k 对 7.6k),因为每个签名都作为单独的问题发送。
固定引擎加大模型分类器。 谷歌翻译,以 gpt-5.6-luna 作分类器:llm classification: 31 verdict(s), 4 skip(s)、Translation plan: … coverage 99.8%,以及分类器的那一行 Classifier (gpt-5.6-luna …): tokens: in 7.6k, out 1.6k (3 requests)。译文紧挨着原文,并且与原文对应(George Orwell → 乔治·奥威尔)。
修正 key 路由之后的第二次 Jev 运行,重试了三次 503,最后是 llm classification: 31 verdict(s), 8 skip(s) 和 Translation plan: 20 documents, 202560 chars, coverage 99.7%。
第二轮:精简请求与闸门
原先每个 Jev 问题都会重复整段按签名生成的提示。精简映射把页面的共享文本作为 state 只发送一次,每个问题只带它自己的指令。没把握的 skip 现在回退为 translate,而不是 unsure。同一本书、同样的参数。照抄自记录:
| 实验组 | 分类器 token 输入 / 输出 | 请求数、每个的延迟 | 非 translate 判定 | 与 c_final(luna)一致 | 与 c_before(luna)一致 |
|---|---|---|---|---|---|
| (a) 之前,F 的映射,网关 | 14.4k / 1.1k | 3,经四次 503 重试之后(总耗时 36 s) | 7 | 27/31 | 26/31 |
(a) 仅精简映射之后(a_lean) |
7.8k / 1.1k | 3(总耗时 14 s) | 9 | 25/31 | 未计算 |
| (a) 最终,精简 + 闸门 | 7.8k / 1.1k | 3:0.87、0.37、0.67 s | 9 | 25/31 | 26/31 |
(b) luna schema(c_final) |
7.6k / ~1.4k | 3:5.07、6.03、4.15 s | 5 | — | 30/31 |
两次 luna 运行彼此一致 30/31,同一份代码的两次 Jev 运行(a_lean、a_final)一致 29/31。
精简映射让 Jev 的提示 token 减半(14.4k → 7.8k),并缩短了总耗时(36 s → 14 s,含无重试这一因素);一致程度的变化没有超出 Jev 自身运行之间的差异。
闸门的发现,照抄自记录:
官方 Jev 的
confidence字段不是所选选项的概率。在两个选项时,它约为2p − 1(0.51 → 0.02,0.61 → 0.22,0.76 → 0.53)。代码保留了 F 的选择,优先读取probabilities[choice]。在两个选项时它永远不会低于 0.5,所以 0.5 的闸门不起任何作用。离线重放
a_final:在概率上设 0.75 的闸门——相当于在 Jev 自己的confidence上设 0.5——会把全部 5 个低置信度的跳过变成翻译,保留 4 个有把握的。与 luna 的一致会升到 30/31。这只是一本书、31 个签名。
那只是一本书、31 个签名,所以 0.75 只是一个提示,不是设定值。随后在语料上测量了闸门:在所选选项的概率上取 0.95,依据是 45 本 EPUB 语料的 662 个计划签名对照 gpt-5.6-luna 的结果,丢失一个跳过记成本 10,多翻译一个记成本 1。取这个值时,Jev 的跳过约有十分之九回退为 translate,留下来的都是附属内容:版权行、行号、注释标记、索引页码。从不跳过的成本是 140,加闸门后是 115,所以在这个语料上,Jev 比全部翻译省得不多。
决定
- Jev 可以用作分类模型:在 TypeSafe 自己的地址上用
--classify-model jev,用网关的 ID 加--classify-base-url和--classify-key。见提供方文件与额外模型。 - 默认分类器仍是翻译模型。
- 第一轮:弃权阈值保持 0.5;四个低置信度的跳过只做了记录,没有评判(两个 URL、一个单独的字母和一个光秃秃的作者名,阅读版保留或翻译都说得通)。
- 第二轮:请求精简了(共享文本只发一次),阈值变成了不对称的闸门。低于它的
skip变成translate;translate不论概率多少都采纳。内容绝不会因为没把握的跳过而丢失。 - 闸门取所选选项概率的 0.95,来自上面的语料测量。
--classify-min-confidence P为单次运行调整它(BBM_JEV_MIN_CONFIDENCE不用参数也能做到同样的事;两者都有时以参数为准)。
什么会改变它:在更多的书上做比较,按读者想要的结果评判判定,并给出两组的价格。
局限
- 一本书、31 个签名,每轮每组只跑一次。两轮的一致率、精简映射带来的 token 减半,以及 0.75 的重放,都只基于这一本书。
- 只有闸门的取值来自 45 本 EPUB 的语料,而那里的一致也仍然只是与 luna 的一致。
- key 是网关的 key,所以 TypeSafe 自己的地址没有实际跑过(它对这个 key 回了 401)。
- 只比较了 token 数,没有比较金额。
- 与 luna 一致不等于正确:luna 的判定也没有被评判。
Zh Evaluation Session Compact Budget (`docs/zh/evaluation/session-compact-budget.md`)
会话压缩预算:`--context-compact-at`
摘要
在会话模式下,历史不断增长,直到达到 --context-compact-at,然后模型写一份交接报告,开启一个新窗口。预算短意味着接缝多;预算长意味着每个请求都要重读很长的历史。一项在三本书上做的 18 次付费运行研究,测量了成本随预算的变化。曲线在 1500 到 4000 之间是平的,之后上升,到 20000 时急剧上升。在 2500 的预算下,44 道接缝处的连续性都保持住了。9 月 7 日分组默认值减半之后,第二次测量发现 8192 的成本比 4096 更高:在一次 300 单元的运行中,相对旧默认值分别为 +39.8% 和 +27.3%。默认值仍然保持 8192,是为了连续性,而不是价格。 更低的值才是便宜的设置。
实验设置
成本研究(2026 年 9 月 5 日)。 18 次付费运行,OpenAI 端点上的 gpt-5.6-luna,共 3.267M token,--language zh-hans,--use_context session,每个单元格的规模约为 25 个请求。三本 epub-samples 书:childrens-literature(人名反复出现的文学散文)、moby-dick-mo(长章节)和 epub30-spec(短小、重复的技术单元)。
| 阶段 | 单元格 | 测量内容 |
|---|---|---|
| 0 | 3 本书,出厂默认值 | 校准:每请求开销 F、JSON 包装 j、交接提示 F_h、报告大小 K |
| 1 | C ∈ {1500, 4000, 8000, 20000} × 3 本书,B=800 | 成本随 C 变化的曲线 |
| 2 | moby-dick 上 B ∈ {400, 1600}(800 取自阶段 1) | 成本随 B 变化的曲线 |
| 3 | 一整本书,用解出的最优值(B=1088, C=2500) | 样本外验证 |
成本以输入 token 当量表示:(tokens_in − tokens_cached) + 0.1·tokens_cached + 4.0·tokens_out,即厂商的价格比例。当时的计量没有统计压缩轮次本身(在 C=1500 时最多占真实成本的 38%;此后已修复),所以每个总数都是计量成本加上重建的压缩轮次。
新默认值的价格核查(2026 年 9 月 7 日)。 animal_farm,gpt-5.6-luna,会话模式,300 和 150 个单元,新的分组(16 个单元,预算 1200 到 1600)在 C=4096 和 C=8192 下,对照 9 月之前的默认值(预算 2400 到 3200,C=8000)。
结果
9 月 5 日,每本书按价格加权的总成本:
| 书 | C=1500 | C=4000 | C=8000 | C=20000 | 解出的 C* |
|---|---|---|---|---|---|
| childrens-literature | 218.8k | 220.1k | 228.4k | 340.7k | 2512 |
| moby-dick-mo | 207.9k | 201.7k | 237.2k | 263.3k | 2191 |
| epub30-spec | 117.2k | 108.4k | 111.9k | 169.0k | 1580 |
- 三本书在 1500 到 4000 之间都平坦,差异在 0.6 到 3% 以内;三个解出的最优值都落在这一段。
- 结果记录认为 8000 比平坦区域高 4 到 18%。之后根据同样的单元格写成的分组报告认为,它比每本书自己的最优值高 9 到 25%。两者描述的是同样的运行,只是对照的基线不同。
- C=20000 的成本最多高出 56%,也是唯一出现漂移的单元格:在一个长窗口内,称谓从“你”变成了“您”。
- 短窗口下连续性依然保持。 C=2500 的整本书运行跨过 44 道接缝,1012 个节点中术语零断裂(皇帝 62/62,夜莺 38/38,公主 34/34,安徒生 12/12)。交接报告起到了不断累积的人名表的作用。
- 曲线背后的模型准确预测了整本书运行的请求数(82/82),成本预测误差在 −6.2% 以内。那次运行每个内容 token 的成本(11.7)低于网格中的每一个单元格(13.1 到 20.4)。
- 拟合常数:F=104,每单元 j=11.3,F_h=72,K=538(139 份报告),ρ 为 1.41 到 1.74,各书 g 为 1245/920/645,r 为 1.12 到 1.18,α 随 C 上升(childrens 上为 0.042 到 0.213)。
图:9 月 5 日的成本模型与单元格。图中标出的是当天的默认值(C=8000,预算限定在 1600 到 2000),即 9 月 7 日裁定之前。12000 和 16000 处的空心方块是模型预测。
9 月 7 日,新分组默认值的价格:
| 实验组 | 300 单元的请求数 | 全部 token | 压缩次数 |
|---|---|---|---|
| 旧默认值 | 33 | 283,361 | 8 |
| 新上限,C=4096 | 63 | 360,681 (+27.3%) | 19 |
| 新上限,C=8192 | 52 | 396,197 (+39.8%) | 9 |
150 个单元时:4096 为 +5.9%,8192 为 +24.0%。把 C 从 4096 提高到 8192,如预期那样让压缩次数回到了旧水平(19 降到 9,旧值为 8),账单却更高了。减半的预算让请求数成倍增加(33 变成 52 或 63),而每个请求都要重读携带的历史:每请求的提示负载分别为 7007(旧)、4690(4096 时)和 6537(8192 时)。缓存没能挽回:约 340k 的提示中只有 9k 到 21k token 被缓存。
图:9 月 5 日的成本模型(左)和 9 月 7 日的价格核查(右)。脚本:docs/img/src/session_compact_cost.py。
决定
--context-compact-at 在每次会话运行中默认都是 8192,不论是否分组,Codex 路线也一样。运行开始时会打印它。
理由是连续性,照顾的是在本地设备模型上运行本工具的众多用户,他们的成本曲线并不是这里测得的托管、带缓存的那一条。它不是便宜的设置。在当前的分组预算下,更低的值更便宜:300 个单元时,4096 用了 360,681 token,8192 用了 396,197。超过 16000 后成本急剧上升,而唯一观察到的漂移就出现在长窗口的单元格里。
什么会改变它:在变化后的分组预算下重新测量这两个单元格(4096 和 8192),或者在本地模型上做一次连续性测量。不重跑这两个单元格,就不要改动其中任何一个数字。
对你来说:不设置它,接缝最少。如果会话成本比接缝更要紧,就把它调低(往 4096 靠)。如果模型的输入上限更小,就设为那个上限。最小值是 1500。
局限
- 只有一个模型(
gpt-5.6-luna),在一个带提示缓存的端点上。没有测量无缓存端点和本地端点。 - 每个单元格只是一次约 25 个请求的运行;单次运行上约 30% 以内的差异接近噪声。
- 9 月 7 日的价格核查只用了一本书(animal_farm)的两种规模。
- 连续性结论只基于一整本书(childrens-literature,zh-hans)。
Zh Features Pdf Ocr Engines (`docs/zh/features/pdf-ocr-engines.md`)
选择 OCR 引擎
PDF 页面是扫描件时,--pdf-ocr 会让一个 OCR 引擎来读它。--ocr-engine 决定用哪一个。有四个引擎在你自己的机器上运行;它们擅长识别的内容不同,要安装的东西不同,是否需要下载也不同。
python make_book.py --book_name scan.pdf --to-epub --pdf-ocr \
--ocr-engine ocrmac --ocr-lang iso:en --language zh-hans先试两页(--pages 1-2),在为翻译付费之前读一读包里的 source.md。运行时会打印实际使用的引擎(OCR engine: …)。
需要 X,就用 Y
| 你的情况 | 使用 | 原因 |
|---|---|---|
| 用的是 Mac,并且不想下载任何东西 | ocrmac(装了 pdf 扩展就已经有了) |
Apple 自己的引擎,是 macOS 的一部分:不用下载模型。测量中最快(每两页 6 s),英文上排第二,在打字机页面和干净页面上与最好的引擎打平。简体中文不如 rapidocr。 |
| 还没想好 | auto(默认) |
取 ocrmac、rapidocr、easyocr 中第一个已安装的,并报出它的名字。装了 pdf 扩展时,在 Mac 上是 ocrmac,其他系统上是 rapidocr。 |
| 简体中文 | rapidocr |
简体中文上最好:平均汉字错误率 0.054,ocrmac 为 0.097,tesseract 为 0.116。 |
| 繁体中文(横排) | ocrmac 或 rapidocr |
在测量的那一页上两者接近(0.043 和 0.060)。tesseract(0.350)和 easyocr(0.724)远远落后。竖排文字无论用哪个引擎,列的顺序都会出错:请检查 source.md。 |
| 日文或韩文 | ocrmac 或 rapidocr,配合 --ocr-lang iso:ja 或 iso:ko |
没有测量。两个引擎都支持这两种语言;tesseract 需要安装 jpn 或 kor 语言文件。 |
| 英文打字机稿或其他质量较差的扫描件 | tesseract 或 ocrmac |
在打字机页面上打平(错误率 0.082 和 0.086;rapidocr 为 0.206)。在老印刷品和一份杂志上,tesseract 明显领先(0.222;ocrmac 0.361,rapidocr 0.383)。 |
| 没有 GPU 的机器 | 除 easyocr 以外的任何一个 |
这里的每个数字都是在 CPU 上测得的。easyocr 每两页要 37 s(其他引擎 6 到 12 s),内存最多用到 6.6 GB。 |
| Linux 或 Windows | 英文用 tesseract,中文用 rapidocr |
ocrmac 只在 macOS 上有。 |
| 上面这些都认不出的文字 | easyocr |
它有 80 多种语言的模型。在测量中,它在英文和中文上都排最后,还丢了文字。 |
各引擎的安装
| 引擎 | 安装 | 首次使用时下载 | 大小 |
|---|---|---|---|
rapidocr |
随 pdf 扩展一起安装,连同 onnxruntime。 |
不下载:它的模型就在包里。 | rapidocr 31 MB,onnxruntime 62 MB |
ocrmac |
在 macOS 上随 pdf 扩展一起安装(仅限 macOS)。 |
不下载。 | 28 MB |
easyocr |
pip install easyocr |
一个检测器(83 MB),外加每种文字一个模型:英文 15 MB,拉丁文 15 MB,简体中文 22 MB,繁体中文 226 MB,下载到 ~/.EasyOCR/model。 |
约 150 MB 的包 |
tesseract |
tesseract 程序及其语言文件,放在 PATH 上:macOS 上 brew install tesseract,Debian 和 Ubuntu 上 apt install tesseract-ocr tesseract-ocr-chi-sim,Windows 上用 tesseract 项目提供的安装程序。 |
不下载:要加一种语言,就安装它的文件(fast 系列中 eng 4 MB,chi_sim 2.5 MB,chi_tra 2.4 MB)。 |
程序 25 MB,另加它依赖的库 |
四个引擎装好之后都可以离线工作;easyocr 还需要先把模型下载好。
你指定的引擎如果没有安装,或者在 Linux、Windows 上指定了 ocrmac,会在读取任何页面之前被拒绝,并给出安装它的那条命令。auto 永远不会被拒绝。
如果你把 tesseract 的语言文件放在自己的文件夹里(TESSDATA_PREFIX),也要把 tesseract 的 configs 文件夹放进去:没有它,tesseract 写不出 docling 要读的那张表,提取就会失败。
语言代码
--ocr-lang 接受每个引擎自己的代码,也接受一个在所有引擎上都有效的通用 iso: 标签。换引擎时,应该用 iso: 标签。
| 语言 | 任何引擎 | rapidocr | ocrmac | easyocr | tesseract |
|---|---|---|---|---|---|
| 英语 | iso:en |
en |
en-US |
en |
eng |
| 简体中文 | iso:zh-Hans(或 iso:zh) |
ch |
zh-Hans |
ch_sim |
chi_sim |
| 繁体中文 | iso:zh-Hant |
chinese_cht |
zh-Hant |
ch_tra |
chi_tra |
| 日语 | iso:ja |
japan |
ja-JP |
ja |
jpn |
| 韩语 | iso:ko |
korean |
ko-KR |
ko |
kor |
rapidocr 每次运行只识别一种语言(第一个)。不写 --ocr-lang 时,每个引擎都识别它自己的默认语言:rapidocr 是中文和英语,ocrmac、easyocr 和 tesseract 是英语、西班牙语、法语和德语。这时其他文字的扫描件就会读错。
测量了什么
20 页扫描页(14 页英文,6 页中文),每个引擎都被要求识别同一种语言,全部在 CPU 上运行。错误率是与人工录入的文本对照得出的字符错误率:0 表示完全正确,段落放错位置也会计为错误。
| 页面 | rapidocr | ocrmac | easyocr | tesseract |
|---|---|---|---|---|
| 英文打字机稿(3) | 0.206 | 0.086 | 0.100 | 0.082 |
| 英文杂志和老印刷品(3) | 0.383 | 0.361 | 0.609 | 0.222 |
| 英文,干净的合成扫描(4) | 0.069 | 0.019 | 0.070 | 0.019 |
| 英文,劣化的合成扫描(4) | 0.080 | 0.124 | 0.333 | 0.116 |
| 简体中文图书扫描(1),汉字 | 0.009 | 0.056 | 0.235 | 0.036 |
| 繁体中文扫描(1),汉字 | 0.060 | 0.043 | 0.724 | 0.350 |
| 简体中文合成扫描(4),汉字 | 0.066 | 0.107 | 0.709 | 0.136 |
| 每两页耗时(中位数) | 12 s | 6 s | 37 s | 10 s |
各自在哪里胜出:
- 英文:tesseract,ocrmac 紧随其后。在打字机页面和干净页面上两者打平(差距不到 0.005)。tesseract 在老印刷品上的领先是实打实的。在劣化的合成页面上,rapidocr 数字更低是因为阅读顺序:不计顺序的话,三者对文字的识别一样好。
- 简体中文:rapidocr,平均值最好,5 页中有 3 页最好;ocrmac 在一张干净的合成页面上领先,ocrmac 和 tesseract 在一张劣化页面上领先。
- 繁体中文:ocrmac 和 rapidocr,在仅有的一页上差距在噪声范围内。
- easyocr 除打字机稿之外在每一类中都排最后,速度最慢,内存占用也最大。
每个数字、所用的页面、评分脚本和命令:扫描页上的 OCR 引擎。
Zh Features Pdf To Epub (`docs/zh/features/pdf-to-epub.md`)
PDF 转双语 EPUB
想把一份 PDF 做成带目录的双语书,先读两页,再跑整个文件:
python make_book.py --book_name paper.pdf --to-epub --pages 1-2 --test
python make_book.py --book_name paper.pdf --to-epub --use_context session两条命令之间,读一读 paper_pages-1-2_book/source.md。PDF 推荐设置按文档类型和操作系统给出了对应的命令。
这条路线在 arXiv 论文和一批图书、扫描件、浏览器另存的网页上检验过,并没有覆盖所有形态的 PDF。欢迎提 issue 和 pull request;如果 PDF 可以公开,请附上它,否则附上 source.md 里出错的那一页。
它做什么
--to-epub 用 docling 的版面模型和表格模型把 PDF 读成 Markdown,用 Markdown 加载器翻译这份 Markdown,再由 Pandoc 生成可重排的双语 EPUB。每一段后面紧跟它的译文,目录按标题生成。标题层级从页面本身读出:先看编号(1.、1.1、I.、A.),再看字号和字重。插图保留为图片,每个行间公式都从页面上裁成图片,放回原来的位置。没有文字层的页面(即扫描件)只有在传入 --pdf-ocr 时才交给 OCR 模型读取;扫描件自带的文字层有误时,可以用 --ocr-replace-layer 替换掉。如果用 --img-model 指定一个视觉模型,它会在写出 Markdown 之前逐页查看,纠正 docling 给各个区域定的角色(比如其实是作者行的标题、被读成脚注的代码清单)。
所有文件都放在 PDF 旁边的工作目录 <name>_book/ 里:source.md(提取结果)、images/、book_bilingual.md(译文)和一份清单。成书会复制到外面,名为 <name>_bilingual.epub。重跑同一条命令会复用已有的提取结果和已完成的翻译,所以你可以中途停下,读 source.md,改一个标题,再接着跑,不必付两次钱。PDF 是对页面的描述,而不是文档:它只记录字形放在什么位置,对段落和标题一无所知,所以任何提取器都只能把结构猜回来。标题差了一级、表格变成了正文,这是格式本身的局限,在 source.md 里花一分钟就能改好。
source.md 用一条注释 标出每一页的开头。N 是该页在 PDF 文件中的序号,从 1 开始数,也就是 `--pages` 用的页码,而不是页面上印的页码。空白页同样保留标记,所以编号不会错位。极少数情况下,docling 会返回不属于任何页面的条目。这些条目只写一次,放在 `source.md` 末尾的 下面,运行时会说明有多少条。它们在阅读顺序中的位置已经丢失:翻译之前,把它们移到该在的位置,或者删掉。
准备工作
- 在仓库的克隆里安装 PDF 扩展:
pip install ".[pdf]"(没有 NVIDIA 显卡的 Linux 和装有 NVIDIA 显卡的 Windows 还要加上 PyTorch 自己的索引,见安装 PDF 扩展)。此外,PATH 上需要有 Pandoc 3.1.12 或更新版本。不需要 Java。 - 模型(约 500 MB)在第一次运行时下载。
- 在跑更长的内容之前先读前两页;在为整份翻译付费之前,至少读一遍
source.md里的标题。它们会成为目录。
这条路线自己的参数:
| 参数 | 作用 |
|---|---|
--to-epub |
走这条路线。 |
--pdf-ocr |
读取没有文字层的页面。默认关闭:原生数字 PDF 本来就能直接读取,OCR 要多花好几倍的时间,读出的内容却不会变。无论开不开,版面、标题和表格检测都会运行。 |
--ocr-replace-layer |
配合 --pdf-ocr:对每一页做 OCR,用识别结果代替 PDF 自带的文字层。默认关闭:保留已有的文字层,只对没有文字层的页面做 OCR。适用于文字层有误的扫描件(识别成了别的语言,或者是劣质 OCR 留下的乱码);在干净的扫描件上,文字层比本地引擎读得更好。打开或关闭它,下一次运行都会重新读取 PDF。 |
--ocr-lang LANGS |
配合 --pdf-ocr:OCR 引擎要识别的语言,用引擎自己的代码。auto 在 Mac 上选 ocrmac,其他系统上选 rapidocr;pdf 扩展把两者都装好了,都不需要下载任何东西。测量数据见选择 OCR 引擎。各引擎的代码:rapidocr 用 ch、en、latin;easyocr 用 ch_sim、ja、ko;ocrmac 用 zh-Hans、ja-JP。在 BCP-47 标签前加 iso:(iso:zh、iso:ja、iso:zh-Hant)在所有引擎上都有效,所以不确定会用哪个引擎时,这是稳妥的写法。rapidocr 没有 ch_sim:中文请写 iso:zh。rapidocr 每次运行只识别一种语言,取第一个。运行时会打印实际使用的引擎和语言。 |
--ocr-engine ENGINE |
配合 --pdf-ocr:由哪个 OCR 引擎读取页面:auto(默认;取 ocrmac、rapidocr、easyocr 中第一个已安装的)、rapidocr、ocrmac(macOS)、easyocr 或 tesseract。指定的引擎没有安装时,在读取任何页面之前就会被拒绝。该选哪个,有测量结果:选择 OCR 引擎。 |
--device auto|cpu|cuda|mps|xpu |
模型在哪里运行。auto 会检测 CUDA 或 MPS,找不到就退回 CPU。CPU 得到的文字完全一样,只是更慢。 |
--pages 12-30 |
只处理这些页,从 1 开始编号(也可以写 1,3,5-7)。这本书会有自己的名字:<name>_pages-12-30_book/、<name>_pages-12-30_bilingual.epub。 |
--no-formula-images |
把行间公式留作 `` 占位符。几乎用不到:解析器从不读取公式,没有这些图片,数学内容就没了。 |
--pdf-image-dpi N |
插图的清晰度,按 PDF 自身页面尺寸计算的每英寸点数(72 到 600)。默认 200:在平板或高像素密度的电子书阅读器上足够清晰;小一些的书用 150,带细小标注的插图用 300。换一个值重跑,只会重画插图,提取结果和译文都保留。公式有自己的分辨率,不受影响。如果一张插图只是一幅嵌入的图片,而且这幅图片自己的分辨率更低,就保留它自己的分辨率;任何插图都不会超过 560 万像素。 |
--img-model MODEL |
根据页面图像纠正区域角色的视觉模型。只有在这里指定,或在提供方条目的 img_model 中指定时才开启;绝不会退而使用翻译模型。none 关闭条目里指定的模型。见用视觉模型纠正区域角色。 |
--img-base-url URL |
这个模型所在的地址,当它不在本次运行的端点上时使用(仅限 OpenAI 兼容接口)。 |
--img-key KEY |
--img-base-url 所用的 key。默认在本次运行自己的端点上使用本次运行的 key。 |
Markdown 加载器的所有参数都照常可用:--use_context session(推荐)、--glossary、--parallel-workers(不能与会话同用)、--no-thinking、--test。完整列表见 PDF 参数与纯文本路线。翻译阶段会拒绝的参数组合,例如在 codex 路线上用 --no-thinking,在提取开始之前就会被拒绝,所以不会先花钱。--classify-model 在这条路线上暂时不起作用,运行时会给出警告。
用视觉模型纠正区域角色
docling 把每一页切成若干区域并给它们打上标签:正文、标题、文档标题、图题、脚注、代码、表格、图片。有些标签是错的,成书里就会露出来:作者行出现在目录里,代码清单被排成正文,插图的标注被升格成一节。使用 --img-model 时,每一页都会连同 docling 画好并编号的区域一起交给模型。模型为每个区域给出一个角色(正文、标题、文档标题、图题、脚注、代码),或者弃权。被采纳的回答会在写出 Markdown 之前替换原来的标签,这样目录和代码块就能正确生成。文字本身绝不会被改写。
- 它修不了什么。 列表、表格、图片、公式以及页眉页脚都不在询问范围内。docling 已经把一张扫描页切成碎片时,重新打标签也拼不回来。
- 它的开销。 每页约 3,000 个提示 token;研究中每页耗时 2.7 到 10.6 s。在它背后的研究里,模型在 12 页上纠正了 66 个错误标签中的 40 个;见用图像模型判定区域角色。
- 它需要什么。 一个能接受图片的 OpenAI 兼容端点。运行时会先检查一次模型能否看懂图片。如果不能,运行时会说明这一点,并按 docling 自己的标签提取。
- 模型要明确指定,绝不自动推定。 只有用
--img-model或在提供方条目的img_model中指定了模型,这一步才会运行。自带的openai条目指定了gpt-6-luna,所以--provider openai会开启这一步;--img-model none可以关掉它。本地运行的翻译模型绝不会被要求去读页面。 - 改动它会重新提取。 图像模型及其地址属于提取结果的比对依据,所以添加、更换或去掉
--img-model都会重新读取 PDF。如果某个包的角色判定没有完成,而本次运行又要求做这一步,也会重新读取。
判定结果保存在 <name>_book/.work/extraction/decisions.json。提取结束时,运行会单独打印一行这一步的 token 用量:Image model (<model> at <address>): …。
手工修补译文
如果某一段译文有误,在 <name>_book/book_bilingual.md 里改正,然后做两步:
- 在仓库目录下运行
python tools/pdf_to_book.py export path/to/<name>_book。它会根据你的修改重新生成<name>_book/book_bilingual.epub;不会重新提取或翻译,也不需要 key。 - 用
<name>_book/book_bilingual.epub覆盖 PDF 旁边的<name>_bilingual.epub。导出只在工作目录里写文件,所以在你复制之前,PDF 旁边那本书里还是旧的文字。
推荐命令
每份 PDF 都从两页和几段译文开始(--pages 1-2 --test),然后读一遍 source.md。小说、教材、论文、扫描书、文字层有误的扫描件和中文扫描件各自的命令,以及 macOS、带 NVIDIA 显卡的 Linux、Windows、只有 CPU 的机器和 Docker 上的安装方式与设备选择,都在 PDF 推荐设置里。
已知限制
- 行内数学公式可能无法正确显示。只有行间公式会被裁剪成图片;句子里的数学式按提取器读到的文字原样输出。
可能出现的问题
这条路线上的每一种失败都会打印一行以 Error: 开头的信息,并且尽可能在花钱之前就打印出来。下面列出这些信息,以及该怎么做。
提取之前
Pandoc is required for --to-epub. Install it and make sure pandoc is on PATH.从 pandoc.org 安装 Pandoc 3.1.12 或更新版本。pandoc 3.1.3 is too old for EPUB export; Pandoc 3.1.12 or newer is required …你的 Pandoc 来自 apt(Ubuntu 24.04 自带 3.1.3,Debian 13 自带 3.1.11)。安装官方发布版,并把它放在 PATH 的最前面;这行信息还提到了另一种办法:分步脚本tools/pdf_to_book.py --pandoc PATH。reading a PDF needs the pdf extra, which is not installed.在克隆的仓库里运行pip install ".[pdf]"(安装 PDF 扩展的第 3 步)。不是pip install "bbook_maker[pdf]"。--device cuda was asked for, but the installed PyTorch is a CPU-only build.按 CUDA 的方式重新安装。… but this machine has no cuda accelerator available.改用--device cpu或--device auto。--parallel-workers is not supported with --use_context session …二者选其一。--no-thinking has no request to travel in on the codex route: …在 codex 上去掉--no-thinking。--img-model needs an OpenAI-compatible endpoint; … resolves to the … format.图像模型要在本次运行的端点或--img-base-url上询问,而那个端点不是 OpenAI 形式的。给一个 OpenAI 兼容的--img-base-url(以及--img-key),或者去掉--img-model。--img-base-url names where --img-model is served, and no --img-model was given. …把模型也写上。--ocr-replace-layer re-reads pages that already carry a text layer with the OCR engine, so it needs --pdf-ocr.加上--pdf-ocr。
提取期间
N of M selected pages have no text layer (page(s) …); rerun with --pdf-ocr to read them with the OCR models.这份 PDF(或其中一部分)是扫描件。加上--pdf-ocr。No --ocr-lang given: the OCR engine reads its own default languages, which may not be the pages'; …随后是OCR engine: rapidocr (docling's choice on this install), languages: the engine's defaults.检查source.md。如果扫描件不是中文或英文,用--ocr-lang重跑;包会被重新读取。The parser produced no text for a document whose pages have no text layer; the OCR pass returned pictures only.或Warning: no text was recognised on page(s) …引擎认不出这种文字。用--ocr-lang指定页面的语言后重跑。The parser returned no text for this PDF; there is nothing to translate. If its pages are scans, rerun with --pdf-ocr.照它说的做。Warning: page N extracted C characters, several times what a printed page holds; inspect source.md before translating.那一页上有什么东西(通常是一张插图)携带的文字远比它显示出来的多。一页的 Markdown 长达几千行,那是垃圾,不是长页面。把它从source.md里删掉,或者用--pages把这一页排除在外。The PDF carries JBIG2 image masks, which docling-parse renders wrongly (docling issue #4329); page images are rendered by pypdfium2 instead, the text layer still by docling-parse.提示信息。Internet Archive 和 ABBYY FineReader 生成的扫描件常用这种蒙版,docling-parse 会把页面画成一片模糊。此时整份文档的页面图像都改由 pypdfium2 生成。N of M selected pages carry only an invisible OCR text layer (a scanned book with recognised text underneath); that layer is kept as the page's text …提示信息。使用--pdf-ocr时,这层文字可能与 OCR 引擎读出的内容混在一起,所以请用--ocr-lang指明语言;语言不对的话,一页清晰可读的内容也会变成乱码。如果这层文字本身就是乱码,用--pdf-ocr --ocr-replace-layer加上语言重跑。OCR: replacing the embedded text layer on every page (--ocr-replace-layer)提示信息。没有--ocr-lang时,后面还会跟着语言提示。page N: the OCR engine read nothing where the PDF carried a text layer; …在--ocr-replace-layer下,这一页的识别结果为空,它就保持为空页:不会拿原来的文字层来顶替。这行信息也会写进清单的限制说明;超过十页后,其余的只计数(... and N more)。用--ocr-lang指定页面的语言后重跑,或者去掉--ocr-replace-layer以保留文字层。The OCR engine read nothing on any selected page, and with --ocr-replace-layer the PDF's text layer is not used, so there is nothing to translate; …运行在任何翻译付费之前停止。清单会记录这次失败的尝试:原因、空白页和所用的设置。常见原因是--ocr-lang写错了,或者扫描件的语言不在引擎的默认语言之内却没有写--ocr-lang。写对语言后重跑,或者去掉--ocr-replace-layer以保留文字层。N item(s) carry no page number; they are placed after the last page in source.md.很少见。docling 返回了不属于任何页面的条目。它们在source.md末尾的 `` 下面,在阅读顺序中的位置已经丢失。翻译之前,把它们移到该在的位置,或者删掉。… has no model for the OCR language 'ch_sim'. …引擎不认识这个代码;信息里列出了它认识的代码。在 rapidocr 上,中文请写iso:zh。这次什么都没有读取。… did not read the probe image (…); image steps are skipped this run.这个图像模型在那个端点上看不到图片。提取会继续,使用 docling 自己的标签。Region roles: A of B asked items changed by <model> (… kept, … rejected, … pages with many changes); overlay at .提示信息:图像模型改了哪些。Region roles: C of D asked items on page N changed; read that page in source.md before translating.一页中的大部分都被改了;这些改动会保留,所以请看一看那一页。Region roles: …; the detector's own labels stand there.这一步有一部分没有完成(预算用完、回答被拒绝、某个区域没有得到回答);这些区域保留 docling 的标签。Extracting again: the bundle's region-role pass is <status>; this run asks for --img-model …, which only a complete pass satisfies.提示信息。之前的判定没有完成,所以重新读取 PDF。--img-model <model> failed: …图像端点返回了一个等待也解决不了的错误,比如 key 被拒绝。运行停止。修正 key(--img-key)或地址。
提取之后、翻译之前
The document does not open with a top-level heading, so a heading "<name>" was added above its text; …EPUB 的目录要求开头有一个一级标题。愿意的话,在翻译之前到source.md里给它改个名。Page 12: the selection starts inside a section, so a heading "Page 12" was added above its prose; …同上,针对从某一节中间开始的--pages范围。The page selection is not one run of pages, so pages 1-7 were read and the ones outside the selection dropped afterwards; …有间隔的范围会读取它跨越的所有页。一个连续的范围读取的页更少。Display formulas kept as images: N. …提示信息:这些公式是图片,不会被翻译。Warning: a formula region on page N covers S% of the page, which is a layout mistake rather than an equation; …这个区域保留为占位符,而不是整页的图片。其他Warning: … formula …行表示某个公式无法放置,它的占位符会保留下来。Extraction: removed N control character(s) that docling read from glyphs on page(s) …; they are not text and would break the EPUB.提示信息。docling 把某些符号(通常是减号或约等号)读成了不可见的控制字符,它们已经从source.md中删去。这些符号在那几页上就缺失了;如果要紧,在source.md里补回去。Unsupported Markdown structure: control character U+0000 at line N. …source.md里仍有控制字符(包是在上面那行信息出现之前提取的,或者是手工编辑带进来的)。删掉它再重跑;此时还什么都没有翻译。Unsupported Markdown structure: raw tex '\s' at block 29 … Normalize the source before translation.提取器不会转义正文中的 Markdown,所以一句含有\s、[u](y)或<k>的话会被当成原始 TeX、链接或 HTML。在source.md里把它转义后重跑;提取不会重做。Reusing the extraction made with docling … on …; this run would use docling … on …. Delete the bundle directory to extract again.提示信息。这份提取结果来自另一个设备或另一个 docling 版本。
翻译与导出
Translation reused: … (same source and settings; delete it to translate again).提示信息。删除book_bilingual.md即可重新翻译。Error: translate failed: Source or translation settings changed; start a new translation bundle.你在翻译到一半之后改了模型、语言或source.md。用原来的设置重跑,或者把这个包移开,从头开始。Error: … Bilingual Markdown was edited; rebuild the EPUB from it with: python tools/pdf_to_book.py export <name>_book, then copy <name>_book/book_bilingual.epub over <name>_bilingual.epub.你手工编辑了book_bilingual.md。这是允许的,但运行不会覆盖它。按这行信息说的做两步(信息里带有完整路径):导出会根据你的修改在工作目录里重新生成成书,复制则把它放到 PDF 旁边;见手工修补译文。如果想重新翻译,删除book_bilingual.md。EPUB navigation is invalid: …这些标题构不成可用的目录。在source.md里修正标题层级(一个#文档标题,然后是##、###)后重跑。Interrupted. Rerun the same command to resume.按了 Ctrl+C。重跑即可;已完成的阶段不会重复。
运行结束后
Image model (<model> at <address>): tokens: …图像模型自己的用量,在提取之后打印,与翻译的用量分开。Nothing on this route classifies yet, so --classify-model is ignored on a … book.某个分类参数或提供方的classify_model传到了source.md的翻译阶段,而那里没有任何东西做分类。无害。
需要了解的限制
- 插图保留为图片,其中的标注不翻译。
- EPUB 中没有
bbm_translation_metadata.json,这条路线暂时也不支持--no_disclosure:署名行总会加上。 --glossary-auto只在发生压缩时学习,所以一篇短论文在默认预算下什么也学不到。- 在带有良好可见文字层的扫描件上,
--ocr-replace-layer读出的效果不如文字层(字符更少,还有明显的误读),所以只在文字层有误时使用它。在带有不可见文字层的扫描件上,两者给出的文字几乎相同。 - 带有不可见文字层时,单用
--pdf-ocr就已经会让 OCR 引擎读取页面,这就是上面中文扫描件的数字相近的原因。目前还做不到在开启 OCR 的同时原封不动地保留这样的文字层。 - 清单只在全部为空而停止的情况下记录失败的尝试。
Zh Features Plan Mode (`docs/zh/features/plan-mode.md`)
计划模式
它做什么
EPUB 把文字放在多种标记里:段落、标题、列表项、表格单元格、引文、诗行、图题。计划模式会把它们全部找出来。加载器把整本书切分成单元,按标签签名(标签及其 class)给单元分组,然后问模型哪些签名值得翻译。答案写进 _plan.json,同一本书之后的每次运行都会复用它。没有计划时,只翻译 --translate-tags 选中的标签(默认 ), 之外的诗歌或表格会一声不响地保留源语言。
计划模式还会给工作分组。相邻的单元在 token 预算(--accumulated_num)和单元数上限(--max-batch-units,默认 16)之内合成一次请求。每个请求都要求按 id 返回各个单元,所以回复里漏掉、合并或打乱了单元时,能被发现并拆成更小的部分重试,而不会让后面每一条译文都错开一个位置。对任何大模型路线上的 EPUB,计划模式默认开启,不需要加参数。这些默认值背后的数据见每次请求的单元数与 token 数。
准备
不需要安装任何东西。计划模式需要一本 EPUB 和一个用来分类的模型。在任何大模型路线上,这个模型就是翻译用的模型。翻译服务路线上没有模型可问,所以运行会回退到 --translate-tags 选中的标签,除非你用 --classify-model 指定一个分类器(见选择分类器)。
参数:
| 参数 | 作用 |
|---|---|
--plan-classify auto |
默认值。由翻译用的模型决定:端点经验证确实执行 JSON schema 时通过 schema,否则通过普通对话,用单个词 skip/translate/unsure 作答。回答 unsure 或无法解析时都翻译。 |
--plan-classify model |
与 auto 相同,但有无法确定的行时中止运行,而不是回退。 |
--plan-classify all |
翻译所有单元;不分类,不写计划文件。 |
--plan-classify none |
不用计划:翻译 --translate-tags 选中的标签,与计划模式出现之前一样。 |
--plan-classify agent |
写出带样本的计划,为编程智能体(或你本人)打印说明,然后停止。重新运行同一条命令开始翻译。 |
--classify-model MODEL |
用另一个模型,或一个 Jev 兼容的分类器(jev)来分类。在命令行上写明时,运行进入 model 模式,分类失败会中止运行。--plan-classify-model 是旧名字,仍然可用。 |
--classify-base-url URL |
该模型的服务地址,当它不是本次运行的端点时使用:一个 OpenAI 兼容地址,或一个 Jev 兼容的分类器的 URL。 |
--classify-key KEY |
--classify-base-url 使用的 key。 |
--plan-dry-run |
打印按签名统计的覆盖率表,写出所有决定都为空的计划,然后退出。不需要 key。 |
--plan-min-coverage FRACTION |
计划覆盖的文字比例低于这个值时中止(默认 0.5;0 关闭这项检查)。 |
--accumulated_num N |
每个请求的 token 预算。不设时由运行自动推算:用自带提示词时为 1200,自定义 --prompt 较长时最多 1600,在不支持严格 schema 的端点上为 800(会话模式运行保持未减半的值)。1 关闭合并。 |
--max-batch-units N |
每个请求最多包含的单元数。默认 16;在不支持严格 schema 的端点上为 8。 |
--exclude-translate-tags TAGS |
其内容从不发送的标签(默认 sup,code)。 |
--only_filelist, --exclude_filelist |
只为这些内部文件做计划,或跳过这些文件。预览运行同样遵守它们。 |
在计划模式下不要传这些参数:--translate-tags(会被忽略,计划已经覆盖一切)、--allow_navigable_strings(会被忽略)、--block_size 和 --batch_size(它们会重新切分计划已经切好的文字)。--retranslate、--batch 和 --sentence_mode 与计划相矛盾,会被拒绝。
选择分类器
分类器按以下顺序确定:--classify-model,然后是提供方条目的 classify_model,最后是翻译用的模型。
- **怎么问。**分类器的端点经验证支持严格 JSON schema 时,一个请求携带一页签名,回复受 schema 约束。其他情况下是普通对话:每轮五个签名,每个回答
skip、translate或unsure。连续两次回复不合格式,就降到每轮三个,再降到每轮一个。连一个都不行时,分类停止,剩下的签名全部翻译。每降一级都会打印一行。 - 单独的分类器(通过参数或提供方条目指定)会为整本书做计划,无论翻译路线本身能做什么。运行会打印
plan mode: on (classified by …)。翻译服务路线就是这样用上计划的。用 gpt-5.6-luna 当分类器、Google 翻译负责翻译,在测试书上完整跑过:31 个签名全部有了决定,覆盖率 99.8%。 - agent 模式不问任何模型。
--plan-classify agent把所有未决定的行交给你或你的编程智能体。指定的分类器不会预先填写计划,因为智能体面对预填的答案判断得更差;运行会警告它被忽略了。--plan-classify all也会忽略它。 - 在哪里问、用哪个 key,见提供方文件与额外模型。在单独地址上的分类器,会在运行结束时打印自己的用量行。
- Jev 是 TypeSafe 的分类器,专为这个问题而生:翻译还是跳过,每个请求一页签名,一次廉价的往返就够。
--classify-model jev就会使用它;TypeSafe 前面的网关(Vercel 的,或 Cloudflare AI Gateway)也可以。哪个 key 发往哪里,以及 URL 规则,见提供方文件与额外模型。 - **Jev 的闸门。**拿不准的
skip会改为translate,所以不会因它丢失内容。闸门设在 Jev 所选答案的概率 0.95 处,这个值是以 gpt-5.6-luna 为参照,在 45 本 EPUB 的 662 个计划签名上测出来的。在这个值下,Jev 的跳过约有十分之九会回退为translate;它仍然跳过的是校勘材料(版权行、行号、注释标记、索引页码)。在这个语料上,Jev 比全部翻译省不了多少。计划文件会在每个回退的行上注明(… below the gate: translate)。--classify-min-confidence P可以为一次运行调整闸门(BBM_JEV_MIN_CONFIDENCE不用参数也能做到);调低会保留更多 Jev 的跳过,风险自负。见用 Jev 做计划分类器。
=== "翻译服务路线加大模型分类器"
```bash
bbook_maker \
--book_name novel.epub \
--api_format google \
--classify-model gpt-6-luna \
--language zh-hans
```
分类器请求发往 OpenAI,读取 `OPENAI_API_KEY`。翻译仍由 Google 完成。=== "随附提供方文件里的 Jev"
```bash
bbook_maker \
--book_name novel.epub \
--provider openai-jev \
--use_context session
```
一行搞定:用 OpenAI 的 gpt-6-luna 翻译(`OPENAI_API_KEY`),用 Jev 为计划分类(`JEV_API_KEY`),两者都来自随附的 `bbm_providers.example.json` 里的 `openai-jev` 条目。=== "TypeSafe 上的 Jev"
```bash
bbook_maker \
--book_name novel.epub \
--model gpt-6-luna \
--classify-model jev \
--use_context session
```
读取 `JEV_API_KEY` 或 `TYPESAFE_API_KEY`。=== "经网关使用 Jev"
```bash
bbook_maker \
--book_name novel.epub \
--model gpt-6-luna \
--classify-model typesafe-ai/jev \
--classify-base-url https://ai-gateway.vercel.sh/typesafe \
--classify-key "$GATEWAY_KEY" \
--use_context session
```
必须写明 key:Jev 的环境变量只会自动发往 typesafe.ai 的地址。=== "经 Cloudflare AI Gateway 使用 Jev"
```bash
bbook_maker \
--book_name novel.epub \
--model gpt-6-luna \
--classify-model jev \
--classify-base-url https://gateway.ai.cloudflare.com/v1/$ACCOUNT_ID/$GATEWAY_ID/custom-typesafe \
--classify-key "$JEV_API_KEY" \
--use_context session
```
TypeSafe 是你的网关上的一个[自定义提供方](https://developers.cloudflare.com/ai-gateway/configuration/custom-providers/),slug 为 `typesafe`,base URL 为 `https://api.typesafe.ai`。网关开启了认证时,设置 `CF_AIG_TOKEN`;如果网关保存了 TypeSafe 的 key,就去掉 `--classify-key`。推荐命令
按书的类型(小说、教材、论文、词典)、按端点(托管、本地、翻译服务)和按系统给出的命令,见 EPUB 推荐设置。
可能出现的问题
下面是运行会打印的提示、它们的含义,以及该怎么做。
plan mode: on (endpoint verified strict JSON schema)、plan mode: on (no structured output here; classifying over a plain session)或plan mode: on (classified by …)。不是问题,只是说明计划将如何决定。plan: this endpoint missed the reply format twice at 5 per turn; continuing at 3 per turn。普通对话分类器降了一级。什么都不用做,只是多花几轮。plan: this endpoint missed the reply format twice even one at a time; classification stops here and the remaining N signature(s) are translated。模型守不住单个词的回答格式。书照样会翻译,而且宁多勿少。想要更好的计划,就用--classify-model指定一个更强的分类器,或者使用--plan-classify agent。… doesn't apply JSON schema (…), using delimiter method或… honors JSON schema shape but not value constraints; using the delimiter method for translation, schema kept for classification。不是问题。端点不做严格的 schema 解码,所以翻译改用分隔符格式。在 anthropic 路线、大多数代理和本地服务器上,这是预期行为。plan mode: off (…)。运行翻译--translate-tags选中的标签。括号里写明了原因:路线没有模型、给了--translate-tags,或者探测失败。如果这不在你意料之中,就看看原因。在不会自己做计划的路线上,运行会建议使用--plan-classify model。N misaligned batches this run — if this keeps happening, a lower --max-batch-units or --accumulated_num may fit this model better。从第三个恢复的批次起打印。模型在大请求上总是数错。没有丢失任何内容(每个出错的批次都拆成两半重试过了),但会多花请求。下次运行把单元数上限减半:--max-batch-units 8,然后4。Plan coverage X% is below the required 50.0% — refusing to translate a fraction of the book silently.计划会跳过书的大部分内容。打开_plan.json,看看哪些被标成了skip。词典或带大量校勘材料的书可能确实只需翻译较少的部分;这时就有意识地调低--plan-min-coverage。--classify-model names a classifier, and --plan-classify agent leaves every row to your agent and asks no model; it is ignored this run.(或… all translates the whole partition …)。如其所言;去掉这个参数或换个模式。--plan-classify model asks an LLM to rule on every plan signature, and the google format translates through one fixed engine with no model to ask. …固定引擎的运行处于model模式,却没有自己的分类器。加上--classify-model,或者改用agent或all。--classify-model needs an OpenAI-compatible endpoint; … resolves to the … format.--classify-base-url不是 OpenAI 形态。--classify-base-url names where --classify-model is served, and no --classify-model was given. …把模型也写上。No API key for the jev classifier at … Pass --classify-key, …Jev 的环境变量只在 typesafe.ai 地址上读取。经网关使用时,用--classify-key写明 key。BBM_JEV_MIN_CONFIDENCE must be a number from 0 to 1; got …改正或取消这个变量。_plan.json has N undecided signature(s) …。用过--plan-classify agent之后,还有一些行没有决定。把每个action都填成translate或skip,然后重新运行。…: invalid action '…' — use …。手工编辑计划时打错了字。改正 JSON 后重新运行。- **续跑因指纹信息被拒绝。**写下检查点之后,书或计划发生了变化。用原来的参数重新运行,或者删掉检查点从头开始——前提是清楚哪些部分要重新付费。
- **计划跳过了你想要的内容。**删掉
_plan.json再运行一次,或者手工编辑其中的action字段。--test运行会为整本书分类,而不只是那一小段,所以它得出的计划就是真正的计划。
Zh Features Recommended Epub (`docs/zh/features/recommended-epub.md`)
EPUB 推荐设置
对大多数书来说,整条命令就是这样:
bbook_maker \
--book_name my_book.epub \
--language zh-hans \
--use_context session \
--quiet书会按计划翻译(找出每一块文字,由模型决定哪几类值得翻译),会话让人名和术语保持一致。本页其余部分讲的是:当你的书、你的模型或你的机器不属于常见情况时,该改什么。
需要什么,就传什么
| 如果你需要…… | 传入 | 延伸阅读 |
|---|---|---|
| 几乎不花钱先看一眼 | --test --test_num 8 |
快速开始 |
| 付费之前先看哪些会翻译、哪些会跳过(不需要 key) | --plan-dry-run |
计划模式 |
| 每一块都翻译,不做分类 | --plan-classify all |
计划模式 |
| 自己或借助编程智能体决定计划 | --plan-classify agent,然后重新运行同一条命令 |
计划模式 |
| 全书必须统一的人名和术语 | --glossary names.txt |
会话模式 |
| 你自己的文风或语域 | --prompt my_prompt.json |
提示词文件 |
| 只翻译部分章节 | --only_filelist ch03.xhtml,ch04.xhtml(EPUB 内部的文件名) |
EPUB |
| 长书翻得更快,一致性其次 | --parallel-workers 4 配合不带值的 --use_context(会话无法在多个 worker 之间共享) |
EPUB |
| 在视觉上把译文区分出来 | --translation_style "color:#808080;font-style:italic" |
命令行参数 |
| 只要译文,不要原文 | --single_translate |
EPUB |
| 不要 AI 翻译署名行 | --no_disclosure(这样读者就无法再分辨译文是否出自人手) |
EPUB |
| 按了 Ctrl+C 或崩溃后继续 | 同一条命令加上 --resume |
快速开始 |
按书的类型
=== "小说"
就用本页开头的命令。如果某个反复出现的人名在窗口接缝之后译法漂移了,用术语表文件把它固定下来:
```bash
bbook_maker \
--book_name novel.epub \
--language zh-hans \
--use_context session \
--glossary names.txt \
--quiet
```=== "带表格和公式的教材"
先预览计划。教材会把文字放在表格单元格、图题和侧栏里,你会希望在付费之前看到哪些会被跳过。
```bash
bbook_maker \
--book_name textbook.epub \
--plan-dry-run
```
如果表格里有你想要却没被选中的文字类型,就改为翻译所有单元而不做分类,并把需要在几百页里保持统一的术语固定下来:
```bash
bbook_maker \
--book_name textbook.epub \
--language zh-hans \
--plan-classify all \
--use_context session \
--glossary terms.txt \
--quiet
```
EPUB 里的公式是 MathML 或图片。MathML(`<math>`)和 SVG 从不发给模型,所以公式保持原样。=== "论文"
EPUB 格式的论文用与小说相同的命令。PDF 格式的论文见 [PDF 推荐设置](recommended-pdf.md)。=== "词典或校勘本"
一本以校勘材料为主的书(词头、版本符号、行号),需要翻译的文字本来就不到一半,运行会在覆盖率检查处中止。先用 `--plan-dry-run` 预览,再有意识地调低这项检查:
```bash
bbook_maker \
--book_name edition.epub \
--language zh-hans \
--plan-min-coverage 0.2 \
--use_context session
```=== "扫描件,或中文扫描件"
扫描件是 PDF,而计划模式只用于 EPUB,见 [PDF 推荐设置](recommended-pdf.md)。把中文 EPUB 译成英文,按小说处理即可:
```bash
bbook_maker \
--book_name chinese_novel.epub \
--language en \
--use_context session
```按模型和端点
| 如果你的端点是…… | 传入 | 原因 |
|---|---|---|
| OpenAI、Anthropic,或支持提示缓存的网关 | --use_context session |
历史按缓存价重读 |
不支持提示缓存的端点(进度条上的 cached= 一直是 0) |
不带值的 --use_context |
会话会在每个请求里为整段历史付全价 |
| Gemini 或 Qwen | --api_format gemini --use_context(或 qwen) |
它们自己维护历史,并拒绝会话模式 |
| 你的 ChatGPT 套餐 | --api_format codex,不加上下文参数 |
线程本身就是上下文;见用大模型翻译 |
| 翻译服务(Google、DeepL……) | --classify-model gpt-6-luna |
引擎无法分类,没有大模型分类器时只翻译 ``;见翻译服务 |
| 小型本地模型(8B 到 16B) | 用默认值;推理模型加 --no-thinking,并把 --context-compact-at 设为模型的输入上限 |
在不支持严格 schema 的端点上,运行会自动把请求大小减半;见本地模型 |
总是错位的模型(N misaligned batches this run) |
--max-batch-units 8,然后 4 |
请求更小;见每次请求的单元数与 token 数 |
| 计划分类总是失败的小模型 | --plan-classify all |
跳过分类,翻译每一块 |
按系统
活是在端点上干的,所以用托管端点时,每个系统上的命令都一样。只有模型在本地运行时,系统才有影响。
=== "macOS(Apple 芯片)"
通过 Ollama 运行本地模型,它使用 Metal。把会话窗口限制在小模型的上下文长度之内:
```bash
bbook_maker \
--book_name novel.epub \
--api_base http://localhost:11434/v1 \
--model qwen3:8b \
--use_context session \
--context-compact-at 4000
```
小型本地模型不支持严格的 JSON schema,所以运行会自动把单元数上限减半到 8。=== "带 NVIDIA 显卡的 Linux"
在 GPU 上运行一个提供 OpenAI 兼容 API 的本地服务器(vLLM、llama.cpp、Ollama):
```bash
bbook_maker \
--book_name novel.epub \
--api_base http://localhost:8000/v1 \
--model your-model-id \
--use_context session \
--context-compact-at 4000
```=== "仅 CPU"
在 CPU 上用本地模型翻完一整本书很慢,而每个请求都重读一遍历史就更慢了。请使用支持提示缓存的托管端点:
```bash
bbook_maker \
--book_name novel.epub \
--model gpt-6-luna \
--use_context session
```=== "Docker"
```bash
docker run --rm \
-v "$PWD":/book \
-e OPENAI_API_KEY \
ghcr.io/yihong0618/bilingual_book_maker:latest \
--book_name /book/novel.epub \
--use_context session \
--quiet
```
见 [Docker](../docker.md)。Zh Features Recommended Pdf (`docs/zh/features/recommended-pdf.md`)
PDF 推荐设置
每份 PDF 都用同样的方式开始:两页、几段译文,然后读一读产出的内容。
python make_book.py \
--book_name book.pdf \
--to-epub \
--pages 1-2 \
--test它只提取这两页,几乎不花钱。打开 book_pages-1-2_book/source.md,至少读一遍标题:它们会成为目录。一页的 Markdown 长达几千行,那是某张插图带出来的垃圾,不是长页面。看起来没问题,就运行下面与你的文档对应的命令。如果有不对的地方,在 source.md 里改好再重跑;提取结果会被复用。
这条路线需要 PDF 扩展和 Pandoc 3.1.12 或更新版本。每个参数的作用见 PDF 转双语 EPUB。
需要什么,就传什么
| 如果你的 PDF 是… | 传入 | 原因 | 详见 |
|---|---|---|---|
| 任何想做成书的 PDF | --to-epub --use_context session |
生成带目录的可重排 EPUB;会话让每个短小的文本块都能看到它前面的内容 | PDF 转双语 EPUB |
| 扫描件(没有文字层) | --pdf-ocr |
不传它,没有文字的页面会被拒绝,所以你永远不用猜 | OCR 参数 |
| 中文、日文、韩文或其他非拉丁字母文字的扫描件 | --pdf-ocr --ocr-lang iso:ja(或 iso:ko,…) |
默认的 OCR 引擎只识别它自己的语言:在 Mac 上(ocrmac)是英语、西班牙语、法语和德语,其他系统上(rapidocr)是中文和英语 | OCR 语言 |
| 中文扫描件 | --pdf-ocr --ocr-lang iso:zh(繁体用 iso:zh-Hant) |
不是 ch_sim:rapidocr 会拒绝它 |
中文扫描件 |
| 简体中文扫描件 | --pdf-ocr --ocr-engine rapidocr --ocr-lang iso:zh |
测量中汉字错误率最低 | 选择 OCR 引擎 |
| 在 Mac 上,并且不想下载任何东西 | --pdf-ocr(auto 会选 ocrmac,pdf 扩展在 Mac 上会安装它;--ocr-engine ocrmac 可以直接指定它) |
Apple 的 Vision 框架,无需下载;在英文和繁体中文上与最好的引擎只差一点,简体中文扫描件上 rapidocr 更好 | 选择 OCR 引擎 |
| 已经带有 OCR 文字层的扫描件(Internet Archive、ABBYY) | 不用额外传什么 | 运行时会使用这层文字,并说明这一点 | 扫描书 |
| 文字层是乱码的扫描件 | --pdf-ocr --ocr-replace-layer --ocr-lang <lang> |
每一页都由 OCR 引擎重新读取 | 文字层有误的扫描件 |
| 论文,或任何带代码清单的文档 | --img-model gpt-6-luna |
视觉模型会纠正被当成标题的作者行和被读成脚注的代码清单,每页约 3,000 个提示 token | 纠正区域角色 |
| 想一次翻译一章的长书 | --pages 12-30 |
每个范围都有自己的书,互不覆盖 | PDF 参数 |
| 满是数学公式 | 不用额外传什么 | 行间公式默认保留为图片 | PDF 参数 |
| 人名或术语的译法必须固定 | --glossary terms.txt |
钉住的译法,只随出现它们的文本块一起发送 | 会话模式 |
| 在一台 GPU 不太正常的机器上 | --device cpu |
文字完全一样,只是更慢 | 按系统 |
按文档类型
=== "小说"
排印的小说标题少,也没有表格。不想要的书前内容,用 `--pages` 排除。
```bash
python make_book.py \
--book_name novel.pdf \
--to-epub \
--language zh-hans \
--use_context session \
--quiet
```
先检查 `source.md` 里的章节标题:小说的章节常常不带编号,这时它们的层级只能靠字号来判断。区域角色判定是在论文、网页和代码清单上测量的,没有在小说上测量过;使用 `--provider openai` 时它是开启的,用 `--img-model none` 可以省下这部分 token。=== "带表格和公式的教材"
不用 OCR 也能检测表格。行间公式会变成图片;公式周围的文字会翻译,公式本身不翻译。
```bash
python make_book.py \
--book_name textbook.pdf \
--to-epub \
--pages 12-30 \
--language zh-hans \
--use_context session \
--glossary terms.txt
```
用 `--pages` 一次翻译一章;每个范围都有自己的书。带代码清单的教材能从 `--img-model gpt-6-luna` 中受益:清单的各行会以代码的形式输出,而不是变成脚注。句子里的行内数学式不是公式区域,不在处理范围内:文字层或 OCR 把它读成什么样,它就是什么样。=== "论文"
论文会被提取成许多短小的文本块;会话让每个文本块都能看到它前面的内容。
```bash
python make_book.py \
--book_name paper.pdf \
--to-epub \
--language zh-hans \
--use_context session \
--img-model gpt-6-luna
```
在大多数论文上,标题层级都完全正确(20 篇 arXiv 论文中,195 个标题有 187 个正确)。`--img-model` 会把被当成标题的作者行或插图标注降级,每页约 3,000 个提示 token;不传它就不在这上面花钱。如果不想为参考文献付费,用 `--pages` 把它排除在外。=== "扫描书"
没有文字层的页面,在你传入 `--pdf-ocr` 之前都会被拒绝。
```bash
python make_book.py \
--book_name scan.pdf \
--to-epub \
--pdf-ocr \
--language zh-hans \
--use_context session
```
不写 `--ocr-lang` 时,OCR 引擎只识别它自己的默认语言:在 Mac 上(ocrmac)是英语、西班牙语、法语和德语,其他系统上(rapidocr)是中文和英语。其他语言的扫描件需要 `--ocr-lang`。
**如果扫描件已经带有 OCR 文字层**(Internet Archive 和 ABBYY FineReader 生成的文件常常如此),先不加 `--pdf-ocr` 试一次:运行会打印 `… selected pages carry only an invisible OCR text layer …` 并使用这层文字,它通常比本地重新做一遍 OCR 读得更好。如果这层文字其实是乱码,见下一个标签页。
`--img-model` 对扫描件作用不大:版面检测器已经切成碎片的页面,它拼不回来。=== "文字层有误的扫描件"
如果页面图像很清晰,`source.md` 读起来却不知所云,说明扫描件的文字层有误:识别成了别的语言,或者是劣质 OCR 留下的乱码。`--ocr-replace-layer` 会丢弃这层文字,改用 OCR 引擎读取每一页。
```bash
python make_book.py \
--book_name scan.pdf \
--to-epub \
--pdf-ocr \
--ocr-replace-layer \
--ocr-lang iso:zh \
--language en \
--use_context session
```
在 `--ocr-lang` 里写扫描件本身的语言(这里是 `iso:zh`,对应中文扫描件):语言不对,引擎就什么也读不出来。先试两页。引擎一无所获的页面会被点名并留空;如果一页都没读出来,运行会在任何翻译付费之前停止。文字层读起来没问题的扫描件不要用它:重新做的 OCR 不如一层好的文字层。=== "中文扫描件"
指明 OCR 引擎要识别的语言。`iso:` 标签无论运行哪个引擎都有效:简体用 `iso:zh`,繁体用 `iso:zh-Hant`。不是 `ch_sim`:rapidocr 会在读取任何页面之前拒绝它。
```bash
python make_book.py \
--book_name scan.pdf \
--to-epub \
--pdf-ocr \
--ocr-lang iso:zh \
--language en \
--use_context session
```
横排文字读得很好。**竖排文字**(从上到下、从右到左排印的繁体书)输出时列的顺序是错的。翻译竖排扫描件之前,先检查 `source.md`。按系统
这条路线不需要 GPU。设备只影响速度,从不影响文字。从仓库的克隆安装;安装 PDF 扩展逐行解释了每一条命令。
=== "macOS(Apple 芯片)"
```bash
pip install ".[pdf]"
```
直接在本机安装;`--device auto` 会找到 MPS,运行时打印 `PDF extraction device: mps.`。Mac 上的 Docker 用不到 MPS。
```bash
python make_book.py \
--book_name paper.pdf \
--to-epub \
--use_context session
```=== "带 NVIDIA 显卡的 Linux"
```bash
pip install ".[pdf]"
```
`--device auto` 会找到 CUDA,运行时打印 `PDF extraction device: cuda.`。想确保用上,就明确指定它;如果用不了,运行会拒绝并说明原因:
```bash
python make_book.py \
--book_name paper.pdf \
--to-epub \
--device cuda \
--use_context session
```=== "带 NVIDIA 显卡的 Windows"
```bat
pip install ".[pdf]" ^
--extra-index-url https://download.pytorch.org/whl/cu126
```
PyPI 上的 Windows wheel 只支持 CPU,所以要指定 PyTorch 的 CUDA 索引,并安装 NVIDIA 驱动。之后的运行方式与 Linux 相同。=== "只有 CPU"
在 Linux 上,加上 PyTorch 的 CPU 索引(约 380 MB,而不是约 3.2 GB 的 CUDA):
```bash
pip install ".[pdf]" \
--extra-index-url https://download.pytorch.org/whl/cpu
```
在 Windows 上,普通的 `pip install ".[pdf]"` 装的就是 CPU 版。文字与 GPU 上完全一样,只是提取更慢(一份两页的 OCR 扫描件:CPU 上 26.3 s,Apple 芯片的 GPU 上 10.6 s)。
```bash
python make_book.py \
--book_name paper.pdf \
--to-epub \
--device cpu \
--use_context session
```=== "Docker"
`pdf` 镜像标签带有 Pandoc 和 PDF 相关的包。把模型放在一个卷里,这样只需下载一次。
```bash
docker run --rm \
-v "$PWD":/book \
-v bbm-models:/root/.cache \
-e OPENAI_API_KEY \
ghcr.io/yihong0618/bilingual_book_maker:pdf \
--book_name /book/paper.pdf \
--to-epub \
--use_context session
```
在带 NVIDIA 显卡的 Linux 或 Windows(WSL2)上,改用 `pdf-cuda` 标签(只有 amd64)并加 `--gpus all`。见 [Docker 安装](../docker.md)。对任何 PDF 都该有的预期
- 插图保留为图片,其中的标注不翻译。
- 行间公式是从页面上裁下来的图片:在原来的位置可以看到,但不翻译,也搜索不到。行内数学式不在处理范围内。
- 含有
\s、[u](y)或<k>的句子可能在翻译之前被拒绝;在source.md里把它转义后重跑。 - 在这条路线上,EPUB 总是带有那一行 AI 翻译署名;这里不支持
--no_disclosure。 - PDF 是对页面的描述,而不是文档:标题差了一级、表格变成了正文,这是格式本身的局限,在
source.md里花一分钟就能改好。
Zh Features Session Mode (`docs/zh/features/session-mode.md`)
会话模式
它做什么
--use_context session 让整本书保持一段对话。每个请求都往对话里追加内容,也都携带整段对话,所以模型在翻译下一段之前,读过的是前面大约一章的内容。人名、语域和术语就是这样从一页到另一页保持一致的。在支持提示缓存的端点上,已经发送过的历史按缓存价计费,所以很长的历史实际花费远低于它的体量。另一种上下文模式是不带值的 --use_context(窗口模式),每个请求只重发最近几对原文与译文。
历史不能无限增长。当它达到 --context-compact-at(默认 8192 个估算 token,包括开头的摘要)时,模型会写一份简短的交接报告,约 300 个 token:到目前为止的人名、语域和术语决定。这份报告作为下一个窗口的开头。最新的一份保存在 _handoff.md 里,--resume 会把它读回来。这个默认值是为连贯性选的,而不是为价格;测量数据见会话压缩预算,其中也说明了更低的值其实更便宜。
准备
不需要安装任何东西。会话模式需要:
- 一本 EPUB、Markdown 或 PDF 书(PDF 两条路线都行:
--to-epub翻译的是 Markdown,纯文本路线也接受会话)。TXT 和 SRT 从不把上下文交给模型。 - 一条能保留历史的路线:OpenAI 系路线(OpenAI、网关、groq、xai、litellm、本地服务器)和 anthropic。Gemini 和 Qwen 自己维护历史,用的是不带值的
--use_context,会拒绝session。codex 路线不管你要不要,都是会话:它的线程就是历史。
参数:
| 参数 | 作用 |
|---|---|
--use_context session |
开启会话模式。 |
--context-compact-at N |
窗口预算,以估算 token 计,包括交接报告。默认 8192,最小 1500。如果你的模型输入上限更小,就设成那个上限。 |
--no-context-compact |
从不要求写交接报告。窗口仍在达到预算时滚动,但下一个窗口从空白开始。 |
--glossary FILE |
固定你能负责的译法(term -> translation 形式的行)。只有出现在某个请求里的术语才会随它发送。 |
--glossary-auto on |
同时保留交接报告里确立的译名。默认关闭。它需要一个会用名字而不是大段文字作答的模型。 |
会话模式与 --parallel-workers 同用会被拒绝(一份历史无法在多个 worker 之间共享),与 --model_list 同用也会被拒绝(每个模型有自己的缓存,而且一段对话会由几个模型轮流书写)。--context_paragraph_limit 属于窗口模式,在这里会被忽略。
推荐命令
翻译小说时,加上 --use_context session;如果某个反复出现的人名在窗口接缝之后译法漂移了,再加上 --glossary names.txt。如果模型在本地运行、上下文较小,就把 --context-compact-at 设为它的输入上限。如果端点没有提示缓存,就改用不带值的 --use_context。按书的类型、端点和系统列出的完整清单见 EPUB 推荐设置;PDF 见 PDF 推荐设置。
可能出现的问题
session: compacting at 8192 estimated tokens (the default; --context-compact-at overrides)。不是问题,运行只是告诉你它用的预算。- **进度条上的
cached=在十几个请求之后仍然是 0。**端点没有提示缓存,每个请求都在按全价为整段历史付费。按 Ctrl+C,改用不带值的--use_context重新运行。 --use_context session outside plan mode leaves grouping off, so every paragraph is its own request and each one re-reads the whole history.不在计划模式下的 EPUB(例如--plan-classify none),或者 codex 路线上的 SRT 书。调高--accumulated_num。Markdown 和 PDF 运行不会打印这一行;在那里,调大--batch_size可以减少请求数。Error: --use_context session is not implemented for the gemini format; it would be accepted and ignored.在 Gemini 和 Qwen 上使用不带值的--use_context。--use_context session is not supported for txt books; it will be ignored.TXT 和 SRT 不携带上下文。--parallel-workers is not supported with --use_context session: one history is the context, and a worker cannot share it.二选一。不带值的--use_context可以保留多个 worker。a compact budget of N is too small for a session; use at least 1500 estimated tokens.低于 1500 时,一个窗口的大部分都是开头的那份交接报告。如果你想要比这更少的上下文,就用不带值的--use_context。ℹ handoff report failed (…); starting the next context window without a summary,或… keeping the current context and retrying on the next paragraph。某次压缩没有产出可用的报告。翻译会继续。如果接缝之后的文字出现漂移,就用--glossary固定术语。--glossary-auto on learns renderings from the handoff report a session writes when it compacts, and this run keeps no session to compact.加上--use_context session,或者改用不需要会话的--glossary。- **小模型压缩之后文字出现漂移。**交接报告由模型来写,小模型可能写得不好。用
--glossary固定人名,或者传入--no-context-compact,接受每个接缝处从空白窗口开始。 --context-compact-at或--no-context-compact被打印为only applies to --use_context session; ignoring it.你给了压缩参数,却没有开启会话模式。
Zh Formats Epub (`docs/zh/formats/epub.md`)
EPUB
如果你想要一本 EPUB 的双语版,大多数人需要的就是这条命令:
bbook_maker --book_name my_book.epub --language zh-hans --use_context session --quiet它会在输入文件旁边生成 my_book_bilingual.epub。遇到教材、小模型或本地服务器时要改什么,见 EPUB 推荐设置。
EPUB 是本工具最熟悉的格式,所有功能都能用在它上面。
文件是怎么读取的
EPUB 是一个由 XHTML 页面组成的 zip 包。工具会读取书的 spine 里列出的每一页。
- **使用大模型路线时(默认):**书会经过计划模式。加载器找出每一个承载文字的块(段落、标题、列表项、表格单元格、引文、诗行、图题),按类型分组,再问模型哪几类值得翻译。答案保存在
_plan.json里,之后的运行会复用它。随后,相邻的块在 token 预算和单元数上限之内合成一次请求。 - **使用翻译服务路线且没有
--classify-model,或使用--plan-classify none时:**只翻译--translate-tags选中的标签,默认是。这时之外的诗歌或表格会保留源语言。 - 段落里的链接、强调和其他行内标记在翻译前被替换成编号标记,翻译后再放回原处,所以模型永远看不到原始 HTML。
--exclude-translate-tags里的内容(默认sup和code)从不发送。
你会得到什么
- 输入文件旁边的
<name>_bilingual.epub。每段译文紧跟在它的原文后面,使用相同的标签和 class。id 和内部链接都会保留。 - 书籍简介下方的一行署名,内容为“Translated by <model>, <year>.”;
--no_disclosure可以去掉它。 - 计划模式和会话模式的运行会在书里写入
bbm_translation_metadata.json:模型、日期,以及--glossary文件的校验和。 - 输入文件旁边的
_plan.json(计划),以及会话模式运行时的_handoff.md(最新的交接报告)。 - 按 Ctrl+C 或出错时:
<name>_bilingual_temp.epub和一个检查点。加上--resume重新运行即可。
推荐命令
bbook_maker \
--book_name my_book.epub \
--language zh-hans \
--use_context session \
--quiet遇到不寻常的书(教材、双语版、正文不在 `` 里的书),先预览计划。这一步不需要 key,也不翻译任何内容:
bbook_maker \
--book_name my_book.epub \
--plan-dry-run适用的参数
路线与运行
这些参数在所有格式上作用相同。
| 参数 | 作用 |
|---|---|
--book_name PATH |
要翻译的文件。扩展名决定格式。 |
-m, --model MODEL |
模型 id,按端点自己的写法。openai 格式下默认 gpt-6-luna。 |
--key KEY |
API key;用逗号分隔多个 key 可以轮换使用。未提供时依次读取 BBM_API_KEY 和该格式自己的变量。 |
--api_base URL |
端点地址。默认是该格式的官方地址。 |
--api_format FORMAT |
端点所用的 API,或一个翻译服务。省略时根据 --api_base 推断。 |
--provider NAME |
bbm_providers.json 里的一个具名端点。 |
--model_list IDS |
轮换使用的多个模型。与 --use_context session 同用会被拒绝。 |
--language LANGUAGE |
目标语言:标签、语言名,或 TAG:NAME。默认 zh-hans。 |
--source_lang LANGUAGE |
明确指定源语言。会写进每个大模型提示词;在 qwen 和 customapi 上作为请求字段发送。 |
--prompt VALUE_OR_FILE |
自定义提示词:user 模板(必须包含 {text})、system、style。 |
--temperature FLOAT |
采样温度,仅用于接受该参数的格式。 |
--no-thinking |
让模型回答前不要先推理。在 OpenAI 形态的路线上,请求字段通过协商确定;在 anthropic 上是 thinking: disabled;在 codex 上会被拒绝。 |
--extra_body JSON |
在 openai 和 anthropic 路线上附加的请求体字段。 |
--extra_headers JSON |
在 openai 和 anthropic 路线上附加的 HTTP 头。 |
--interval SECONDS |
请求之间的间隔。只有 gemini 格式使用它。 |
-p, --proxy URL |
本次运行使用的 HTTP 代理。 |
--test |
只翻译开头几段。 |
--test_num N |
配合 --test,翻译多少段(默认 10)。 |
--resume |
从检查点继续一次中断的运行。 |
--single_translate |
只写出译文,不带原文。 |
EPUB 参数
| 参数 | 作用 |
|---|---|
--translate-tags TAGS |
没有计划时要翻译的标签(默认 p)。计划模式下忽略。 |
--exclude-translate-tags TAGS |
其内容从不翻译的标签(默认 sup,code;传 "" 清空)。 |
--allow_navigable_strings |
连不在任何标签里的文字也一起翻译。计划模式下多余。 |
--only_filelist FILES |
只翻译这些内部文件(相对于 OPF 的名字,逗号分隔)。 |
--exclude_filelist FILES |
跳过这些内部文件。给了 --only_filelist 时忽略。 |
--plan-classify MODE |
计划由谁决定:auto(默认)、none、all、model、agent。见计划模式。 |
--classify-model MODEL |
用另一个模型分类(默认:提供方条目的 classify_model,其次是翻译用的模型)。在命令行上写明时,运行进入 model 模式,分类失败会中止运行;它也能让翻译服务路线用上计划。--plan-classify-model 是旧名字。在 --plan-classify all 或 agent 下忽略。 |
--classify-base-url URL |
该模型的服务地址,当它不是本次运行的端点时使用(仅限 OpenAI 兼容)。 |
--classify-key KEY |
--classify-base-url 使用的 key。见哪个 key 发往哪里。 |
--plan-dry-run |
打印计划并写出 _plan.json,不翻译。不需要 key。 |
--plan-min-coverage FRACTION |
计划覆盖的文字比例低于这个值时中止(默认 0.5)。 |
--poetry-group-size N |
已弃用;仍然可用,但会警告。请改用 --max-batch-units。 |
--accumulated_num N |
计划模式下是每个请求的 token 预算(不设时自动推算;1 关闭合并)。没有计划时,是每个请求累积的字符数。 |
--max-batch-units N |
计划模式:一个请求最多包含的单元数(默认 16;在不支持严格 schema 的端点上为 8)。 |
--block_size N |
没有计划时:把段落合并成用分隔符方式翻译的块。--accumulated_num 大于 1 时忽略。 |
--sentence_mode |
逐句翻译。不能与计划同用;--accumulated_num 大于 1 时忽略。 |
--use_context [window|session] |
带上前文:不带值或 window 会重发最近几对原文与译文;session 保持一份历史。见会话模式。 |
--context_paragraph_limit N |
窗口模式:重发多少对。 |
--context-compact-at N |
会话模式:历史达到这么多估算 token 时压缩(默认 8192,最小 1500)。 |
--no-context-compact |
会话模式:窗口滚动时不写交接报告。 |
--glossary FILE |
term -> translation 形式的固定译法,只随包含这些术语的请求发送。适用于 openai 系和 codex 路线。--terminology 是同一个参数。 |
--glossary-auto on|off |
保留会话的交接报告里确立的译名。除非你要求,否则关闭。 |
--parallel-workers N |
同时翻译几个章节(2 到 4 比较有用)。与会话模式同用或在 codex 上会被拒绝。 |
--translation_style CSS |
译文段落的 CSS。 |
--translation_color COLOR |
只设颜色;--translation_style 优先。 |
--no_disclosure |
去掉那一行 AI 翻译署名,同时不写元数据文件。 |
--translation-metadata |
在普通的标签模式运行中也写入 bbm_translation_metadata.json(计划模式和会话模式的运行本来就会写)。 |
--retranslate OUT FILE START END |
重新翻译一本已有双语 EPUB 中的一段范围。 |
--quiet |
不显示进度条,也不回显段落;报告和错误照常打印。 |
不适用于本格式
--batch_size:EPUB 加载器用--accumulated_num合并请求;运行会给出警告。--batch、--batch-use:在 EPUB 上会被拒绝,因为那里永远走不到 Batch API 的路径。--to-epub、--pdf-ocr、--ocr-replace-layer、--device、--ocr-lang、--pages、--no-formula-images、--pdf_layout、--img-model、--img-base-url、--img-key:仅限 PDF。在 EPUB 上用--to-epub会中止运行;其他参数会给出警告或不起作用(三个图像参数:运行会警告只有 PDF 路线才有图像步骤)。
Zh Formats Md (`docs/zh/formats/md.md`)
Markdown
Markdown 文件按正文块逐块翻译。代码、表格和 front matter 原样保留。
文件是怎样读取的
- YAML front matter、围栏代码块、表格以及不含正文的行都原样通过,不翻译。
- 标题和段落是翻译块。
- 这些块按
--batch_size(默认 10)分批发送,一批在大约 2000 个字符处也会截止。批次上方的标题路径会作为面包屑一起发送,让模型知道自己在哪一节。 --use_context在这里可用,窗口模式和会话模式都行。
这也是 PDF 转双语 EPUB 背后的加载器:PDF 先被转成 Markdown,再在这里翻译。
你会得到什么
- 输入文件旁边的
<name>_bilingual.md:每一块后面紧跟它的译文。使用--single_translate时只有译文。 - 会话模式的运行还会生成
<name>_handoff.md,里面是最新的交接报告。 - 按 Ctrl+C 或出错时:
<name>_bilingual_temp.txt和一个检查点。用--resume重跑。
推荐命令
bbook_maker \
--book_name my_doc.md \
--language zh-hans \
--prompt prompt_md.json \
--use_context session \
--batch_size 20仓库里的 prompt_md.json 是专为 Markdown 写的提示词:它要求模型保留标记。Markdown 没有计划模式,所以在会话模式下,每个请求都会重读历史。调大 --batch_size,就能用更少、更大的请求发送。
适用的参数
路线与运行
这些参数在所有格式上的作用都一样。
| 参数 | 作用 |
|---|---|
--book_name PATH |
要翻译的文件。扩展名决定格式。 |
-m, --model MODEL |
模型 ID,按端点自己的写法原样填写。openai 格式下默认 gpt-6-luna。 |
--key KEY |
API key;用逗号分隔的多个 key 会轮流使用。不写时先读 BBM_API_KEY,再读该格式自己的变量。 |
--api_base URL |
端点地址。默认是该格式的官方地址。 |
--api_format FORMAT |
端点使用的 API,或者一个翻译服务。不写时根据 --api_base 推断。 |
--provider NAME |
bbm_providers.json 中的一个具名端点。 |
--model_list IDS |
轮流使用的多个模型。与 --use_context session 同用时会被拒绝。 |
--language LANGUAGE |
目标语言:一个标签、一个名称,或 TAG:NAME。默认 zh-hans。 |
--source_lang LANGUAGE |
明确给出源语言。会写进每一个大模型提示词;在 qwen 和 customapi 上作为一个字段发送。 |
--prompt VALUE_OR_FILE |
自定义提示词:user 模板(必须包含 {text})、system、style。 |
--temperature FLOAT |
采样温度,用于接受这一参数的格式。 |
--no-thinking |
要求模型回答之前不做推理。在 OpenAI 形式的路线上,这个字段会自动协商;anthropic 上是 thinking: disabled;codex 上会被拒绝。 |
--extra_body JSON |
openai 和 anthropic 路线上额外的请求体字段。 |
--extra_headers JSON |
openai 和 anthropic 路线上额外的 HTTP 头。 |
--interval SECONDS |
请求之间的间隔。只有 gemini 格式使用它。 |
-p, --proxy URL |
本次运行使用的 HTTP 代理。 |
--test |
只翻译开头的若干段。 |
--test_num N |
配合 --test:翻译多少段(默认 10)。 |
--resume |
从检查点继续一次中断的运行。 |
--single_translate |
只写出译文,不带原文。 |
Markdown 参数
| 参数 | 作用 |
|---|---|
--batch_size N |
一个请求发送的正文块数(默认 10;一批在大约 2000 个字符处也会截止)。 |
--use_context [window|session] |
携带前面的段落:不带值或 window 会重发几对原文与译文;session 保持一份历史。 |
--context_paragraph_limit N |
窗口模式:重发多少对原文与译文。 |
--context-compact-at N |
会话模式:估算 token 数达到这个值时压缩(默认 8192,最小 1500)。 |
--no-context-compact |
会话模式:滚动到下一个窗口时不写交接报告。 |
--glossary FILE |
term -> translation 形式钉住的译法(openai 形式的路线和 codex 路线)。--terminology 是同一个参数。 |
--glossary-auto on|off |
保留会话交接报告中确立的译名。除非明确要求,否则关闭。 |
--parallel-workers N |
同时翻译多个批次或分段。与会话同用、以及在 codex 上都会被拒绝。 |
不适用于这种格式的参数
--accumulated_num:Markdown 加载器不读它;运行时会给出警告,并提示改用--batch_size。在计划模式之外,会话每个请求都会重读历史,所以更大的--batch_size能让请求数量少一些。--max-batch-units、--plan-classify、--plan-dry-run、--plan-min-coverage、--poetry-group-size:计划模式只用于 EPUB。--classify-model、--classify-base-url、--classify-key(以及旧名称--plan-classify-model):这种格式上暂时没有任何东西做分类。运行时会警告该参数被忽略。--exclude-translate-tags:接受且不给警告,但 Markdown 加载器不读它。--translate-tags、--allow_navigable_strings:EPUB 标记选择器;运行时会给出警告。--only_filelist、--exclude_filelist:EPUB 内部文件;忽略。--block_size、--sentence_mode:仅用于 EPUB;忽略。--translation_style、--translation_color:Markdown 没有样式;运行时会给出警告。--no_disclosure、--translation-metadata:输出不带署名行,也不带元数据文件;运行时会给出警告。--retranslate:仅用于 EPUB;会被拒绝。--quiet:仅用于 EPUB;运行时会给出警告。--batch、--batch-use:Markdown 加载器没有实现 Batch API。--to-epub、--pdf-ocr、--ocr-replace-layer、--device、--ocr-lang、--pages、--no-formula-images、--pdf_layout、--img-model、--img-base-url、--img-key:仅用于 PDF。在这种格式上,--to-epub会让运行停止;其余的会给出警告或不起作用(三个图像参数:运行时会警告只有 PDF 路线有图像这一步)。
Zh Formats Pdf (`docs/zh/formats/pdf.md`)
PDF 可以走两条路线。除非你只想要纯文本,否则选第一条。
| 路线 | 参数 | 输出 | 需要 |
|---|---|---|---|
| PDF 转双语 EPUB | --to-epub |
带目录的可重排 EPUB,插图和行间公式保留为图片 | PDF 扩展和 Pandoc 3.1.12+ |
| 纯文本路线(旧) | 无 | 一个双语 .txt,另外尝试生成一个 EPUB |
基础安装 |
`--to-epub` 路线
文件是怎样读取的
- docling 用它的版面模型和表格模型读取 PDF 的文字层,写出 Markdown:
<name>_book/source.md和图片。 - 没有文字层的页面(扫描件)会被拒绝,除非你传入
--pdf-ocr,这时由 OCR 模型读取它们。有文字层的页面保留自己的文字层,除非你同时传入--ocr-replace-layer:这时每一页都做 OCR,PDF 自带的文字层不再使用。 - 使用
--img-model时,视觉模型会根据页面图像,纠正 docling 给页面各区域定的角色(标题、图题、脚注、代码)。 - 标题层级从页面上读出(先看编号,再看字号和字重)。行间公式从页面上裁成图片。
- Markdown 加载器把
source.md翻译成book_bilingual.md。 - Pandoc 生成 EPUB。它的导航按标题生成。
重跑同一条命令会复用提取结果和已完成的翻译。如果有内容出错,在翻译开始之前编辑 source.md。删除 book_bilingual.md 即可重新翻译。推荐的命令和要留意的终端信息见 PDF 转双语 EPUB。
你会得到什么
- PDF 旁边的
<name>_book/:source.md、images/、book_bilingual.md、一份清单,以及.work/。 - PDF 旁边的
<name>_bilingual.epub,是成书的一份副本。 source.md中,每一页开头都有一条注释,编号与 PDF 文件中的一致(从 1 开始,与 `--pages` 的计数相同),空白页也包括在内。docling 没有给出页码的条目(很少见)放在最后,位于下面;运行时会说明有多少条。- 使用
--pages 12-30时:<name>_pages-12-30_book/和<name>_pages-12-30_bilingual.epub,所以某一章永远不会覆盖整本书。
适用的参数
这条路线自己的参数:
| 参数 | 作用 |
|---|---|
--to-epub |
走这条路线。 |
--pdf-ocr |
用 OCR 模型读取没有文字层的页面。默认关闭;不开的话,这些页面会被拒绝。 |
--ocr-replace-layer |
配合 --pdf-ocr:对每一页做 OCR,用识别结果代替 PDF 的文字层。默认关闭:保留已有的文字层,只对没有文字层的页面做 OCR。适用于有误的文字层(别的语言、乱码);在干净的扫描件上,文字层读得更好。切换它会重新读取 PDF。 |
--ocr-lang LANGS |
配合 --pdf-ocr:OCR 引擎要识别的语言,用逗号分隔,写引擎自己的代码(rapidocr:ch、en、latin;easyocr:ch_sim、ja、ko;ocrmac:zh-Hans、ja-JP),或在 BCP-47 标签前加 iso:(iso:zh、iso:ja),所有引擎都接受。rapidocr 只用第一种语言,而且拒绝 ch_sim:中文请写 iso:zh。 |
--device auto|cpu|cuda|mps|xpu |
提取模型在哪里运行。auto(默认)会检测加速器,找不到就退回 CPU。CPU 的输出完全一样,只是更慢。 |
--pages PAGES |
只处理这些页,从 1 开始编号(12-30、1,3,5-7)。 |
--no-formula-images |
把行间公式留作 `` 占位符,而不是图片。 |
--img-model MODEL |
根据页面图像纠正区域角色的视觉模型。只有在这里指定,或在提供方条目的 img_model 中指定时才开启(自带的 openai 条目指定了一个);none 可以关掉它。绝不会退而使用翻译模型。每页约 3,000 个提示 token。 |
--img-base-url URL |
这个模型所在的地址,当它不在本次运行的端点上时使用(仅限 OpenAI 兼容接口)。 |
--img-key KEY |
--img-base-url 所用的 key;默认在本次运行自己的端点上使用本次运行的 key。 |
--quiet |
提取期间不显示实时进度行。 |
其他所有参数都原样传给 Markdown 翻译。下面这些在那里会起作用:
| 参数 | 作用 |
|---|---|
--book_name PATH |
要翻译的文件。扩展名决定格式。 |
-m, --model MODEL |
模型 ID,按端点自己的写法原样填写。openai 格式下默认 gpt-6-luna。 |
--key KEY |
API key;用逗号分隔的多个 key 会轮流使用。不写时先读 BBM_API_KEY,再读该格式自己的变量。 |
--api_base URL |
端点地址。默认是该格式的官方地址。 |
--api_format FORMAT |
端点使用的 API,或者一个翻译服务。不写时根据 --api_base 推断。 |
--provider NAME |
bbm_providers.json 中的一个具名端点。 |
--model_list IDS |
轮流使用的多个模型。与 --use_context session 同用时会被拒绝。 |
--language LANGUAGE |
目标语言:一个标签、一个名称,或 TAG:NAME。默认 zh-hans。 |
--source_lang LANGUAGE |
明确给出源语言。会写进每一个大模型提示词;在 qwen 和 customapi 上作为一个字段发送。 |
--prompt VALUE_OR_FILE |
自定义提示词:user 模板(必须包含 {text})、system、style。 |
--temperature FLOAT |
采样温度,用于接受这一参数的格式。 |
--no-thinking |
要求模型回答之前不做推理。在 OpenAI 形式的路线上,这个字段会自动协商;anthropic 上是 thinking: disabled;codex 上会被拒绝。 |
--extra_body JSON |
openai 和 anthropic 路线上额外的请求体字段。 |
--extra_headers JSON |
openai 和 anthropic 路线上额外的 HTTP 头。 |
--interval SECONDS |
请求之间的间隔。只有 gemini 格式使用它。 |
-p, --proxy URL |
本次运行使用的 HTTP 代理。 |
--test |
只翻译开头的若干段。 |
--test_num N |
配合 --test:翻译多少段(默认 10)。 |
--resume |
从检查点继续一次中断的运行。 |
--single_translate |
只写出译文,不带原文。 |
--batch_size N |
每个请求包含的正文块数(默认 10)。 |
--use_context [window|session] |
携带前文。推荐 session:PDF 会被提取成许多短小的文本块。 |
--context_paragraph_limit N |
窗口模式:重发多少对原文与译文。 |
--context-compact-at N |
会话模式:估算 token 数达到这个值时压缩(默认 8192,最小 1500)。 |
--no-context-compact |
会话模式:滚动到下一个窗口时不写交接报告。 |
--glossary FILE |
term -> translation 形式钉住的译法(openai 形式的路线和 codex 路线)。 |
--glossary-auto on|off |
保留交接报告中确立的译名。只在发生压缩时学习,所以一篇短论文在默认预算下什么也学不到。 |
--parallel-workers N |
同时处理多个批次。与会话同用时会被拒绝。 |
这条路线的旧写法 --with-ocr 和 --no-gpu 在一个发布周期内仍然有效,分别等同于 --pdf-ocr 和 --device cpu,并会打印一条提示。它们不出现在 --help 里。
不适用于 `--to-epub` 路线的参数
--no_disclosure:暂不支持;署名行总会加上。--translation-metadata:这个 EPUB 由 Pandoc 生成,不带元数据文件,也不嵌入术语表。--pdf_layout:属于纯文本路线。--accumulated_num、--max-batch-units以及计划模式参数(--plan-classify、--plan-dry-run、--plan-min-coverage):计划模式只用于 EPUB;Markdown 加载器用--batch_size分组。--classify-model、--classify-base-url、--classify-key:这条路线上暂时没有任何东西做分类。运行时会警告该参数被忽略。--translate-tags、--exclude-translate-tags、--allow_navigable_strings、--only_filelist、--exclude_filelist、--block_size、--sentence_mode、--translation_style、--translation_color、--retranslate:仅用于 EPUB 输入。--batch、--batch-use:这条路线没有实现。
纯文本路线(不加 `--to-epub`)
文件是怎样读取的
每一页的文字由 PyMuPDF 取出,再切分成行。这些行按 --batch_size(默认 10)分组发送;开启 --use_context(window 或 session)时带上前文,使用 --glossary 时带上钉住的译法。没有任何结构:没有标题,没有插图,没有表格。
你会得到什么
- PDF 旁边的
<name>_bilingual.txt。 - 用同样的文字尝试生成
<name>_bilingual.epub。如果失败,TXT 仍然保留,所以不会重复翻译。 - 使用
--pdf_layout时:<name>_bilingual_<layout>.pdf,对应top-bottom、side-by-side,或用all两者都生成。这需要reportlab包,它不在依赖列表里;没有它时,运行会打印pdf creation skipped: install reportlab first并继续。 - 按 Ctrl+C 或出错时:
<name>_bilingual_temp.txt。用--resume重跑。
bbook_maker \
--book_name paper.pdf \
--language zh-hans \
--batch_size 20适用的参数
上表中的路线参数和运行参数,另外还有:
| 参数 | 作用 |
|---|---|
--batch_size N |
每个请求包含的行数(默认 10)。 |
--pdf_layout none|top-bottom|side-by-side|all |
另外按这种版式写出一个双语 PDF(需要 reportlab)。 |
不适用于纯文本路线的参数
--parallel-workers:运行保持串行。运行时会给出警告。--pdf-ocr、--ocr-replace-layer、--device、--ocr-lang、--pages、--no-formula-images:仅用于--to-epub。运行时会给出警告,并读取整个文件。--img-model、--img-base-url、--img-key:图像这一步属于--to-epub。运行时会给出警告。--classify-model及其两个配套参数:这里没有任何东西做分类。运行时会给出警告。- 上面为
--to-epub路线列出的所有仅限 EPUB 的参数,以及--accumulated_num、--quiet、--no_disclosure、--translation-metadata。
Zh Formats Srt (`docs/zh/formats/srt.md`)
SRT
字幕逐块翻译。每一块都保留它的序号和时间轴。
文件是怎样读取的
- 文件在空行处切分成块。一块由一个序号、一条时间轴和一行或多行文字组成。
- 默认每一块单独发一个请求。
--accumulated_num N把相邻的块放进同一个请求,最多N个字符(上限 512)。 - 加载器自带的提示词会告诉模型不要动每一块的序号和时间轴。如果你用
--prompt替换了user模板,也要在你的模板里写明这一点,否则回答无法按 SRT 解析。运行时会就此给出警告。
你会得到什么
- 输入文件旁边的
<name>_bilingual.srt。每一块都保留序号和时间轴,原文后面紧跟译文。使用--single_translate时只有译文。 - 按 Ctrl+C 或出错时:
<name>_bilingual_temp.srt和一个检查点。用--resume重跑。
推荐命令
bbook_maker \
--book_name test_books/Lex_Fridman_episode_322.srt \
--language zh-hans \
--accumulated_num 400适用的参数
路线与运行
这些参数在所有格式上的作用都一样。
| 参数 | 作用 |
|---|---|
--book_name PATH |
要翻译的文件。扩展名决定格式。 |
-m, --model MODEL |
模型 ID,按端点自己的写法原样填写。openai 格式下默认 gpt-6-luna。 |
--key KEY |
API key;用逗号分隔的多个 key 会轮流使用。不写时先读 BBM_API_KEY,再读该格式自己的变量。 |
--api_base URL |
端点地址。默认是该格式的官方地址。 |
--api_format FORMAT |
端点使用的 API,或者一个翻译服务。不写时根据 --api_base 推断。 |
--provider NAME |
bbm_providers.json 中的一个具名端点。 |
--model_list IDS |
轮流使用的多个模型。与 --use_context session 同用时会被拒绝。 |
--language LANGUAGE |
目标语言:一个标签、一个名称,或 TAG:NAME。默认 zh-hans。 |
--source_lang LANGUAGE |
明确给出源语言。会写进每一个大模型提示词;在 qwen 和 customapi 上作为一个字段发送。 |
--prompt VALUE_OR_FILE |
自定义提示词:user 模板(必须包含 {text})、system、style。 |
--temperature FLOAT |
采样温度,用于接受这一参数的格式。 |
--no-thinking |
要求模型回答之前不做推理。在 OpenAI 形式的路线上,这个字段会自动协商;anthropic 上是 thinking: disabled;codex 上会被拒绝。 |
--extra_body JSON |
openai 和 anthropic 路线上额外的请求体字段。 |
--extra_headers JSON |
openai 和 anthropic 路线上额外的 HTTP 头。 |
--interval SECONDS |
请求之间的间隔。只有 gemini 格式使用它。 |
-p, --proxy URL |
本次运行使用的 HTTP 代理。 |
--test |
只翻译开头的若干段。 |
--test_num N |
配合 --test:翻译多少段(默认 10)。 |
--resume |
从检查点继续一次中断的运行。 |
--single_translate |
只写出译文,不带原文。 |
SRT 参数
| 参数 | 作用 |
|---|---|
--accumulated_num N |
一个请求发送的字幕块字符数(SRT 上限为 512)。 |
不适用于这种格式的参数
--batch_size:SRT 加载器不读它;用--accumulated_num分组。--use_context:SRT 加载器在两种模式下都不会把上下文交给模型;运行时会给出警告。--context_paragraph_limit、--context-compact-at、--no-context-compact:上下文参数;这里没有东西读取它们。--glossary、--glossary-auto:只有 EPUB、Markdown 和 PDF 加载器会转发它们;运行时会给出警告。--max-batch-units、--plan-classify、--plan-dry-run、--plan-min-coverage、--poetry-group-size:计划模式只用于 EPUB。--classify-model、--classify-base-url、--classify-key(以及旧名称--plan-classify-model):这种格式上暂时没有任何东西做分类。运行时会警告该参数被忽略。--translate-tags、--exclude-translate-tags、--allow_navigable_strings:EPUB 标记选择器;运行时会给出警告。--only_filelist、--exclude_filelist:EPUB 内部文件;忽略。--block_size、--sentence_mode:仅用于 EPUB;忽略。--parallel-workers:SRT 的运行保持串行;运行时会给出警告。--translation_style、--translation_color:字幕没有样式;运行时会给出警告。--no_disclosure、--translation-metadata:输出不带署名行,也不带元数据文件;运行时会给出警告。--retranslate:仅用于 EPUB;会被拒绝。--quiet:仅用于 EPUB;运行时会给出警告。--batch、--batch-use:SRT 加载器没有实现 Batch API。--to-epub、--pdf-ocr、--ocr-replace-layer、--device、--ocr-lang、--pages、--no-formula-images、--pdf_layout、--img-model、--img-base-url、--img-key:仅用于 PDF。在这种格式上,--to-epub会让运行停止;其余的会给出警告或不起作用(三个图像参数:运行时会警告只有 PDF 路线有图像这一步)。
Zh Formats Txt (`docs/zh/formats/txt.md`)
TXT
bbook_maker --book_name my_book.txt --language zh-hans --batch_size 20这会在输入文件旁边写出 my_book_bilingual.txt。SRT 字幕和 Markdown 在下面各有自己的页面;PDF 有单独的一节。
纯文本文件按行分组逐组翻译。没有要保留的结构,所以可选项也不多。
文件是怎样读取的
- 文件按 UTF-8 读取,并切分成行。
- 这些行按
--batch_size(默认 10)分组发送,组内用换行符连接。 - 空的、只有空白或只有一个数字的组会原样复制,不发请求。
- 请求不携带任何前文:TXT 加载器没有上下文模式。
你会得到什么
- 输入文件旁边的
<name>_bilingual.txt:每组原文行后面紧跟它的译文。使用--single_translate时只有译文。 - 按 Ctrl+C 或出错时:
<name>_bilingual_temp.txt和一个检查点。用--resume重跑。
推荐命令
bbook_maker \
--book_name test_books/the_little_prince.txt \
--language zh-hans \
--batch_size 20--batch_size 越大,请求越少,每个请求的上下文越多。如果模型开始把行合并或漏掉,就把它调小。
适用的参数
路线与运行
这些参数在所有格式上的作用都一样。
| 参数 | 作用 |
|---|---|
--book_name PATH |
要翻译的文件。扩展名决定格式。 |
-m, --model MODEL |
模型 ID,按端点自己的写法原样填写。openai 格式下默认 gpt-6-luna。 |
--key KEY |
API key;用逗号分隔的多个 key 会轮流使用。不写时先读 BBM_API_KEY,再读该格式自己的变量。 |
--api_base URL |
端点地址。默认是该格式的官方地址。 |
--api_format FORMAT |
端点使用的 API,或者一个翻译服务。不写时根据 --api_base 推断。 |
--provider NAME |
bbm_providers.json 中的一个具名端点。 |
--model_list IDS |
轮流使用的多个模型。与 --use_context session 同用时会被拒绝。 |
--language LANGUAGE |
目标语言:一个标签、一个名称,或 TAG:NAME。默认 zh-hans。 |
--source_lang LANGUAGE |
明确给出源语言。会写进每一个大模型提示词;在 qwen 和 customapi 上作为一个字段发送。 |
--prompt VALUE_OR_FILE |
自定义提示词:user 模板(必须包含 {text})、system、style。 |
--temperature FLOAT |
采样温度,用于接受这一参数的格式。 |
--no-thinking |
要求模型回答之前不做推理。在 OpenAI 形式的路线上,这个字段会自动协商;anthropic 上是 thinking: disabled;codex 上会被拒绝。 |
--extra_body JSON |
openai 和 anthropic 路线上额外的请求体字段。 |
--extra_headers JSON |
openai 和 anthropic 路线上额外的 HTTP 头。 |
--interval SECONDS |
请求之间的间隔。只有 gemini 格式使用它。 |
-p, --proxy URL |
本次运行使用的 HTTP 代理。 |
--test |
只翻译开头的若干段。 |
--test_num N |
配合 --test:翻译多少段(默认 10)。 |
--resume |
从检查点继续一次中断的运行。 |
--single_translate |
只写出译文,不带原文。 |
TXT 参数
| 参数 | 作用 |
|---|---|
--batch_size N |
一个请求发送的行数(默认 10)。 |
不适用于这种格式的参数
--use_context:TXT 加载器在两种模式下都不会把上下文交给模型。运行时会给出警告;对会话模式,它打印--use_context session is not supported for txt books; it will be ignored.--context_paragraph_limit、--context-compact-at、--no-context-compact:上下文参数;这里没有东西读取它们。--glossary、--glossary-auto:只有 EPUB、Markdown 和 PDF 加载器会转发它们;运行时会给出警告。--accumulated_num:TXT 加载器不读它;运行时会给出警告,并提示改用--batch_size。--max-batch-units、--plan-classify、--plan-dry-run、--plan-min-coverage、--poetry-group-size:计划模式只用于 EPUB。--classify-model、--classify-base-url、--classify-key(以及旧名称--plan-classify-model):这种格式上暂时没有任何东西做分类。运行时会警告该参数被忽略。--translate-tags、--exclude-translate-tags、--allow_navigable_strings:EPUB 标记选择器;运行时会给出警告。--only_filelist、--exclude_filelist:EPUB 内部文件;忽略。--block_size、--sentence_mode:仅用于 EPUB;忽略。--parallel-workers:TXT 的运行保持串行;运行时会给出警告。--translation_style、--translation_color:纯文本没有样式;运行时会给出警告。--no_disclosure、--translation-metadata:输出不带署名行,也不带元数据文件;运行时会给出警告。--retranslate:仅用于 EPUB;会被拒绝。--quiet:仅用于 EPUB;运行时会给出警告。--batch、--batch-use:TXT 加载器没有实现 Batch API。--to-epub、--pdf-ocr、--ocr-replace-layer、--device、--ocr-lang、--pages、--no-formula-images、--pdf_layout、--img-model、--img-base-url、--img-key:仅用于 PDF。在这种格式上,--to-epub会让运行停止;其余的会给出警告或不起作用(三个图像参数:运行时会警告只有 PDF 路线有图像这一步)。
--- METRICS ---
- Files Extracted: 71
- Estimated Token Budget: ~118613 tokens
- Canonical Reference: https://codewiki.google/github.com/yihong0618/bilingual_book_maker