output-and-translation
GitHub提供电子书输出格式扩展与翻译服务集成的架构说明及操作指南,涵盖Binder构建器契约、Calibre集成及Translation服务配置。
Trigger Scenarios
Install
npx skills add lncrawl/lightnovel-crawler --skill output-and-translation -g -y
SKILL.md
Frontmatter
{
"name": "output-and-translation",
"description": "Binder (e-book output formats) and the Translation service — architecture and recipes for adding an output format or changing how novels are translated. Use when working in services\/binder\/ or services\/translators.py."
}
Binder (lncrawl/services/binder/)
A dispatch table archive_maker: Dict[OutputFormat, Callable] in service.py maps each
format to a builder. EPUB, JSON, and text have native builders; every other format is
convert_epub — Calibre converts from a previously built EPUB artifact (it raises
ServerErrors.no_epub_file without one). available_formats derives from Calibre
availability: without Calibre only the native formats exist. Zip-emitting builders are listed
in requires_zip.
Builder contract (all builders): def make_x(working_dir: Path, artifact: Artifact, signal=Event(), **kwargs) -> None — write to a temp file inside working_dir, then
atomically replace ctx.files.resolve(artifact.output_file). Poll signal.is_set() and raise
AbortedException for cancellation. make_artifact owns the Artifact row, temp-dir lifecycle,
and cleanup.
Calibre (calibre.py) has two paths: a remote convert API (posted through
ctx.http.session(signal)) and the local ebook-convert executable, with configurable
fallback. Options are built backend-agnostic as (flag, value) tuples and rendered per path.
Recipe — new output format:
- Add the value to
OutputFormatinlncrawl/enums.py(string enum; note name ≠ value is allowed, e.g.text→"txt"). Enum changes need a Postgres enum-sync migration — see thedb-migrationskill. - Register it in
archive_maker. If Calibre supports the extension, point it atconvert_epub(one line); otherwise write a builder following the contract (add torequires_zipif it emits a zip). - Nothing else:
available_formatsand the API derive from the enum + table. Per-tier format gating is separate (ctx.tier.enabled_formats).
Files: ctx.files (services/file.py) resolves artifact-relative paths under the app
data dir, writes atomically, and transparently gzip-compresses text — chapter content on disk
is compressed; always read it via ctx.files.load_text(...).
Translation (lncrawl/services/translators.py)
Translation runs in-process via the external lncrawl-translator package (imported as
translator; sibling repo). ctx.translator (TranslationService) owns the glossary loop
and persistence; the package owns engines, routing lanes, rate limits, retries, and failover.
ctx.translator.engine— a lazily constructedtranslator.TranslatorService: a sync facade running the async engine router on its own event-loop thread. Its YAML config path comes fromctx.config.translator.config_file(app data dir); engines/keys/routing are edited through the mounted dashboard, not lncrawl settings.close()is guarded inctx.destroy().- Dashboard — the package's web UI is mounted at
/api/translatorby an admin-gated ASGI wrapper (server/api/translator.py,TranslatorDashboard): Bearer/?token=/cookie auth, token→cookie redirect dance, trailing-slash enforcement. It is a raw ASGI mount — app-level exception handlers and router security do NOT apply inside it. - Calls —
_translate_texts/_translate_htmlbuild request dicts and go through_invoke, which maps package errors toServerErrors:ApiError503 →translation_quota_exhausted, otherApiError/ValidationError→translation_failure, facadeAbortedError→AbortedException. The jobsignalandctx.config.translator.request_timeoutare passed to every engine call. - Glossary loop —
translate_novel/volume/chapterload the storedNovelGlossary, send it with each request, and merge returnednew_termsback. Chapter translation dedupes via a content hash and stores results throughctx.files.save_text. - Detection —
ctx.translator.detect_language(text)(local, no quota, no event loop) returns an ISO 639-1 code or None.fetch_novel/fetch_chapteruse it to fillNovel.languagewhen the source doesn't provide one; values are normalized via_normalize_languageinservices/crawler.py(dropsmulti/unknown, mapszh-cn→zh).
Changing engines/prompts/chunking happens in the lncrawl-translator package (sibling
repo), not here — bump the dependency version in pyproject.toml to pick up a release.
Version History
- b76d44a Current 2026-07-25 09:00


