Skip to content

Repository files navigation

perseus-cli

A headless CLI that translates an English Wikipedia article into a target Wikipedia language (Persian by default) using Perseus Desktop's existing Core pipeline, translating via OpenRouter only.

What this is (and isn't)

This is not a rewrite of Perseus. It reuses Core's real Parser, Chunker, Translator, Merger, Wikitext Generator, and Translation Session logic verbatim. The only things added are a thin CLI layer and a Node DOM polyfill; the only things removed are Desktop-only code (Tauri file/clipboard APIs) and every LLM provider except OpenRouter.

Wikipedia tables are still not translated — that limitation lives in Core's Extractor/ParsoidParser and was left untouched, exactly as specified.

Install & build

pnpm install
pnpm run build      # -> dist/cli.js

Run

export OPENROUTER_API_KEY=sk-...

# Fresh translation
node dist/cli.js translate "https://en.wikipedia.org/wiki/Special_relativity" \
  --model google/gemini-2.5-flash \
  --output ./output \
  --target-wiki fa

# Resume an interrupted or partially-failed run
node dist/cli.js resume ./output/Special_relativity.session.json \
  --output ./output

(Or pnpm link / install as a package, then perseus-cli translate "<url>".)

Environment variables

Variable Required Purpose
OPENROUTER_API_KEY Yes OpenRouter API key. Never hard-coded, never written into a session file.

Output

Each run writes two files into --output (default ./output), named after the article's slugified title:

  • <Title>.txt — the final translated Wikitext.
  • <Title>.session.json — a Translation Session file, byte-for-byte the same format Perseus Desktop reads/writes (format: "perseus-package", formatVersion: 1). You can open this directly in Perseus Desktop to keep editing, or hand it back to perseus-cli resume later.

The session is saved incrementally after every chunk (translated or skipped), not just at the end — killing the process mid-run still leaves a resumable session file on disk.

stdout carries exactly one JSON object per invocation; all logging goes to stderr. Example success output:

{
  "sourceTitle": "Special relativity",
  "targetWiki": "fa",
  "totalChunks": 12,
  "translatedChunks": 12,
  "skippedAlreadyTranslatedChunks": 0,
  "failedChunks": [],
  "wikitextPath": "output/Special_relativity.txt",
  "sessionPath": "output/Special_relativity.session.json"
}

If some chunks fail (e.g. persistent OpenRouter errors after retries), they are listed in failedChunks ({chunkId, message}) — Wikitext/session are still written with whatever did translate, and the session remains resumable for just the failed chunk(s).

Exit codes

Code Meaning
0 Full success — every chunk translated.
1 Fatal error — nothing usable was produced (bad URL, missing API key, Parsoid/Wikidata unreachable, invalid session file, etc.). Error JSON printed to stdout with category/message.
2 Partial success — Wikitext/session were written, but one or more chunks are in failedChunks.

Resumability

Resuming re-fetches the article by its saved revision ID (never the live/latest revision — see Core's Pipeline.reconstructFromRevision), then re-derives the same chunk grouping (deterministic given the same HTML and chunk-size budget) and skips any chunk whose session entry is already fully translated. Only the chunks that are missing or partially translated get sent to OpenRouter again.

Translation Session compatibility with Perseus Desktop

Nothing about the session format was changed. translation-sessions/types.ts, export.ts, import.ts, and validate.ts are Core's unmodified files. A session this CLI writes can be opened in Perseus Desktop, and a session saved by Perseus Desktop can be resumed by this CLI.

The one real technical wrinkle: DOM globals in Node

Core keeps the parsed Parsoid HTML alive as the IR's structural backbone and therefore calls DOMParser, Element, Document directly — APIs that only exist in a browser/webview, not plain Node. src/cli/dom-polyfill.ts installs linkedom (a fast, spec-compliant DOM implementation) onto globalThis before any Core module is imported. This is the only accommodation Core needed to run headlessly — no parsing/merge/generation logic was changed.

Retry & validation additions (CLI-side, not in Core)

Per the requirements, two small decorators wrap the OpenRouter provider outside Core:

  • src/cli/retryingProvider.ts — retries a transient ProviderError up to 3 times with linear backoff; does not retry ConfigurationError (e.g. a missing API key can't be fixed by retrying).
  • src/cli/sanitizingProvider.ts — strips a wrapping Markdown code fence if the model wraps its whole answer in one.

Everything else — missing/duplicate segment handling, per-unit fallback retry, placeholder-token corruption tolerance, and the hard "unknown-node-reference" merge check — is Core's existing, unmodified validation (see stages/06-translation/Translator.ts, stages/02-parsing/placeholders.ts, stages/07-merge/Merger.ts).

Invoking from Hermes

perseus-cli translate "<url>" --output /path/to/drop

Then read stdout as JSON, check the exit code, and hand wikitextPath/sessionPath off to Telegram. perseus-cli never sends anything to Telegram or Wikipedia itself — output is files only, exactly as specified.

Known limitations / things worth knowing

  • Wikipedia tables are never translated (an intentional, pre-existing Core limitation — not something this CLI added or could easily lift without touching Extractor/ParsoidParser, which was explicitly out of scope).
  • Chunk grouping (SizeBoundedChunker, 2500-char budget) is fixed, not CLI-configurable, to guarantee a resumed session's chunks line up with the original run's.
  • Resuming still requires network access throughout (revision fetch, link resolution, final Wikitext generation) — there is no offline restore path, matching Core's own documented design.
  • A chunk is treated as "already translated" only if every unit inside it differs from the freshly reconstructed original text. A partially-edited chunk (e.g. someone hand-edited one paragraph in Perseus Desktop but left another untouched) will be re-translated in full on resume, overwriting the hand-edit — this CLI doesn't do unit-level patching within a chunk, only whole-chunk skip/redo.
  • Reference Attention (Core's heuristic "needs human review" classifier) runs and annotates the IR during generation, matching Core's normal pipeline flow, but this CLI does not currently surface those annotations anywhere (Core doesn't persist them in a Translation Session either).

About

A headless CLI that translates an English Wikipedia article into a target Wikipedia language.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages